@runtypelabs/persona 3.5.2 → 3.7.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 +46 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.global.js +70 -70
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -46
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +18015 -0
- package/dist/theme-editor.d.cts +3888 -0
- package/dist/theme-editor.d.ts +3888 -0
- package/dist/theme-editor.js +17909 -0
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +33 -0
- package/dist/theme-reference.d.ts +33 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +69 -25
- package/package.json +9 -7
- package/src/components/artifact-card.ts +1 -1
- package/src/components/composer-builder.ts +16 -29
- package/src/components/demo-carousel.ts +5 -5
- package/src/components/event-stream-view.test.ts +142 -0
- package/src/components/event-stream-view.ts +68 -29
- package/src/components/header-builder.ts +2 -2
- package/src/components/launcher.ts +9 -0
- package/src/components/message-bubble.ts +9 -3
- package/src/components/suggestions.ts +1 -1
- package/src/defaults.ts +24 -9
- package/src/scroll-to-bottom-defaults.test.ts +13 -0
- package/src/styles/widget.css +69 -25
- package/src/theme-editor/color-utils.ts +252 -0
- package/src/theme-editor/index.ts +131 -0
- package/src/theme-editor/presets.ts +144 -0
- package/src/theme-editor/preview-utils.ts +265 -0
- package/src/theme-editor/preview.ts +445 -0
- package/src/theme-editor/role-mappings.ts +343 -0
- package/src/theme-editor/sections.test.ts +43 -0
- package/src/theme-editor/sections.ts +994 -0
- package/src/theme-editor/state.ts +298 -0
- package/src/theme-editor/types.ts +177 -0
- package/src/theme-editor.ts +2 -0
- package/src/theme-reference.ts +8 -0
- package/src/types/theme.ts +11 -0
- package/src/types.ts +22 -0
- package/src/ui.scroll.test.ts +554 -0
- package/src/ui.ts +223 -133
- package/src/utils/auto-follow.test.ts +110 -0
- package/src/utils/auto-follow.ts +112 -0
- package/src/utils/plugins.ts +1 -1
- package/src/utils/theme.test.ts +44 -8
- package/src/utils/theme.ts +11 -11
- package/src/utils/tokens.ts +137 -41
- package/widget.css +0 -1
package/src/ui.ts
CHANGED
|
@@ -33,6 +33,13 @@ import { renderLucideIcon } from "./utils/icons";
|
|
|
33
33
|
import { createElement, createElementInDocument } from "./utils/dom";
|
|
34
34
|
import { morphMessages } from "./utils/morph";
|
|
35
35
|
import { computeMessageFingerprint, createMessageCache, getCachedWrapper, setCachedWrapper, pruneCache } from "./utils/message-fingerprint";
|
|
36
|
+
import {
|
|
37
|
+
createFollowStateController,
|
|
38
|
+
getScrollBottomOffset,
|
|
39
|
+
isElementNearBottom,
|
|
40
|
+
resolveFollowStateFromScroll,
|
|
41
|
+
resolveFollowStateFromWheel
|
|
42
|
+
} from "./utils/auto-follow";
|
|
36
43
|
import { statusCopy } from "./utils/constants";
|
|
37
44
|
import { isDockedMountMode, resolveDockConfig } from "./utils/dock";
|
|
38
45
|
import { createLauncherButton } from "./components/launcher";
|
|
@@ -545,6 +552,7 @@ export const createAgentExperience = (
|
|
|
545
552
|
let showReasoning = config.features?.showReasoning ?? true;
|
|
546
553
|
let showToolCalls = config.features?.showToolCalls ?? true;
|
|
547
554
|
let showEventStreamToggle = config.features?.showEventStreamToggle ?? false;
|
|
555
|
+
let scrollToBottomFeature = config.features?.scrollToBottom ?? {};
|
|
548
556
|
const persistKeyPrefix = (typeof config.persistState === 'object' ? config.persistState?.keyPrefix : undefined) ?? "persona-";
|
|
549
557
|
const eventStreamDbName = `${persistKeyPrefix}event-stream`;
|
|
550
558
|
let eventStreamStore = showEventStreamToggle ? new EventStreamStore(eventStreamDbName) : null;
|
|
@@ -655,6 +663,49 @@ export const createAgentExperience = (
|
|
|
655
663
|
let attachmentButtonWrapper: HTMLElement | null = panelElements.attachmentButtonWrapper;
|
|
656
664
|
let attachmentInput: HTMLInputElement | null = panelElements.attachmentInput;
|
|
657
665
|
let attachmentPreviewsContainer: HTMLElement | null = panelElements.attachmentPreviewsContainer;
|
|
666
|
+
container.classList.add("persona-relative");
|
|
667
|
+
body.classList.add("persona-relative");
|
|
668
|
+
const SCROLL_TO_BOTTOM_EDGE_OFFSET = 12;
|
|
669
|
+
|
|
670
|
+
const getScrollToBottomLabel = () => scrollToBottomFeature.label ?? "";
|
|
671
|
+
const getScrollToBottomIconName = () => scrollToBottomFeature.iconName ?? "arrow-down";
|
|
672
|
+
const isScrollToBottomEnabled = () => scrollToBottomFeature.enabled !== false;
|
|
673
|
+
const scrollToBottomButton = createElement(
|
|
674
|
+
"button",
|
|
675
|
+
"persona-scroll-to-bottom-indicator persona-absolute persona-bottom-3 persona-left-1/2 persona-z-10 persona-flex persona-items-center persona-gap-1 persona-text-xs persona-transform persona--translate-x-1/2 persona-cursor-pointer"
|
|
676
|
+
) as HTMLButtonElement;
|
|
677
|
+
scrollToBottomButton.type = "button";
|
|
678
|
+
scrollToBottomButton.style.display = "none";
|
|
679
|
+
scrollToBottomButton.setAttribute("data-persona-scroll-to-bottom", "true");
|
|
680
|
+
const scrollToBottomIcon = createElement("span", "persona-flex persona-items-center");
|
|
681
|
+
const scrollToBottomLabel = createElement("span", "");
|
|
682
|
+
scrollToBottomButton.append(scrollToBottomIcon, scrollToBottomLabel);
|
|
683
|
+
container.appendChild(scrollToBottomButton);
|
|
684
|
+
|
|
685
|
+
const updateScrollToBottomButtonOffset = () => {
|
|
686
|
+
const footerHidden = footer.style.display === "none";
|
|
687
|
+
const footerHeight = footerHidden ? 0 : footer.offsetHeight;
|
|
688
|
+
scrollToBottomButton.style.bottom = `${footerHeight + SCROLL_TO_BOTTOM_EDGE_OFFSET}px`;
|
|
689
|
+
};
|
|
690
|
+
updateScrollToBottomButtonOffset();
|
|
691
|
+
|
|
692
|
+
const renderScrollToBottomButton = () => {
|
|
693
|
+
const hasLabel = Boolean(getScrollToBottomLabel());
|
|
694
|
+
scrollToBottomButton.setAttribute("aria-label", getScrollToBottomLabel() || "Jump to latest");
|
|
695
|
+
scrollToBottomButton.title = getScrollToBottomLabel();
|
|
696
|
+
scrollToBottomButton.setAttribute("data-persona-scroll-to-bottom-has-label", hasLabel ? "true" : "false");
|
|
697
|
+
scrollToBottomIcon.innerHTML = "";
|
|
698
|
+
const icon = renderLucideIcon(getScrollToBottomIconName(), "14px", "currentColor", 2);
|
|
699
|
+
if (icon) {
|
|
700
|
+
scrollToBottomIcon.appendChild(icon);
|
|
701
|
+
scrollToBottomIcon.style.display = "";
|
|
702
|
+
} else {
|
|
703
|
+
scrollToBottomIcon.style.display = "none";
|
|
704
|
+
}
|
|
705
|
+
scrollToBottomLabel.textContent = getScrollToBottomLabel();
|
|
706
|
+
scrollToBottomLabel.style.display = hasLabel ? "" : "none";
|
|
707
|
+
};
|
|
708
|
+
renderScrollToBottomButton();
|
|
658
709
|
|
|
659
710
|
// Initialized after composer plugins rebind footer DOM (see `bindComposerRefsFromFooter`)
|
|
660
711
|
let attachmentManager: AttachmentManager | null = null;
|
|
@@ -703,9 +754,7 @@ export const createAgentExperience = (
|
|
|
703
754
|
eventStreamView.update();
|
|
704
755
|
}
|
|
705
756
|
if (eventStreamToggleBtn) {
|
|
706
|
-
eventStreamToggleBtn.
|
|
707
|
-
eventStreamToggleBtn.classList.add("persona-text-persona-accent");
|
|
708
|
-
eventStreamToggleBtn.style.boxShadow = "inset 0 0 0 1.5px var(--persona-accent, #3b82f6)";
|
|
757
|
+
eventStreamToggleBtn.style.boxShadow = `inset 0 0 0 1.5px ${HEADER_THEME_CSS.actionIconColor}`;
|
|
709
758
|
const activeClasses = config.features?.eventStream?.classNames?.toggleButtonActive;
|
|
710
759
|
if (activeClasses) activeClasses.split(/\s+/).forEach(c => c && eventStreamToggleBtn!.classList.add(c));
|
|
711
760
|
}
|
|
@@ -721,6 +770,7 @@ export const createAgentExperience = (
|
|
|
721
770
|
};
|
|
722
771
|
eventStreamLastUpdate = 0;
|
|
723
772
|
eventStreamRAF = requestAnimationFrame(rafLoop);
|
|
773
|
+
syncScrollToBottomButton();
|
|
724
774
|
eventBus.emit("eventStream:opened", { timestamp: Date.now() });
|
|
725
775
|
};
|
|
726
776
|
|
|
@@ -732,8 +782,6 @@ export const createAgentExperience = (
|
|
|
732
782
|
}
|
|
733
783
|
body.style.display = "";
|
|
734
784
|
if (eventStreamToggleBtn) {
|
|
735
|
-
eventStreamToggleBtn.classList.remove("persona-text-persona-accent");
|
|
736
|
-
eventStreamToggleBtn.classList.add("persona-text-persona-muted");
|
|
737
785
|
eventStreamToggleBtn.style.boxShadow = "";
|
|
738
786
|
const activeClasses = config.features?.eventStream?.classNames?.toggleButtonActive;
|
|
739
787
|
if (activeClasses) activeClasses.split(/\s+/).forEach(c => c && eventStreamToggleBtn!.classList.remove(c));
|
|
@@ -743,6 +791,7 @@ export const createAgentExperience = (
|
|
|
743
791
|
cancelAnimationFrame(eventStreamRAF);
|
|
744
792
|
eventStreamRAF = null;
|
|
745
793
|
}
|
|
794
|
+
syncScrollToBottomButton();
|
|
746
795
|
eventBus.emit("eventStream:closed", { timestamp: Date.now() });
|
|
747
796
|
};
|
|
748
797
|
|
|
@@ -750,10 +799,11 @@ export const createAgentExperience = (
|
|
|
750
799
|
let eventStreamToggleBtn: HTMLButtonElement | null = null;
|
|
751
800
|
if (showEventStreamToggle) {
|
|
752
801
|
const esClassNames = config.features?.eventStream?.classNames;
|
|
753
|
-
const toggleBtnClasses = "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full
|
|
802
|
+
const toggleBtnClasses = "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-opacity-80 persona-cursor-pointer persona-border-none persona-bg-transparent persona-p-1" + (esClassNames?.toggleButton ? " " + esClassNames.toggleButton : "");
|
|
754
803
|
eventStreamToggleBtn = createElement("button", toggleBtnClasses) as HTMLButtonElement;
|
|
755
804
|
eventStreamToggleBtn.style.width = "28px";
|
|
756
805
|
eventStreamToggleBtn.style.height = "28px";
|
|
806
|
+
eventStreamToggleBtn.style.color = HEADER_THEME_CSS.actionIconColor;
|
|
757
807
|
eventStreamToggleBtn.type = "button";
|
|
758
808
|
eventStreamToggleBtn.setAttribute("aria-label", "Event Stream");
|
|
759
809
|
eventStreamToggleBtn.title = "Event Stream";
|
|
@@ -877,13 +927,18 @@ export const createAgentExperience = (
|
|
|
877
927
|
ensureComposerAttachmentSurface(footer);
|
|
878
928
|
bindComposerRefsFromFooter(footer);
|
|
879
929
|
|
|
880
|
-
// Apply contentMaxWidth to composer form if configured
|
|
930
|
+
// Apply contentMaxWidth to composer form and attachment previews if configured
|
|
881
931
|
const contentMaxWidth = config.layout?.contentMaxWidth;
|
|
882
932
|
if (contentMaxWidth && composerForm) {
|
|
883
933
|
composerForm.style.maxWidth = contentMaxWidth;
|
|
884
934
|
composerForm.style.marginLeft = "auto";
|
|
885
935
|
composerForm.style.marginRight = "auto";
|
|
886
936
|
}
|
|
937
|
+
if (contentMaxWidth && attachmentPreviewsContainer) {
|
|
938
|
+
attachmentPreviewsContainer.style.maxWidth = contentMaxWidth;
|
|
939
|
+
attachmentPreviewsContainer.style.marginLeft = "auto";
|
|
940
|
+
attachmentPreviewsContainer.style.marginRight = "auto";
|
|
941
|
+
}
|
|
887
942
|
|
|
888
943
|
if (config.attachments?.enabled && attachmentInput && attachmentPreviewsContainer) {
|
|
889
944
|
attachmentManager = AttachmentManager.fromConfig(config.attachments);
|
|
@@ -1843,18 +1898,13 @@ export const createAgentExperience = (
|
|
|
1843
1898
|
let isStreaming = false;
|
|
1844
1899
|
const messageCache = createMessageCache();
|
|
1845
1900
|
let configVersion = 0;
|
|
1846
|
-
|
|
1901
|
+
const autoFollow = createFollowStateController();
|
|
1847
1902
|
let lastScrollTop = 0;
|
|
1848
|
-
let lastAutoScrollTime = 0;
|
|
1849
1903
|
let scrollRAF: number | null = null;
|
|
1850
|
-
let isAutoScrollBlocked = false;
|
|
1851
|
-
let blockUntilTime = 0;
|
|
1852
1904
|
let isAutoScrolling = false;
|
|
1853
1905
|
|
|
1854
|
-
const
|
|
1855
|
-
const
|
|
1856
|
-
const USER_SCROLL_THRESHOLD = 5;
|
|
1857
|
-
const BOTTOM_THRESHOLD = 50;
|
|
1906
|
+
const USER_SCROLL_THRESHOLD = 1;
|
|
1907
|
+
const BOTTOM_THRESHOLD = 8;
|
|
1858
1908
|
const messageState = new Map<
|
|
1859
1909
|
string,
|
|
1860
1910
|
{ streaming?: boolean; role: AgentWidgetMessage["role"] }
|
|
@@ -1944,75 +1994,91 @@ export const createAgentExperience = (
|
|
|
1944
1994
|
}
|
|
1945
1995
|
}
|
|
1946
1996
|
|
|
1947
|
-
|
|
1948
|
-
|
|
1997
|
+
// Track ongoing smooth scroll animation
|
|
1998
|
+
let smoothScrollRAF: number | null = null;
|
|
1949
1999
|
|
|
1950
|
-
|
|
2000
|
+
// Get the scrollable container using its unique ID
|
|
2001
|
+
const getScrollableContainer = (): HTMLElement => {
|
|
2002
|
+
// Use the unique ID for reliable selection
|
|
2003
|
+
const scrollable = wrapper.querySelector('#persona-scroll-container') as HTMLElement;
|
|
2004
|
+
// Fallback to body if ID not found (shouldn't happen, but safe fallback)
|
|
2005
|
+
return scrollable || body;
|
|
2006
|
+
};
|
|
1951
2007
|
|
|
1952
|
-
|
|
1953
|
-
|
|
2008
|
+
const cancelSmoothScroll = () => {
|
|
2009
|
+
if (smoothScrollRAF !== null) {
|
|
2010
|
+
cancelAnimationFrame(smoothScrollRAF);
|
|
2011
|
+
smoothScrollRAF = null;
|
|
1954
2012
|
}
|
|
2013
|
+
isAutoScrolling = false;
|
|
2014
|
+
};
|
|
1955
2015
|
|
|
1956
|
-
|
|
1957
|
-
|
|
2016
|
+
const cancelAutoScroll = () => {
|
|
2017
|
+
if (scrollRAF !== null) {
|
|
2018
|
+
cancelAnimationFrame(scrollRAF);
|
|
2019
|
+
scrollRAF = null;
|
|
1958
2020
|
}
|
|
2021
|
+
cancelSmoothScroll();
|
|
2022
|
+
};
|
|
1959
2023
|
|
|
1960
|
-
|
|
2024
|
+
const syncScrollToBottomButton = () => {
|
|
2025
|
+
if (!isScrollToBottomEnabled() || eventStreamVisible) {
|
|
2026
|
+
if (scrollToBottomButton.parentNode) {
|
|
2027
|
+
scrollToBottomButton.remove();
|
|
2028
|
+
}
|
|
2029
|
+
scrollToBottomButton.style.display = "none";
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
if (scrollToBottomButton.parentNode !== container) {
|
|
2033
|
+
container.appendChild(scrollToBottomButton);
|
|
2034
|
+
}
|
|
2035
|
+
updateScrollToBottomButtonOffset();
|
|
2036
|
+
scrollToBottomButton.style.display = autoFollow.isFollowing() ? "none" : "";
|
|
2037
|
+
};
|
|
1961
2038
|
|
|
1962
|
-
|
|
1963
|
-
|
|
2039
|
+
const pauseAutoScroll = () => {
|
|
2040
|
+
if (!autoFollow.pause()) return;
|
|
2041
|
+
cancelAutoScroll();
|
|
2042
|
+
syncScrollToBottomButton();
|
|
2043
|
+
};
|
|
1964
2044
|
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2045
|
+
const resumeAutoScroll = () => {
|
|
2046
|
+
autoFollow.resume();
|
|
2047
|
+
syncScrollToBottomButton();
|
|
2048
|
+
};
|
|
2049
|
+
|
|
2050
|
+
const scheduleAutoScroll = (force = false) => {
|
|
2051
|
+
if (!autoFollow.isFollowing()) return;
|
|
2052
|
+
|
|
2053
|
+
if (!force && !isStreaming) return;
|
|
2054
|
+
|
|
2055
|
+
cancelAutoScroll();
|
|
1968
2056
|
|
|
1969
2057
|
scrollRAF = requestAnimationFrame(() => {
|
|
1970
|
-
if (isAutoScrollBlocked || !shouldAutoScroll) return;
|
|
1971
|
-
isAutoScrolling = true;
|
|
1972
|
-
body.scrollTop = body.scrollHeight;
|
|
1973
|
-
lastScrollTop = body.scrollTop;
|
|
1974
|
-
requestAnimationFrame(() => {
|
|
1975
|
-
isAutoScrolling = false;
|
|
1976
|
-
});
|
|
1977
2058
|
scrollRAF = null;
|
|
2059
|
+
if (!autoFollow.isFollowing()) return;
|
|
2060
|
+
smoothScrollToBottom(getScrollableContainer(), force ? 220 : 140);
|
|
1978
2061
|
});
|
|
1979
2062
|
};
|
|
1980
2063
|
|
|
1981
|
-
// Track ongoing smooth scroll animation
|
|
1982
|
-
let smoothScrollRAF: number | null = null;
|
|
1983
|
-
|
|
1984
|
-
// Get the scrollable container using its unique ID
|
|
1985
|
-
const getScrollableContainer = (): HTMLElement => {
|
|
1986
|
-
// Use the unique ID for reliable selection
|
|
1987
|
-
const scrollable = wrapper.querySelector('#persona-scroll-container') as HTMLElement;
|
|
1988
|
-
// Fallback to body if ID not found (shouldn't happen, but safe fallback)
|
|
1989
|
-
return scrollable || body;
|
|
1990
|
-
};
|
|
1991
|
-
|
|
1992
2064
|
// Custom smooth scroll animation with easing
|
|
1993
2065
|
const smoothScrollToBottom = (element: HTMLElement, duration = 500) => {
|
|
1994
2066
|
const start = element.scrollTop;
|
|
1995
|
-
const clientHeight = element.clientHeight;
|
|
1996
2067
|
// Recalculate target dynamically to handle layout changes
|
|
1997
|
-
let target = element
|
|
2068
|
+
let target = getScrollBottomOffset(element);
|
|
1998
2069
|
let distance = target - start;
|
|
1999
2070
|
|
|
2000
|
-
// Check if already at bottom: scrollTop + clientHeight should be >= scrollHeight
|
|
2001
|
-
// Add a small threshold (2px) to account for rounding/subpixel differences
|
|
2002
|
-
const isAtBottom = start + clientHeight >= target - 2;
|
|
2003
|
-
|
|
2004
2071
|
// If already at bottom or very close, skip animation to prevent glitch
|
|
2005
|
-
if (
|
|
2072
|
+
if (Math.abs(distance) < 1) {
|
|
2073
|
+
lastScrollTop = element.scrollTop;
|
|
2006
2074
|
return;
|
|
2007
2075
|
}
|
|
2008
2076
|
|
|
2009
2077
|
// Cancel any ongoing smooth scroll animation
|
|
2010
|
-
|
|
2011
|
-
cancelAnimationFrame(smoothScrollRAF);
|
|
2012
|
-
smoothScrollRAF = null;
|
|
2013
|
-
}
|
|
2078
|
+
cancelSmoothScroll();
|
|
2014
2079
|
|
|
2015
2080
|
const startTime = performance.now();
|
|
2081
|
+
isAutoScrolling = true;
|
|
2016
2082
|
|
|
2017
2083
|
// Easing function: ease-out cubic for smooth deceleration
|
|
2018
2084
|
const easeOutCubic = (t: number): number => {
|
|
@@ -2020,8 +2086,13 @@ export const createAgentExperience = (
|
|
|
2020
2086
|
};
|
|
2021
2087
|
|
|
2022
2088
|
const animate = (currentTime: number) => {
|
|
2089
|
+
if (!autoFollow.isFollowing()) {
|
|
2090
|
+
cancelSmoothScroll();
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2023
2094
|
// Recalculate target each frame in case scrollHeight changed
|
|
2024
|
-
const currentTarget = element
|
|
2095
|
+
const currentTarget = getScrollBottomOffset(element);
|
|
2025
2096
|
if (currentTarget !== target) {
|
|
2026
2097
|
target = currentTarget;
|
|
2027
2098
|
distance = target - start;
|
|
@@ -2033,13 +2104,16 @@ export const createAgentExperience = (
|
|
|
2033
2104
|
|
|
2034
2105
|
const currentScroll = start + distance * eased;
|
|
2035
2106
|
element.scrollTop = currentScroll;
|
|
2107
|
+
lastScrollTop = element.scrollTop;
|
|
2036
2108
|
|
|
2037
2109
|
if (progress < 1) {
|
|
2038
2110
|
smoothScrollRAF = requestAnimationFrame(animate);
|
|
2039
2111
|
} else {
|
|
2040
2112
|
// Ensure we end exactly at the target
|
|
2041
|
-
element.scrollTop =
|
|
2113
|
+
element.scrollTop = target;
|
|
2114
|
+
lastScrollTop = element.scrollTop;
|
|
2042
2115
|
smoothScrollRAF = null;
|
|
2116
|
+
isAutoScrolling = false;
|
|
2043
2117
|
}
|
|
2044
2118
|
};
|
|
2045
2119
|
|
|
@@ -2388,7 +2462,6 @@ export const createAgentExperience = (
|
|
|
2388
2462
|
"persona-shadow-sm",
|
|
2389
2463
|
"persona-bg-persona-surface",
|
|
2390
2464
|
"persona-border",
|
|
2391
|
-
"persona-border-persona-message-border",
|
|
2392
2465
|
"persona-text-persona-primary",
|
|
2393
2466
|
"persona-px-5",
|
|
2394
2467
|
"persona-py-3"
|
|
@@ -2400,6 +2473,7 @@ export const createAgentExperience = (
|
|
|
2400
2473
|
"persona-text-persona-primary"
|
|
2401
2474
|
].join(" ");
|
|
2402
2475
|
typingBubble.setAttribute("data-typing-indicator", "true");
|
|
2476
|
+
typingBubble.style.borderColor = "var(--persona-message-assistant-border, var(--persona-border, #e5e7eb))";
|
|
2403
2477
|
|
|
2404
2478
|
typingBubble.appendChild(typingIndicator);
|
|
2405
2479
|
|
|
@@ -2479,16 +2553,6 @@ export const createAgentExperience = (
|
|
|
2479
2553
|
|
|
2480
2554
|
// Use idiomorph to morph the container contents
|
|
2481
2555
|
morphMessages(container, tempContainer);
|
|
2482
|
-
// Defer scroll to next frame for smoother animation and to prevent jolt
|
|
2483
|
-
// This allows the browser to update layout (e.g., typing indicator removal) before scrolling
|
|
2484
|
-
// Use double RAF to ensure layout has fully settled before starting scroll animation
|
|
2485
|
-
// Get the scrollable container using its unique ID (#persona-scroll-container)
|
|
2486
|
-
requestAnimationFrame(() => {
|
|
2487
|
-
requestAnimationFrame(() => {
|
|
2488
|
-
const scrollableContainer = getScrollableContainer();
|
|
2489
|
-
smoothScrollToBottom(scrollableContainer);
|
|
2490
|
-
});
|
|
2491
|
-
});
|
|
2492
2556
|
};
|
|
2493
2557
|
|
|
2494
2558
|
// Alias for clarity - the implementation handles flicker prevention via typing indicator logic
|
|
@@ -2982,17 +3046,16 @@ export const createAgentExperience = (
|
|
|
2982
3046
|
iconSize: parseFloat(voiceConfig.iconSize ?? config.sendButton?.size ?? "40") || 24,
|
|
2983
3047
|
};
|
|
2984
3048
|
|
|
2985
|
-
// Apply recording state styles from config
|
|
2986
|
-
const recordingBackgroundColor = voiceConfig.recordingBackgroundColor
|
|
3049
|
+
// Apply recording state styles from config or theme tokens
|
|
3050
|
+
const recordingBackgroundColor = voiceConfig.recordingBackgroundColor;
|
|
2987
3051
|
const recordingIconColor = voiceConfig.recordingIconColor;
|
|
2988
3052
|
const recordingBorderColor = voiceConfig.recordingBorderColor;
|
|
2989
|
-
|
|
3053
|
+
|
|
2990
3054
|
micButton.classList.add("persona-voice-recording");
|
|
2991
|
-
micButton.style.backgroundColor = recordingBackgroundColor;
|
|
2992
|
-
|
|
3055
|
+
micButton.style.backgroundColor = recordingBackgroundColor ?? "var(--persona-voice-recording-bg, #ef4444)";
|
|
3056
|
+
micButton.style.color = recordingIconColor ?? "var(--persona-voice-recording-indicator, #ffffff)";
|
|
3057
|
+
|
|
2993
3058
|
if (recordingIconColor) {
|
|
2994
|
-
micButton.style.color = recordingIconColor;
|
|
2995
|
-
// Update SVG stroke color if present
|
|
2996
3059
|
const svg = micButton.querySelector("svg");
|
|
2997
3060
|
if (svg) {
|
|
2998
3061
|
svg.setAttribute("stroke", recordingIconColor);
|
|
@@ -3092,30 +3155,27 @@ export const createAgentExperience = (
|
|
|
3092
3155
|
micButton.style.fontSize = "18px";
|
|
3093
3156
|
micButton.style.lineHeight = "1";
|
|
3094
3157
|
|
|
3095
|
-
//
|
|
3158
|
+
// Set mic button foreground from config or theme token
|
|
3159
|
+
if (iconColor) {
|
|
3160
|
+
micButton.style.color = iconColor;
|
|
3161
|
+
} else {
|
|
3162
|
+
micButton.style.color = "var(--persona-text, #111827)";
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
// Use Lucide mic icon (stroke width 1.5 for minimalist outline style)
|
|
3096
3166
|
const iconColorValue = iconColor || "currentColor";
|
|
3097
3167
|
const micIconSvg = renderLucideIcon(micIconName, micIconSizeNum, iconColorValue, 1.5);
|
|
3098
3168
|
if (micIconSvg) {
|
|
3099
3169
|
micButton.appendChild(micIconSvg);
|
|
3100
|
-
micButton.style.color = iconColorValue;
|
|
3101
3170
|
} else {
|
|
3102
|
-
// Fallback to text if icon fails
|
|
3103
3171
|
micButton.textContent = "🎤";
|
|
3104
|
-
micButton.style.color = iconColorValue;
|
|
3105
3172
|
}
|
|
3106
|
-
|
|
3173
|
+
|
|
3107
3174
|
// Apply background color
|
|
3108
3175
|
if (backgroundColor) {
|
|
3109
3176
|
micButton.style.backgroundColor = backgroundColor;
|
|
3110
3177
|
} else {
|
|
3111
|
-
micButton.
|
|
3112
|
-
}
|
|
3113
|
-
|
|
3114
|
-
// Apply icon/text color
|
|
3115
|
-
if (iconColor) {
|
|
3116
|
-
micButton.style.color = iconColor;
|
|
3117
|
-
} else if (!iconColor && !sendButtonConfig?.textColor) {
|
|
3118
|
-
micButton.classList.add("persona-text-white");
|
|
3178
|
+
micButton.style.backgroundColor = "";
|
|
3119
3179
|
}
|
|
3120
3180
|
|
|
3121
3181
|
// Apply border styling
|
|
@@ -3187,14 +3247,14 @@ export const createAgentExperience = (
|
|
|
3187
3247
|
if (!micButton) return;
|
|
3188
3248
|
storeOriginalMicStyles();
|
|
3189
3249
|
const voiceConfig = config.voiceRecognition ?? {};
|
|
3190
|
-
const recordingBackgroundColor = voiceConfig.recordingBackgroundColor
|
|
3250
|
+
const recordingBackgroundColor = voiceConfig.recordingBackgroundColor;
|
|
3191
3251
|
const recordingIconColor = voiceConfig.recordingIconColor;
|
|
3192
3252
|
const recordingBorderColor = voiceConfig.recordingBorderColor;
|
|
3193
3253
|
removeAllVoiceStateClasses();
|
|
3194
3254
|
micButton.classList.add("persona-voice-recording");
|
|
3195
|
-
micButton.style.backgroundColor = recordingBackgroundColor;
|
|
3255
|
+
micButton.style.backgroundColor = recordingBackgroundColor ?? "var(--persona-voice-recording-bg, #ef4444)";
|
|
3256
|
+
micButton.style.color = recordingIconColor ?? "var(--persona-voice-recording-indicator, #ffffff)";
|
|
3196
3257
|
if (recordingIconColor) {
|
|
3197
|
-
micButton.style.color = recordingIconColor;
|
|
3198
3258
|
const svg = micButton.querySelector("svg");
|
|
3199
3259
|
if (svg) svg.setAttribute("stroke", recordingIconColor);
|
|
3200
3260
|
}
|
|
@@ -3240,7 +3300,7 @@ export const createAgentExperience = (
|
|
|
3240
3300
|
const iconColor = voiceConfig.speakingIconColor
|
|
3241
3301
|
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingIconColor ?? originalMicStyles?.color ?? "") : (originalMicStyles?.color ?? ""));
|
|
3242
3302
|
const bgColor = voiceConfig.speakingBackgroundColor
|
|
3243
|
-
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingBackgroundColor ?? "#ef4444") : (originalMicStyles?.backgroundColor ?? ""));
|
|
3303
|
+
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingBackgroundColor ?? "var(--persona-voice-recording-bg, #ef4444)") : (originalMicStyles?.backgroundColor ?? ""));
|
|
3244
3304
|
const borderColor = voiceConfig.speakingBorderColor
|
|
3245
3305
|
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingBorderColor ?? "") : (originalMicStyles?.borderColor ?? ""));
|
|
3246
3306
|
|
|
@@ -3510,6 +3570,7 @@ export const createAgentExperience = (
|
|
|
3510
3570
|
} finally {
|
|
3511
3571
|
// applyFullHeightStyles() assigns wrapper.style.cssText (e.g. display:flex !important), which
|
|
3512
3572
|
// overwrites updateOpenState()'s display:none when docked+closed. Re-sync after every recalc.
|
|
3573
|
+
updateScrollToBottomButtonOffset();
|
|
3513
3574
|
updateOpenState();
|
|
3514
3575
|
}
|
|
3515
3576
|
};
|
|
@@ -3518,37 +3579,68 @@ export const createAgentExperience = (
|
|
|
3518
3579
|
const ownerWindow = mount.ownerDocument.defaultView ?? window;
|
|
3519
3580
|
ownerWindow.addEventListener("resize", recalcPanelHeight);
|
|
3520
3581
|
destroyCallbacks.push(() => ownerWindow.removeEventListener("resize", recalcPanelHeight));
|
|
3582
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
3583
|
+
const footerResizeObserver = new ResizeObserver(() => {
|
|
3584
|
+
updateScrollToBottomButtonOffset();
|
|
3585
|
+
});
|
|
3586
|
+
footerResizeObserver.observe(footer);
|
|
3587
|
+
destroyCallbacks.push(() => footerResizeObserver.disconnect());
|
|
3588
|
+
}
|
|
3521
3589
|
|
|
3522
3590
|
lastScrollTop = body.scrollTop;
|
|
3523
3591
|
|
|
3524
3592
|
const handleScroll = () => {
|
|
3525
3593
|
const scrollTop = body.scrollTop;
|
|
3526
|
-
const
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3594
|
+
const { action, nextLastScrollTop } = resolveFollowStateFromScroll({
|
|
3595
|
+
following: autoFollow.isFollowing(),
|
|
3596
|
+
currentScrollTop: scrollTop,
|
|
3597
|
+
lastScrollTop,
|
|
3598
|
+
nearBottom: isElementNearBottom(body, BOTTOM_THRESHOLD),
|
|
3599
|
+
userScrollThreshold: USER_SCROLL_THRESHOLD,
|
|
3600
|
+
isAutoScrolling,
|
|
3601
|
+
pauseOnUpwardScroll: true,
|
|
3602
|
+
pauseWhenAwayFromBottom: false,
|
|
3603
|
+
resumeRequiresDownwardScroll: true
|
|
3604
|
+
});
|
|
3605
|
+
lastScrollTop = nextLastScrollTop;
|
|
3606
|
+
|
|
3607
|
+
if (action === "resume") {
|
|
3608
|
+
resumeAutoScroll();
|
|
3538
3609
|
return;
|
|
3539
3610
|
}
|
|
3540
3611
|
|
|
3541
|
-
if (
|
|
3542
|
-
|
|
3543
|
-
blockUntilTime = Date.now() + AUTO_SCROLL_BLOCK_TIME;
|
|
3544
|
-
shouldAutoScroll = false;
|
|
3612
|
+
if (action === "pause") {
|
|
3613
|
+
pauseAutoScroll();
|
|
3545
3614
|
}
|
|
3546
3615
|
};
|
|
3547
3616
|
|
|
3548
3617
|
body.addEventListener("scroll", handleScroll, { passive: true });
|
|
3549
3618
|
destroyCallbacks.push(() => body.removeEventListener("scroll", handleScroll));
|
|
3619
|
+
const handleWheel = (event: WheelEvent) => {
|
|
3620
|
+
const action = resolveFollowStateFromWheel({
|
|
3621
|
+
following: autoFollow.isFollowing(),
|
|
3622
|
+
deltaY: event.deltaY,
|
|
3623
|
+
nearBottom: isElementNearBottom(body, BOTTOM_THRESHOLD),
|
|
3624
|
+
resumeWhenNearBottom: true
|
|
3625
|
+
});
|
|
3626
|
+
|
|
3627
|
+
if (action === "pause") {
|
|
3628
|
+
pauseAutoScroll();
|
|
3629
|
+
} else if (action === "resume") {
|
|
3630
|
+
resumeAutoScroll();
|
|
3631
|
+
}
|
|
3632
|
+
};
|
|
3633
|
+
body.addEventListener("wheel", handleWheel, { passive: true });
|
|
3634
|
+
destroyCallbacks.push(() => body.removeEventListener("wheel", handleWheel));
|
|
3635
|
+
scrollToBottomButton.addEventListener("click", () => {
|
|
3636
|
+
body.scrollTop = body.scrollHeight;
|
|
3637
|
+
lastScrollTop = body.scrollTop;
|
|
3638
|
+
resumeAutoScroll();
|
|
3639
|
+
scheduleAutoScroll(true);
|
|
3640
|
+
});
|
|
3641
|
+
destroyCallbacks.push(() => scrollToBottomButton.remove());
|
|
3550
3642
|
destroyCallbacks.push(() => {
|
|
3551
|
-
|
|
3643
|
+
cancelAutoScroll();
|
|
3552
3644
|
});
|
|
3553
3645
|
|
|
3554
3646
|
const refreshCloseButton = () => {
|
|
@@ -3695,6 +3787,9 @@ export const createAgentExperience = (
|
|
|
3695
3787
|
autoExpand = config.launcher?.autoExpand ?? false;
|
|
3696
3788
|
showReasoning = config.features?.showReasoning ?? true;
|
|
3697
3789
|
showToolCalls = config.features?.showToolCalls ?? true;
|
|
3790
|
+
scrollToBottomFeature = config.features?.scrollToBottom ?? {};
|
|
3791
|
+
renderScrollToBottomButton();
|
|
3792
|
+
syncScrollToBottomButton();
|
|
3698
3793
|
const prevShowEventStreamToggle = showEventStreamToggle;
|
|
3699
3794
|
showEventStreamToggle = config.features?.showEventStreamToggle ?? false;
|
|
3700
3795
|
|
|
@@ -3718,10 +3813,11 @@ export const createAgentExperience = (
|
|
|
3718
3813
|
// Add header toggle button if not present
|
|
3719
3814
|
if (!eventStreamToggleBtn && header) {
|
|
3720
3815
|
const dynEsClassNames = config.features?.eventStream?.classNames;
|
|
3721
|
-
const dynToggleBtnClasses = "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full
|
|
3816
|
+
const dynToggleBtnClasses = "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-opacity-80 persona-cursor-pointer persona-border-none persona-bg-transparent persona-p-1" + (dynEsClassNames?.toggleButton ? " " + dynEsClassNames.toggleButton : "");
|
|
3722
3817
|
eventStreamToggleBtn = createElement("button", dynToggleBtnClasses) as HTMLButtonElement;
|
|
3723
3818
|
eventStreamToggleBtn.style.width = "28px";
|
|
3724
3819
|
eventStreamToggleBtn.style.height = "28px";
|
|
3820
|
+
eventStreamToggleBtn.style.color = HEADER_THEME_CSS.actionIconColor;
|
|
3725
3821
|
eventStreamToggleBtn.type = "button";
|
|
3726
3822
|
eventStreamToggleBtn.setAttribute("aria-label", "Event Stream");
|
|
3727
3823
|
eventStreamToggleBtn.title = "Event Stream";
|
|
@@ -3872,6 +3968,8 @@ export const createAgentExperience = (
|
|
|
3872
3968
|
if (footer) {
|
|
3873
3969
|
footer.style.display = showFooter ? "" : "none";
|
|
3874
3970
|
}
|
|
3971
|
+
updateScrollToBottomButtonOffset();
|
|
3972
|
+
syncScrollToBottomButton();
|
|
3875
3973
|
|
|
3876
3974
|
// Only update open state if launcher enabled state changed or autoExpand value changed
|
|
3877
3975
|
const launcherEnabledChanged = launcherEnabled !== prevLauncherEnabled;
|
|
@@ -4488,22 +4586,18 @@ export const createAgentExperience = (
|
|
|
4488
4586
|
micButton.textContent = "🎤";
|
|
4489
4587
|
}
|
|
4490
4588
|
|
|
4491
|
-
// Update colors
|
|
4589
|
+
// Update colors from config or theme tokens
|
|
4492
4590
|
const backgroundColor = voiceConfig.backgroundColor ?? sendButtonConfig.backgroundColor;
|
|
4493
4591
|
if (backgroundColor) {
|
|
4494
4592
|
micButton.style.backgroundColor = backgroundColor;
|
|
4495
|
-
micButton.classList.remove("persona-bg-persona-primary");
|
|
4496
4593
|
} else {
|
|
4497
4594
|
micButton.style.backgroundColor = "";
|
|
4498
|
-
micButton.classList.add("persona-bg-persona-primary");
|
|
4499
4595
|
}
|
|
4500
|
-
|
|
4596
|
+
|
|
4501
4597
|
if (iconColor) {
|
|
4502
4598
|
micButton.style.color = iconColor;
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
micButton.style.color = "";
|
|
4506
|
-
micButton.classList.add("persona-text-white");
|
|
4599
|
+
} else {
|
|
4600
|
+
micButton.style.color = "var(--persona-text, #111827)";
|
|
4507
4601
|
}
|
|
4508
4602
|
|
|
4509
4603
|
// Update border styling
|
|
@@ -4729,30 +4823,25 @@ export const createAgentExperience = (
|
|
|
4729
4823
|
// Clear existing content
|
|
4730
4824
|
sendButton.innerHTML = "";
|
|
4731
4825
|
|
|
4826
|
+
// Set foreground color from config or theme token
|
|
4827
|
+
if (textColor) {
|
|
4828
|
+
sendButton.style.color = textColor;
|
|
4829
|
+
} else {
|
|
4830
|
+
sendButton.style.color = "var(--persona-button-primary-fg, #ffffff)";
|
|
4831
|
+
}
|
|
4832
|
+
|
|
4732
4833
|
// Use Lucide icon if iconName is provided, otherwise fall back to iconText
|
|
4733
4834
|
if (iconName) {
|
|
4734
4835
|
const iconSize = parseFloat(buttonSize) || 24;
|
|
4735
|
-
const iconColor = textColor
|
|
4836
|
+
const iconColor = textColor?.trim() || "currentColor";
|
|
4736
4837
|
const iconSvg = renderLucideIcon(iconName, iconSize, iconColor, 2);
|
|
4737
4838
|
if (iconSvg) {
|
|
4738
4839
|
sendButton.appendChild(iconSvg);
|
|
4739
|
-
sendButton.style.color = iconColor;
|
|
4740
4840
|
} else {
|
|
4741
|
-
// Fallback to text if icon fails to render
|
|
4742
4841
|
sendButton.textContent = iconText;
|
|
4743
|
-
if (textColor) {
|
|
4744
|
-
sendButton.style.color = textColor;
|
|
4745
|
-
} else {
|
|
4746
|
-
sendButton.classList.add("persona-text-white");
|
|
4747
|
-
}
|
|
4748
4842
|
}
|
|
4749
4843
|
} else {
|
|
4750
4844
|
sendButton.textContent = iconText;
|
|
4751
|
-
if (textColor) {
|
|
4752
|
-
sendButton.style.color = textColor;
|
|
4753
|
-
} else {
|
|
4754
|
-
sendButton.classList.add("persona-text-white");
|
|
4755
|
-
}
|
|
4756
4845
|
}
|
|
4757
4846
|
|
|
4758
4847
|
// Update classes
|
|
@@ -4762,6 +4851,7 @@ export const createAgentExperience = (
|
|
|
4762
4851
|
sendButton.style.backgroundColor = backgroundColor;
|
|
4763
4852
|
sendButton.classList.remove("persona-bg-persona-primary");
|
|
4764
4853
|
} else {
|
|
4854
|
+
sendButton.style.backgroundColor = "";
|
|
4765
4855
|
sendButton.classList.add("persona-bg-persona-primary");
|
|
4766
4856
|
}
|
|
4767
4857
|
} else {
|