vanilla-agent 1.29.0 → 1.30.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 +24 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.global.js +46 -46
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +24 -24
- 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/reasoning-bubble.ts +3 -0
- package/src/components/tool-bubble.ts +3 -0
- package/src/styles/widget.css +6 -2
- package/src/ui.ts +137 -26
- package/src/utils/morph.ts +36 -0
package/dist/widget.css
CHANGED
|
@@ -1240,8 +1240,12 @@
|
|
|
1240
1240
|
margin-top: 0.5rem;
|
|
1241
1241
|
padding-top: 0.5rem;
|
|
1242
1242
|
border-top: 1px solid var(--cw-divider, #f1f5f9);
|
|
1243
|
-
|
|
1244
|
-
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/* Fade in animation only for "always" visibility mode (not hover) */
|
|
1246
|
+
/* forwards ensures final state is kept, idiomorph preserves element so animation only plays once */
|
|
1247
|
+
.tvw-message-actions:not(.tvw-message-actions-hover) {
|
|
1248
|
+
animation: tvw-message-actions-fade-in 0.3s ease-out forwards;
|
|
1245
1249
|
}
|
|
1246
1250
|
|
|
1247
1251
|
/* Action bar alignment */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"src"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"idiomorph": "^0.7.4",
|
|
26
27
|
"lucide": "^0.552.0",
|
|
27
28
|
"marked": "^12.0.2",
|
|
28
29
|
"partial-json": "^0.1.7",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
declare module "idiomorph" {
|
|
2
|
+
export interface IdiomorphCallbacks {
|
|
3
|
+
beforeNodeAdded?: (node: Node) => boolean | void;
|
|
4
|
+
afterNodeAdded?: (node: Node) => void;
|
|
5
|
+
beforeNodeMorphed?: (oldNode: Node, newNode: Node) => boolean | void;
|
|
6
|
+
afterNodeMorphed?: (oldNode: Node, newNode: Node) => void;
|
|
7
|
+
beforeNodeRemoved?: (node: Node) => boolean | void;
|
|
8
|
+
afterNodeRemoved?: (node: Node) => void;
|
|
9
|
+
beforeAttributeUpdated?: (
|
|
10
|
+
attributeName: string,
|
|
11
|
+
node: Node,
|
|
12
|
+
mutationType: "update" | "remove"
|
|
13
|
+
) => boolean | void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IdiomorphOptions {
|
|
17
|
+
morphStyle?: "outerHTML" | "innerHTML";
|
|
18
|
+
ignoreActive?: boolean;
|
|
19
|
+
ignoreActiveValue?: boolean;
|
|
20
|
+
restoreFocus?: boolean;
|
|
21
|
+
callbacks?: IdiomorphCallbacks;
|
|
22
|
+
head?: {
|
|
23
|
+
style?: "merge" | "append" | "morph" | "none";
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IdiomorphStatic {
|
|
28
|
+
morph(
|
|
29
|
+
oldNode: Element | Document,
|
|
30
|
+
newContent: Element | Node | string,
|
|
31
|
+
options?: IdiomorphOptions
|
|
32
|
+
): void;
|
|
33
|
+
defaults: IdiomorphOptions;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Idiomorph: IdiomorphStatic;
|
|
37
|
+
}
|
|
@@ -246,6 +246,9 @@ export const createMessageActions = (
|
|
|
246
246
|
visibility === "hover" ? "tvw-message-actions-hover" : ""
|
|
247
247
|
}`
|
|
248
248
|
);
|
|
249
|
+
// Set id for idiomorph matching (prevents recreation on morph)
|
|
250
|
+
container.id = `actions-${message.id}`;
|
|
251
|
+
container.setAttribute("data-actions-for", message.id);
|
|
249
252
|
|
|
250
253
|
// Track vote state for this message
|
|
251
254
|
let currentVote: "upvote" | "downvote" | null = null;
|
|
@@ -414,6 +417,9 @@ export const createStandardBubble = (
|
|
|
414
417
|
// Create the bubble element
|
|
415
418
|
const classes = getBubbleClasses(message.role, layout);
|
|
416
419
|
const bubble = createElement("div", classes.join(" "));
|
|
420
|
+
// Set id for idiomorph matching
|
|
421
|
+
bubble.id = `bubble-${message.id}`;
|
|
422
|
+
bubble.setAttribute("data-message-id", message.id);
|
|
417
423
|
|
|
418
424
|
// Add message content
|
|
419
425
|
const contentDiv = document.createElement("div");
|
|
@@ -26,6 +26,9 @@ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement
|
|
|
26
26
|
"tvw-py-0"
|
|
27
27
|
].join(" ")
|
|
28
28
|
);
|
|
29
|
+
// Set id for idiomorph matching
|
|
30
|
+
bubble.id = `bubble-${message.id}`;
|
|
31
|
+
bubble.setAttribute("data-message-id", message.id);
|
|
29
32
|
|
|
30
33
|
if (!reasoning) {
|
|
31
34
|
return bubble;
|
|
@@ -28,6 +28,9 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
28
28
|
"tvw-py-0"
|
|
29
29
|
].join(" ")
|
|
30
30
|
);
|
|
31
|
+
// Set id for idiomorph matching
|
|
32
|
+
bubble.id = `bubble-${message.id}`;
|
|
33
|
+
bubble.setAttribute("data-message-id", message.id);
|
|
31
34
|
|
|
32
35
|
// Apply bubble-level styles
|
|
33
36
|
if (toolCallConfig.backgroundColor) {
|
package/src/styles/widget.css
CHANGED
|
@@ -1240,8 +1240,12 @@
|
|
|
1240
1240
|
margin-top: 0.5rem;
|
|
1241
1241
|
padding-top: 0.5rem;
|
|
1242
1242
|
border-top: 1px solid var(--cw-divider, #f1f5f9);
|
|
1243
|
-
|
|
1244
|
-
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/* Fade in animation only for "always" visibility mode (not hover) */
|
|
1246
|
+
/* forwards ensures final state is kept, idiomorph preserves element so animation only plays once */
|
|
1247
|
+
.tvw-message-actions:not(.tvw-message-actions-hover) {
|
|
1248
|
+
animation: tvw-message-actions-fade-in 0.3s ease-out forwards;
|
|
1245
1249
|
}
|
|
1246
1250
|
|
|
1247
1251
|
/* Action bar alignment */
|
package/src/ui.ts
CHANGED
|
@@ -17,9 +17,11 @@ 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";
|
|
@@ -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;
|
|
@@ -904,8 +907,8 @@ export const createAgentExperience = (
|
|
|
904
907
|
messages: AgentWidgetMessage[],
|
|
905
908
|
transform: MessageTransform
|
|
906
909
|
) => {
|
|
907
|
-
container
|
|
908
|
-
const
|
|
910
|
+
// Build new content in a temporary container for morphing
|
|
911
|
+
const tempContainer = document.createElement("div");
|
|
909
912
|
|
|
910
913
|
messages.forEach((message) => {
|
|
911
914
|
let bubble: HTMLElement | null = null;
|
|
@@ -976,8 +979,8 @@ export const createAgentExperience = (
|
|
|
976
979
|
});
|
|
977
980
|
if (componentBubble) {
|
|
978
981
|
// Wrap component in standard bubble styling
|
|
979
|
-
const
|
|
980
|
-
|
|
982
|
+
const componentWrapper = document.createElement("div");
|
|
983
|
+
componentWrapper.className = [
|
|
981
984
|
"vanilla-message-bubble",
|
|
982
985
|
"tvw-max-w-[85%]",
|
|
983
986
|
"tvw-rounded-2xl",
|
|
@@ -986,7 +989,9 @@ export const createAgentExperience = (
|
|
|
986
989
|
"tvw-border-cw-message-border",
|
|
987
990
|
"tvw-p-4"
|
|
988
991
|
].join(" ");
|
|
989
|
-
|
|
992
|
+
// Set id for idiomorph matching
|
|
993
|
+
componentWrapper.id = `bubble-${message.id}`;
|
|
994
|
+
componentWrapper.setAttribute("data-message-id", message.id);
|
|
990
995
|
|
|
991
996
|
// Add text content above component if present (combined text+component response)
|
|
992
997
|
if (message.content && message.content.trim()) {
|
|
@@ -998,11 +1003,11 @@ export const createAgentExperience = (
|
|
|
998
1003
|
streaming: Boolean(message.streaming),
|
|
999
1004
|
raw: message.rawContent
|
|
1000
1005
|
});
|
|
1001
|
-
|
|
1006
|
+
componentWrapper.appendChild(textDiv);
|
|
1002
1007
|
}
|
|
1003
1008
|
|
|
1004
|
-
|
|
1005
|
-
bubble =
|
|
1009
|
+
componentWrapper.appendChild(componentBubble);
|
|
1010
|
+
bubble = componentWrapper;
|
|
1006
1011
|
}
|
|
1007
1012
|
}
|
|
1008
1013
|
}
|
|
@@ -1048,11 +1053,14 @@ export const createAgentExperience = (
|
|
|
1048
1053
|
|
|
1049
1054
|
const wrapper = document.createElement("div");
|
|
1050
1055
|
wrapper.className = "tvw-flex";
|
|
1056
|
+
// Set id for idiomorph matching
|
|
1057
|
+
wrapper.id = `wrapper-${message.id}`;
|
|
1058
|
+
wrapper.setAttribute("data-wrapper-id", message.id);
|
|
1051
1059
|
if (message.role === "user") {
|
|
1052
1060
|
wrapper.classList.add("tvw-justify-end");
|
|
1053
1061
|
}
|
|
1054
1062
|
wrapper.appendChild(bubble);
|
|
1055
|
-
|
|
1063
|
+
tempContainer.appendChild(wrapper);
|
|
1056
1064
|
});
|
|
1057
1065
|
|
|
1058
1066
|
// Add standalone typing indicator only if streaming but no assistant message is streaming yet
|
|
@@ -1085,16 +1093,21 @@ export const createAgentExperience = (
|
|
|
1085
1093
|
"tvw-px-5",
|
|
1086
1094
|
"tvw-py-3"
|
|
1087
1095
|
].join(" ");
|
|
1096
|
+
typingBubble.setAttribute("data-typing-indicator", "true");
|
|
1088
1097
|
|
|
1089
1098
|
typingBubble.appendChild(typingIndicator);
|
|
1090
1099
|
|
|
1091
1100
|
const typingWrapper = document.createElement("div");
|
|
1092
1101
|
typingWrapper.className = "tvw-flex";
|
|
1102
|
+
// Set id for idiomorph matching
|
|
1103
|
+
typingWrapper.id = "wrapper-typing-indicator";
|
|
1104
|
+
typingWrapper.setAttribute("data-wrapper-id", "typing-indicator");
|
|
1093
1105
|
typingWrapper.appendChild(typingBubble);
|
|
1094
|
-
|
|
1106
|
+
tempContainer.appendChild(typingWrapper);
|
|
1095
1107
|
}
|
|
1096
1108
|
|
|
1097
|
-
container
|
|
1109
|
+
// Use idiomorph to morph the container contents
|
|
1110
|
+
morphMessages(container, tempContainer);
|
|
1098
1111
|
// Defer scroll to next frame for smoother animation and to prevent jolt
|
|
1099
1112
|
// This allows the browser to update layout (e.g., typing indicator removal) before scrolling
|
|
1100
1113
|
// Use double RAF to ensure layout has fully settled before starting scroll animation
|
|
@@ -1902,6 +1915,66 @@ export const createAgentExperience = (
|
|
|
1902
1915
|
headerSubtitle.textContent = config.launcher.subtitle;
|
|
1903
1916
|
}
|
|
1904
1917
|
|
|
1918
|
+
// Update header layout if it changed
|
|
1919
|
+
const headerLayoutConfig = config.layout?.header;
|
|
1920
|
+
const headerLayoutChanged = headerLayoutConfig?.layout !== prevHeaderLayout;
|
|
1921
|
+
|
|
1922
|
+
if (headerLayoutChanged && header) {
|
|
1923
|
+
// Rebuild header with new layout
|
|
1924
|
+
const newHeaderElements = headerLayoutConfig
|
|
1925
|
+
? buildHeaderWithLayout(config, headerLayoutConfig, {
|
|
1926
|
+
showClose: launcherEnabled,
|
|
1927
|
+
onClose: () => setOpenState(false, "user")
|
|
1928
|
+
})
|
|
1929
|
+
: buildHeader({
|
|
1930
|
+
config,
|
|
1931
|
+
showClose: launcherEnabled,
|
|
1932
|
+
onClose: () => setOpenState(false, "user")
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
// Replace the old header with the new one
|
|
1936
|
+
header.replaceWith(newHeaderElements.header);
|
|
1937
|
+
|
|
1938
|
+
// Update references
|
|
1939
|
+
header = newHeaderElements.header;
|
|
1940
|
+
iconHolder = newHeaderElements.iconHolder;
|
|
1941
|
+
headerTitle = newHeaderElements.headerTitle;
|
|
1942
|
+
headerSubtitle = newHeaderElements.headerSubtitle;
|
|
1943
|
+
closeButton = newHeaderElements.closeButton;
|
|
1944
|
+
|
|
1945
|
+
prevHeaderLayout = headerLayoutConfig?.layout;
|
|
1946
|
+
} else if (headerLayoutConfig) {
|
|
1947
|
+
// Apply visibility settings without rebuilding
|
|
1948
|
+
if (iconHolder) {
|
|
1949
|
+
iconHolder.style.display = headerLayoutConfig.showIcon === false ? "none" : "";
|
|
1950
|
+
}
|
|
1951
|
+
if (headerTitle) {
|
|
1952
|
+
headerTitle.style.display = headerLayoutConfig.showTitle === false ? "none" : "";
|
|
1953
|
+
}
|
|
1954
|
+
if (headerSubtitle) {
|
|
1955
|
+
headerSubtitle.style.display = headerLayoutConfig.showSubtitle === false ? "none" : "";
|
|
1956
|
+
}
|
|
1957
|
+
if (closeButton) {
|
|
1958
|
+
closeButton.style.display = headerLayoutConfig.showCloseButton === false ? "none" : "";
|
|
1959
|
+
}
|
|
1960
|
+
if (panelElements.clearChatButtonWrapper) {
|
|
1961
|
+
// showClearChat explicitly controls visibility when set
|
|
1962
|
+
const showClearChat = headerLayoutConfig.showClearChat;
|
|
1963
|
+
if (showClearChat !== undefined) {
|
|
1964
|
+
panelElements.clearChatButtonWrapper.style.display = showClearChat ? "" : "none";
|
|
1965
|
+
// When clear chat is hidden, close button needs ml-auto to stay right-aligned
|
|
1966
|
+
const { closeButtonWrapper } = panelElements;
|
|
1967
|
+
if (closeButtonWrapper && !closeButtonWrapper.classList.contains("tvw-absolute")) {
|
|
1968
|
+
if (showClearChat) {
|
|
1969
|
+
closeButtonWrapper.classList.remove("tvw-ml-auto");
|
|
1970
|
+
} else {
|
|
1971
|
+
closeButtonWrapper.classList.add("tvw-ml-auto");
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1905
1978
|
// Only update open state if launcher enabled state changed or autoExpand value changed
|
|
1906
1979
|
const launcherEnabledChanged = launcherEnabled !== prevLauncherEnabled;
|
|
1907
1980
|
const autoExpandChanged = autoExpand !== prevAutoExpand;
|
|
@@ -1937,20 +2010,23 @@ export const createAgentExperience = (
|
|
|
1937
2010
|
// Update panel icon sizes
|
|
1938
2011
|
const launcher = config.launcher ?? {};
|
|
1939
2012
|
const headerIconHidden = launcher.headerIconHidden ?? false;
|
|
2013
|
+
const layoutShowIcon = config.layout?.header?.showIcon;
|
|
2014
|
+
// Hide icon if either headerIconHidden is true OR layout.header.showIcon is false
|
|
2015
|
+
const shouldHideIcon = headerIconHidden || layoutShowIcon === false;
|
|
1940
2016
|
const headerIconName = launcher.headerIconName;
|
|
1941
2017
|
const headerIconSize = launcher.headerIconSize ?? "48px";
|
|
1942
|
-
|
|
2018
|
+
|
|
1943
2019
|
if (iconHolder) {
|
|
1944
|
-
const
|
|
1945
|
-
const headerCopy =
|
|
1946
|
-
|
|
2020
|
+
const headerEl = container.querySelector(".tvw-border-b-cw-divider");
|
|
2021
|
+
const headerCopy = headerEl?.querySelector(".tvw-flex-col");
|
|
2022
|
+
|
|
1947
2023
|
// Handle hide/show
|
|
1948
|
-
if (
|
|
2024
|
+
if (shouldHideIcon) {
|
|
1949
2025
|
// Hide iconHolder
|
|
1950
2026
|
iconHolder.style.display = "none";
|
|
1951
2027
|
// Ensure headerCopy is still in header
|
|
1952
|
-
if (
|
|
1953
|
-
|
|
2028
|
+
if (headerEl && headerCopy && !headerEl.contains(headerCopy)) {
|
|
2029
|
+
headerEl.insertBefore(headerCopy, headerEl.firstChild);
|
|
1954
2030
|
}
|
|
1955
2031
|
} else {
|
|
1956
2032
|
// Show iconHolder
|
|
@@ -1959,13 +2035,13 @@ export const createAgentExperience = (
|
|
|
1959
2035
|
iconHolder.style.width = headerIconSize;
|
|
1960
2036
|
|
|
1961
2037
|
// Ensure iconHolder is before headerCopy in header
|
|
1962
|
-
if (
|
|
1963
|
-
if (!
|
|
1964
|
-
|
|
2038
|
+
if (headerEl && headerCopy) {
|
|
2039
|
+
if (!headerEl.contains(iconHolder)) {
|
|
2040
|
+
headerEl.insertBefore(iconHolder, headerCopy);
|
|
1965
2041
|
} else if (iconHolder.nextSibling !== headerCopy) {
|
|
1966
2042
|
// Reorder if needed
|
|
1967
2043
|
iconHolder.remove();
|
|
1968
|
-
|
|
2044
|
+
headerEl.insertBefore(iconHolder, headerCopy);
|
|
1969
2045
|
}
|
|
1970
2046
|
}
|
|
1971
2047
|
|
|
@@ -2015,7 +2091,26 @@ export const createAgentExperience = (
|
|
|
2015
2091
|
}
|
|
2016
2092
|
}
|
|
2017
2093
|
}
|
|
2094
|
+
|
|
2095
|
+
// Handle title/subtitle visibility from layout config
|
|
2096
|
+
const layoutShowTitle = config.layout?.header?.showTitle;
|
|
2097
|
+
const layoutShowSubtitle = config.layout?.header?.showSubtitle;
|
|
2098
|
+
if (headerTitle) {
|
|
2099
|
+
headerTitle.style.display = layoutShowTitle === false ? "none" : "";
|
|
2100
|
+
}
|
|
2101
|
+
if (headerSubtitle) {
|
|
2102
|
+
headerSubtitle.style.display = layoutShowSubtitle === false ? "none" : "";
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2018
2105
|
if (closeButton) {
|
|
2106
|
+
// Handle close button visibility from layout config
|
|
2107
|
+
const layoutShowCloseButton = config.layout?.header?.showCloseButton;
|
|
2108
|
+
if (layoutShowCloseButton === false) {
|
|
2109
|
+
closeButton.style.display = "none";
|
|
2110
|
+
} else {
|
|
2111
|
+
closeButton.style.display = "";
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2019
2114
|
const closeButtonSize = launcher.closeButtonSize ?? "32px";
|
|
2020
2115
|
const closeButtonPlacement = launcher.closeButtonPlacement ?? "inline";
|
|
2021
2116
|
closeButton.style.height = closeButtonSize;
|
|
@@ -2189,17 +2284,33 @@ export const createAgentExperience = (
|
|
|
2189
2284
|
if (clearChatButton) {
|
|
2190
2285
|
const clearChatConfig = launcher.clearChat ?? {};
|
|
2191
2286
|
const clearChatEnabled = clearChatConfig.enabled ?? true;
|
|
2287
|
+
const layoutShowClearChat = config.layout?.header?.showClearChat;
|
|
2288
|
+
// layout.header.showClearChat takes precedence if explicitly set
|
|
2289
|
+
// Otherwise fall back to launcher.clearChat.enabled
|
|
2290
|
+
const shouldShowClearChat = layoutShowClearChat !== undefined
|
|
2291
|
+
? layoutShowClearChat
|
|
2292
|
+
: clearChatEnabled;
|
|
2192
2293
|
const clearChatPlacement = clearChatConfig.placement ?? "inline";
|
|
2193
2294
|
|
|
2194
|
-
// Show/hide button based on
|
|
2295
|
+
// Show/hide button based on layout config (primary) or launcher config (fallback)
|
|
2195
2296
|
if (clearChatButtonWrapper) {
|
|
2196
|
-
clearChatButtonWrapper.style.display =
|
|
2297
|
+
clearChatButtonWrapper.style.display = shouldShowClearChat ? "" : "none";
|
|
2298
|
+
|
|
2299
|
+
// When clear chat is hidden, close button needs ml-auto to stay right-aligned
|
|
2300
|
+
const { closeButtonWrapper } = panelElements;
|
|
2301
|
+
if (closeButtonWrapper && !closeButtonWrapper.classList.contains("tvw-absolute")) {
|
|
2302
|
+
if (shouldShowClearChat) {
|
|
2303
|
+
closeButtonWrapper.classList.remove("tvw-ml-auto");
|
|
2304
|
+
} else {
|
|
2305
|
+
closeButtonWrapper.classList.add("tvw-ml-auto");
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2197
2308
|
|
|
2198
2309
|
// Update placement if changed
|
|
2199
2310
|
const isTopRight = clearChatPlacement === "top-right";
|
|
2200
2311
|
const currentlyTopRight = clearChatButtonWrapper.classList.contains("tvw-absolute");
|
|
2201
2312
|
|
|
2202
|
-
if (isTopRight !== currentlyTopRight &&
|
|
2313
|
+
if (isTopRight !== currentlyTopRight && shouldShowClearChat) {
|
|
2203
2314
|
clearChatButtonWrapper.remove();
|
|
2204
2315
|
|
|
2205
2316
|
if (isTopRight) {
|
|
@@ -2239,7 +2350,7 @@ export const createAgentExperience = (
|
|
|
2239
2350
|
}
|
|
2240
2351
|
}
|
|
2241
2352
|
|
|
2242
|
-
if (
|
|
2353
|
+
if (shouldShowClearChat) {
|
|
2243
2354
|
// Update size
|
|
2244
2355
|
const clearChatSize = clearChatConfig.size ?? "32px";
|
|
2245
2356
|
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
|
+
};
|