vanilla-agent 1.29.0 → 1.31.0
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/dist/index.cjs +23 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.global.js +46 -46
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +23 -23
- package/dist/index.js.map +1 -1
- package/dist/widget.css +6 -2
- package/package.json +2 -1
- package/src/@types/idiomorph.d.ts +37 -0
- package/src/components/message-bubble.ts +6 -0
- package/src/components/panel.ts +19 -2
- package/src/components/reasoning-bubble.ts +33 -23
- package/src/components/tool-bubble.ts +34 -23
- package/src/styles/widget.css +6 -2
- package/src/types.ts +13 -0
- package/src/ui.ts +205 -28
- package/src/utils/morph.ts +36 -0
package/src/ui.ts
CHANGED
|
@@ -17,15 +17,17 @@ import {
|
|
|
17
17
|
import { applyThemeVariables, createThemeObserver } from "./utils/theme";
|
|
18
18
|
import { renderLucideIcon } from "./utils/icons";
|
|
19
19
|
import { createElement } from "./utils/dom";
|
|
20
|
+
import { morphMessages } from "./utils/morph";
|
|
20
21
|
import { statusCopy } from "./utils/constants";
|
|
21
22
|
import { createLauncherButton } from "./components/launcher";
|
|
22
23
|
import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
|
|
24
|
+
import { buildHeaderWithLayout } from "./components/header-layouts";
|
|
23
25
|
import { positionMap } from "./utils/positioning";
|
|
24
26
|
import type { HeaderElements, ComposerElements } from "./components/panel";
|
|
25
27
|
import { MessageTransform, MessageActionCallbacks } from "./components/message-bubble";
|
|
26
28
|
import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
|
|
27
|
-
import { createReasoningBubble } from "./components/reasoning-bubble";
|
|
28
|
-
import { createToolBubble } from "./components/tool-bubble";
|
|
29
|
+
import { createReasoningBubble, reasoningExpansionState, updateReasoningBubbleUI } from "./components/reasoning-bubble";
|
|
30
|
+
import { createToolBubble, toolExpansionState, updateToolBubbleUI } from "./components/tool-bubble";
|
|
29
31
|
import { createSuggestions } from "./components/suggestions";
|
|
30
32
|
import { enhanceWithForms } from "./components/forms";
|
|
31
33
|
import { pluginRegistry } from "./plugins/registry";
|
|
@@ -235,6 +237,7 @@ export const createAgentExperience = (
|
|
|
235
237
|
let autoExpand = config.launcher?.autoExpand ?? false;
|
|
236
238
|
let prevAutoExpand = autoExpand;
|
|
237
239
|
let prevLauncherEnabled = launcherEnabled;
|
|
240
|
+
let prevHeaderLayout = config.layout?.header?.layout;
|
|
238
241
|
let open = launcherEnabled ? autoExpand : true;
|
|
239
242
|
let postprocess = buildPostprocessor(config, actionManager);
|
|
240
243
|
let showReasoning = config.features?.showReasoning ?? true;
|
|
@@ -450,6 +453,60 @@ export const createAgentExperience = (
|
|
|
450
453
|
// Render custom slots
|
|
451
454
|
renderSlots();
|
|
452
455
|
|
|
456
|
+
// Add event delegation for reasoning and tool bubble expansion
|
|
457
|
+
// This handles clicks even after idiomorph morphs the DOM
|
|
458
|
+
const handleBubbleExpansion = (event: Event) => {
|
|
459
|
+
const target = event.target as HTMLElement;
|
|
460
|
+
|
|
461
|
+
// Check if the click/keypress is on an expand header button
|
|
462
|
+
const headerButton = target.closest('button[data-expand-header="true"]') as HTMLElement;
|
|
463
|
+
if (!headerButton) return;
|
|
464
|
+
|
|
465
|
+
// Find the parent bubble element
|
|
466
|
+
const bubble = headerButton.closest('.vanilla-reasoning-bubble, .vanilla-tool-bubble') as HTMLElement;
|
|
467
|
+
if (!bubble) return;
|
|
468
|
+
|
|
469
|
+
// Get message ID from bubble
|
|
470
|
+
const messageId = bubble.getAttribute('data-message-id');
|
|
471
|
+
if (!messageId) return;
|
|
472
|
+
|
|
473
|
+
const bubbleType = headerButton.getAttribute('data-bubble-type');
|
|
474
|
+
|
|
475
|
+
// Toggle expansion state
|
|
476
|
+
if (bubbleType === 'reasoning') {
|
|
477
|
+
if (reasoningExpansionState.has(messageId)) {
|
|
478
|
+
reasoningExpansionState.delete(messageId);
|
|
479
|
+
} else {
|
|
480
|
+
reasoningExpansionState.add(messageId);
|
|
481
|
+
}
|
|
482
|
+
updateReasoningBubbleUI(messageId, bubble);
|
|
483
|
+
} else if (bubbleType === 'tool') {
|
|
484
|
+
if (toolExpansionState.has(messageId)) {
|
|
485
|
+
toolExpansionState.delete(messageId);
|
|
486
|
+
} else {
|
|
487
|
+
toolExpansionState.add(messageId);
|
|
488
|
+
}
|
|
489
|
+
updateToolBubbleUI(messageId, bubble, config);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// Attach event listeners to messagesWrapper for event delegation
|
|
494
|
+
messagesWrapper.addEventListener('pointerdown', (event) => {
|
|
495
|
+
const target = event.target as HTMLElement;
|
|
496
|
+
if (target.closest('button[data-expand-header="true"]')) {
|
|
497
|
+
event.preventDefault();
|
|
498
|
+
handleBubbleExpansion(event);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
messagesWrapper.addEventListener('keydown', (event) => {
|
|
503
|
+
const target = event.target as HTMLElement;
|
|
504
|
+
if ((event.key === 'Enter' || event.key === ' ') && target.closest('button[data-expand-header="true"]')) {
|
|
505
|
+
event.preventDefault();
|
|
506
|
+
handleBubbleExpansion(event);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
453
510
|
panel.appendChild(container);
|
|
454
511
|
mount.appendChild(wrapper);
|
|
455
512
|
|
|
@@ -904,8 +961,8 @@ export const createAgentExperience = (
|
|
|
904
961
|
messages: AgentWidgetMessage[],
|
|
905
962
|
transform: MessageTransform
|
|
906
963
|
) => {
|
|
907
|
-
container
|
|
908
|
-
const
|
|
964
|
+
// Build new content in a temporary container for morphing
|
|
965
|
+
const tempContainer = document.createElement("div");
|
|
909
966
|
|
|
910
967
|
messages.forEach((message) => {
|
|
911
968
|
let bubble: HTMLElement | null = null;
|
|
@@ -976,8 +1033,8 @@ export const createAgentExperience = (
|
|
|
976
1033
|
});
|
|
977
1034
|
if (componentBubble) {
|
|
978
1035
|
// Wrap component in standard bubble styling
|
|
979
|
-
const
|
|
980
|
-
|
|
1036
|
+
const componentWrapper = document.createElement("div");
|
|
1037
|
+
componentWrapper.className = [
|
|
981
1038
|
"vanilla-message-bubble",
|
|
982
1039
|
"tvw-max-w-[85%]",
|
|
983
1040
|
"tvw-rounded-2xl",
|
|
@@ -986,7 +1043,9 @@ export const createAgentExperience = (
|
|
|
986
1043
|
"tvw-border-cw-message-border",
|
|
987
1044
|
"tvw-p-4"
|
|
988
1045
|
].join(" ");
|
|
989
|
-
|
|
1046
|
+
// Set id for idiomorph matching
|
|
1047
|
+
componentWrapper.id = `bubble-${message.id}`;
|
|
1048
|
+
componentWrapper.setAttribute("data-message-id", message.id);
|
|
990
1049
|
|
|
991
1050
|
// Add text content above component if present (combined text+component response)
|
|
992
1051
|
if (message.content && message.content.trim()) {
|
|
@@ -998,11 +1057,11 @@ export const createAgentExperience = (
|
|
|
998
1057
|
streaming: Boolean(message.streaming),
|
|
999
1058
|
raw: message.rawContent
|
|
1000
1059
|
});
|
|
1001
|
-
|
|
1060
|
+
componentWrapper.appendChild(textDiv);
|
|
1002
1061
|
}
|
|
1003
1062
|
|
|
1004
|
-
|
|
1005
|
-
bubble =
|
|
1063
|
+
componentWrapper.appendChild(componentBubble);
|
|
1064
|
+
bubble = componentWrapper;
|
|
1006
1065
|
}
|
|
1007
1066
|
}
|
|
1008
1067
|
}
|
|
@@ -1048,11 +1107,14 @@ export const createAgentExperience = (
|
|
|
1048
1107
|
|
|
1049
1108
|
const wrapper = document.createElement("div");
|
|
1050
1109
|
wrapper.className = "tvw-flex";
|
|
1110
|
+
// Set id for idiomorph matching
|
|
1111
|
+
wrapper.id = `wrapper-${message.id}`;
|
|
1112
|
+
wrapper.setAttribute("data-wrapper-id", message.id);
|
|
1051
1113
|
if (message.role === "user") {
|
|
1052
1114
|
wrapper.classList.add("tvw-justify-end");
|
|
1053
1115
|
}
|
|
1054
1116
|
wrapper.appendChild(bubble);
|
|
1055
|
-
|
|
1117
|
+
tempContainer.appendChild(wrapper);
|
|
1056
1118
|
});
|
|
1057
1119
|
|
|
1058
1120
|
// Add standalone typing indicator only if streaming but no assistant message is streaming yet
|
|
@@ -1085,16 +1147,21 @@ export const createAgentExperience = (
|
|
|
1085
1147
|
"tvw-px-5",
|
|
1086
1148
|
"tvw-py-3"
|
|
1087
1149
|
].join(" ");
|
|
1150
|
+
typingBubble.setAttribute("data-typing-indicator", "true");
|
|
1088
1151
|
|
|
1089
1152
|
typingBubble.appendChild(typingIndicator);
|
|
1090
1153
|
|
|
1091
1154
|
const typingWrapper = document.createElement("div");
|
|
1092
1155
|
typingWrapper.className = "tvw-flex";
|
|
1156
|
+
// Set id for idiomorph matching
|
|
1157
|
+
typingWrapper.id = "wrapper-typing-indicator";
|
|
1158
|
+
typingWrapper.setAttribute("data-wrapper-id", "typing-indicator");
|
|
1093
1159
|
typingWrapper.appendChild(typingBubble);
|
|
1094
|
-
|
|
1160
|
+
tempContainer.appendChild(typingWrapper);
|
|
1095
1161
|
}
|
|
1096
1162
|
|
|
1097
|
-
container
|
|
1163
|
+
// Use idiomorph to morph the container contents
|
|
1164
|
+
morphMessages(container, tempContainer);
|
|
1098
1165
|
// Defer scroll to next frame for smoother animation and to prevent jolt
|
|
1099
1166
|
// This allows the browser to update layout (e.g., typing indicator removal) before scrolling
|
|
1100
1167
|
// Use double RAF to ensure layout has fully settled before starting scroll animation
|
|
@@ -1902,6 +1969,78 @@ export const createAgentExperience = (
|
|
|
1902
1969
|
headerSubtitle.textContent = config.launcher.subtitle;
|
|
1903
1970
|
}
|
|
1904
1971
|
|
|
1972
|
+
// Update header layout if it changed
|
|
1973
|
+
const headerLayoutConfig = config.layout?.header;
|
|
1974
|
+
const headerLayoutChanged = headerLayoutConfig?.layout !== prevHeaderLayout;
|
|
1975
|
+
|
|
1976
|
+
if (headerLayoutChanged && header) {
|
|
1977
|
+
// Rebuild header with new layout
|
|
1978
|
+
const newHeaderElements = headerLayoutConfig
|
|
1979
|
+
? buildHeaderWithLayout(config, headerLayoutConfig, {
|
|
1980
|
+
showClose: launcherEnabled,
|
|
1981
|
+
onClose: () => setOpenState(false, "user")
|
|
1982
|
+
})
|
|
1983
|
+
: buildHeader({
|
|
1984
|
+
config,
|
|
1985
|
+
showClose: launcherEnabled,
|
|
1986
|
+
onClose: () => setOpenState(false, "user")
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
// Replace the old header with the new one
|
|
1990
|
+
header.replaceWith(newHeaderElements.header);
|
|
1991
|
+
|
|
1992
|
+
// Update references
|
|
1993
|
+
header = newHeaderElements.header;
|
|
1994
|
+
iconHolder = newHeaderElements.iconHolder;
|
|
1995
|
+
headerTitle = newHeaderElements.headerTitle;
|
|
1996
|
+
headerSubtitle = newHeaderElements.headerSubtitle;
|
|
1997
|
+
closeButton = newHeaderElements.closeButton;
|
|
1998
|
+
|
|
1999
|
+
prevHeaderLayout = headerLayoutConfig?.layout;
|
|
2000
|
+
} else if (headerLayoutConfig) {
|
|
2001
|
+
// Apply visibility settings without rebuilding
|
|
2002
|
+
if (iconHolder) {
|
|
2003
|
+
iconHolder.style.display = headerLayoutConfig.showIcon === false ? "none" : "";
|
|
2004
|
+
}
|
|
2005
|
+
if (headerTitle) {
|
|
2006
|
+
headerTitle.style.display = headerLayoutConfig.showTitle === false ? "none" : "";
|
|
2007
|
+
}
|
|
2008
|
+
if (headerSubtitle) {
|
|
2009
|
+
headerSubtitle.style.display = headerLayoutConfig.showSubtitle === false ? "none" : "";
|
|
2010
|
+
}
|
|
2011
|
+
if (closeButton) {
|
|
2012
|
+
closeButton.style.display = headerLayoutConfig.showCloseButton === false ? "none" : "";
|
|
2013
|
+
}
|
|
2014
|
+
if (panelElements.clearChatButtonWrapper) {
|
|
2015
|
+
// showClearChat explicitly controls visibility when set
|
|
2016
|
+
const showClearChat = headerLayoutConfig.showClearChat;
|
|
2017
|
+
if (showClearChat !== undefined) {
|
|
2018
|
+
panelElements.clearChatButtonWrapper.style.display = showClearChat ? "" : "none";
|
|
2019
|
+
// When clear chat is hidden, close button needs ml-auto to stay right-aligned
|
|
2020
|
+
const { closeButtonWrapper } = panelElements;
|
|
2021
|
+
if (closeButtonWrapper && !closeButtonWrapper.classList.contains("tvw-absolute")) {
|
|
2022
|
+
if (showClearChat) {
|
|
2023
|
+
closeButtonWrapper.classList.remove("tvw-ml-auto");
|
|
2024
|
+
} else {
|
|
2025
|
+
closeButtonWrapper.classList.add("tvw-ml-auto");
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Update header visibility based on layout.showHeader
|
|
2033
|
+
const showHeader = config.layout?.showHeader !== false; // default to true
|
|
2034
|
+
if (header) {
|
|
2035
|
+
header.style.display = showHeader ? "" : "none";
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// Update footer visibility based on layout.showFooter
|
|
2039
|
+
const showFooter = config.layout?.showFooter !== false; // default to true
|
|
2040
|
+
if (footer) {
|
|
2041
|
+
footer.style.display = showFooter ? "" : "none";
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1905
2044
|
// Only update open state if launcher enabled state changed or autoExpand value changed
|
|
1906
2045
|
const launcherEnabledChanged = launcherEnabled !== prevLauncherEnabled;
|
|
1907
2046
|
const autoExpandChanged = autoExpand !== prevAutoExpand;
|
|
@@ -1937,20 +2076,23 @@ export const createAgentExperience = (
|
|
|
1937
2076
|
// Update panel icon sizes
|
|
1938
2077
|
const launcher = config.launcher ?? {};
|
|
1939
2078
|
const headerIconHidden = launcher.headerIconHidden ?? false;
|
|
2079
|
+
const layoutShowIcon = config.layout?.header?.showIcon;
|
|
2080
|
+
// Hide icon if either headerIconHidden is true OR layout.header.showIcon is false
|
|
2081
|
+
const shouldHideIcon = headerIconHidden || layoutShowIcon === false;
|
|
1940
2082
|
const headerIconName = launcher.headerIconName;
|
|
1941
2083
|
const headerIconSize = launcher.headerIconSize ?? "48px";
|
|
1942
|
-
|
|
2084
|
+
|
|
1943
2085
|
if (iconHolder) {
|
|
1944
|
-
const
|
|
1945
|
-
const headerCopy =
|
|
1946
|
-
|
|
2086
|
+
const headerEl = container.querySelector(".tvw-border-b-cw-divider");
|
|
2087
|
+
const headerCopy = headerEl?.querySelector(".tvw-flex-col");
|
|
2088
|
+
|
|
1947
2089
|
// Handle hide/show
|
|
1948
|
-
if (
|
|
2090
|
+
if (shouldHideIcon) {
|
|
1949
2091
|
// Hide iconHolder
|
|
1950
2092
|
iconHolder.style.display = "none";
|
|
1951
2093
|
// Ensure headerCopy is still in header
|
|
1952
|
-
if (
|
|
1953
|
-
|
|
2094
|
+
if (headerEl && headerCopy && !headerEl.contains(headerCopy)) {
|
|
2095
|
+
headerEl.insertBefore(headerCopy, headerEl.firstChild);
|
|
1954
2096
|
}
|
|
1955
2097
|
} else {
|
|
1956
2098
|
// Show iconHolder
|
|
@@ -1959,13 +2101,13 @@ export const createAgentExperience = (
|
|
|
1959
2101
|
iconHolder.style.width = headerIconSize;
|
|
1960
2102
|
|
|
1961
2103
|
// Ensure iconHolder is before headerCopy in header
|
|
1962
|
-
if (
|
|
1963
|
-
if (!
|
|
1964
|
-
|
|
2104
|
+
if (headerEl && headerCopy) {
|
|
2105
|
+
if (!headerEl.contains(iconHolder)) {
|
|
2106
|
+
headerEl.insertBefore(iconHolder, headerCopy);
|
|
1965
2107
|
} else if (iconHolder.nextSibling !== headerCopy) {
|
|
1966
2108
|
// Reorder if needed
|
|
1967
2109
|
iconHolder.remove();
|
|
1968
|
-
|
|
2110
|
+
headerEl.insertBefore(iconHolder, headerCopy);
|
|
1969
2111
|
}
|
|
1970
2112
|
}
|
|
1971
2113
|
|
|
@@ -2015,7 +2157,26 @@ export const createAgentExperience = (
|
|
|
2015
2157
|
}
|
|
2016
2158
|
}
|
|
2017
2159
|
}
|
|
2160
|
+
|
|
2161
|
+
// Handle title/subtitle visibility from layout config
|
|
2162
|
+
const layoutShowTitle = config.layout?.header?.showTitle;
|
|
2163
|
+
const layoutShowSubtitle = config.layout?.header?.showSubtitle;
|
|
2164
|
+
if (headerTitle) {
|
|
2165
|
+
headerTitle.style.display = layoutShowTitle === false ? "none" : "";
|
|
2166
|
+
}
|
|
2167
|
+
if (headerSubtitle) {
|
|
2168
|
+
headerSubtitle.style.display = layoutShowSubtitle === false ? "none" : "";
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2018
2171
|
if (closeButton) {
|
|
2172
|
+
// Handle close button visibility from layout config
|
|
2173
|
+
const layoutShowCloseButton = config.layout?.header?.showCloseButton;
|
|
2174
|
+
if (layoutShowCloseButton === false) {
|
|
2175
|
+
closeButton.style.display = "none";
|
|
2176
|
+
} else {
|
|
2177
|
+
closeButton.style.display = "";
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2019
2180
|
const closeButtonSize = launcher.closeButtonSize ?? "32px";
|
|
2020
2181
|
const closeButtonPlacement = launcher.closeButtonPlacement ?? "inline";
|
|
2021
2182
|
closeButton.style.height = closeButtonSize;
|
|
@@ -2189,17 +2350,33 @@ export const createAgentExperience = (
|
|
|
2189
2350
|
if (clearChatButton) {
|
|
2190
2351
|
const clearChatConfig = launcher.clearChat ?? {};
|
|
2191
2352
|
const clearChatEnabled = clearChatConfig.enabled ?? true;
|
|
2353
|
+
const layoutShowClearChat = config.layout?.header?.showClearChat;
|
|
2354
|
+
// layout.header.showClearChat takes precedence if explicitly set
|
|
2355
|
+
// Otherwise fall back to launcher.clearChat.enabled
|
|
2356
|
+
const shouldShowClearChat = layoutShowClearChat !== undefined
|
|
2357
|
+
? layoutShowClearChat
|
|
2358
|
+
: clearChatEnabled;
|
|
2192
2359
|
const clearChatPlacement = clearChatConfig.placement ?? "inline";
|
|
2193
2360
|
|
|
2194
|
-
// Show/hide button based on
|
|
2361
|
+
// Show/hide button based on layout config (primary) or launcher config (fallback)
|
|
2195
2362
|
if (clearChatButtonWrapper) {
|
|
2196
|
-
clearChatButtonWrapper.style.display =
|
|
2363
|
+
clearChatButtonWrapper.style.display = shouldShowClearChat ? "" : "none";
|
|
2364
|
+
|
|
2365
|
+
// When clear chat is hidden, close button needs ml-auto to stay right-aligned
|
|
2366
|
+
const { closeButtonWrapper } = panelElements;
|
|
2367
|
+
if (closeButtonWrapper && !closeButtonWrapper.classList.contains("tvw-absolute")) {
|
|
2368
|
+
if (shouldShowClearChat) {
|
|
2369
|
+
closeButtonWrapper.classList.remove("tvw-ml-auto");
|
|
2370
|
+
} else {
|
|
2371
|
+
closeButtonWrapper.classList.add("tvw-ml-auto");
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2197
2374
|
|
|
2198
2375
|
// Update placement if changed
|
|
2199
2376
|
const isTopRight = clearChatPlacement === "top-right";
|
|
2200
2377
|
const currentlyTopRight = clearChatButtonWrapper.classList.contains("tvw-absolute");
|
|
2201
2378
|
|
|
2202
|
-
if (isTopRight !== currentlyTopRight &&
|
|
2379
|
+
if (isTopRight !== currentlyTopRight && shouldShowClearChat) {
|
|
2203
2380
|
clearChatButtonWrapper.remove();
|
|
2204
2381
|
|
|
2205
2382
|
if (isTopRight) {
|
|
@@ -2239,7 +2416,7 @@ export const createAgentExperience = (
|
|
|
2239
2416
|
}
|
|
2240
2417
|
}
|
|
2241
2418
|
|
|
2242
|
-
if (
|
|
2419
|
+
if (shouldShowClearChat) {
|
|
2243
2420
|
// Update size
|
|
2244
2421
|
const clearChatSize = clearChatConfig.size ?? "32px";
|
|
2245
2422
|
clearChatButton.style.height = clearChatSize;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Idiomorph } from "idiomorph";
|
|
2
|
+
|
|
3
|
+
export type MorphOptions = {
|
|
4
|
+
preserveTypingAnimation?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Morph a container's contents using idiomorph with chat-widget-specific
|
|
9
|
+
* preservation rules for typing indicators.
|
|
10
|
+
*
|
|
11
|
+
* Action buttons are matched by their `id` attribute (set to `actions-{messageId}`)
|
|
12
|
+
* so idiomorph updates them in place rather than recreating them.
|
|
13
|
+
*/
|
|
14
|
+
export const morphMessages = (
|
|
15
|
+
container: HTMLElement,
|
|
16
|
+
newContent: HTMLElement,
|
|
17
|
+
options: MorphOptions = {}
|
|
18
|
+
): void => {
|
|
19
|
+
const { preserveTypingAnimation = true } = options;
|
|
20
|
+
|
|
21
|
+
Idiomorph.morph(container, newContent.innerHTML, {
|
|
22
|
+
morphStyle: "innerHTML",
|
|
23
|
+
callbacks: {
|
|
24
|
+
beforeNodeMorphed(oldNode: Node, newNode: Node): boolean | void {
|
|
25
|
+
if (!(oldNode instanceof HTMLElement)) return;
|
|
26
|
+
|
|
27
|
+
// Preserve typing indicator dots to maintain animation continuity
|
|
28
|
+
if (preserveTypingAnimation) {
|
|
29
|
+
if (oldNode.classList.contains("tvw-animate-typing")) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
};
|