@hef2024/llmasaservice-ui 0.23.2 → 0.24.1
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.css +284 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +86 -30
- package/dist/index.mjs +86 -30
- package/package.json +1 -1
- package/src/AIChatPanel.css +178 -0
- package/src/AIChatPanel.tsx +30 -20
- package/src/ChatPanel.css +178 -0
- package/src/ChatPanel.tsx +89 -21
- package/src/components/ui/ThinkingBlock.tsx +22 -8
package/dist/index.mjs
CHANGED
|
@@ -627,10 +627,15 @@ var ThinkingBlock = ({
|
|
|
627
627
|
}) => {
|
|
628
628
|
const displayTitle = title || getDefaultTitle(type);
|
|
629
629
|
const icon = getIcon(type);
|
|
630
|
+
const getPreview = (text, maxLength = 60) => {
|
|
631
|
+
const cleaned = text.replace(/\s+/g, " ").trim();
|
|
632
|
+
if (cleaned.length <= maxLength) return cleaned;
|
|
633
|
+
return cleaned.substring(0, maxLength).trim() + "...";
|
|
634
|
+
};
|
|
630
635
|
return /* @__PURE__ */ React10.createElement(
|
|
631
636
|
"div",
|
|
632
637
|
{
|
|
633
|
-
className: `thinking-block thinking-block--${type} ${isCollapsed ? "thinking-block--collapsed" : ""}`
|
|
638
|
+
className: `thinking-block thinking-block--${type} ${isCollapsed ? "thinking-block--collapsed" : ""} ${isStreaming ? "thinking-block--streaming" : ""}`
|
|
634
639
|
},
|
|
635
640
|
/* @__PURE__ */ React10.createElement(
|
|
636
641
|
"button",
|
|
@@ -640,7 +645,7 @@ var ThinkingBlock = ({
|
|
|
640
645
|
type: "button",
|
|
641
646
|
"aria-expanded": !isCollapsed
|
|
642
647
|
},
|
|
643
|
-
/* @__PURE__ */ React10.createElement("div", { className: "thinking-block__header-left" },
|
|
648
|
+
/* @__PURE__ */ React10.createElement("div", { className: "thinking-block__header-left" }, /* @__PURE__ */ React10.createElement("span", { className: `thinking-block__icon-wrapper ${isStreaming ? "thinking-block__icon-wrapper--spinning" : ""}` }, icon), /* @__PURE__ */ React10.createElement("span", { className: `thinking-block__title ${isStreaming ? "thinking-block__title--shimmer" : ""}` }, displayTitle), isCollapsed && content && /* @__PURE__ */ React10.createElement("span", { className: "thinking-block__preview" }, getPreview(content))),
|
|
644
649
|
/* @__PURE__ */ React10.createElement(ChevronIcon, { isCollapsed })
|
|
645
650
|
),
|
|
646
651
|
/* @__PURE__ */ React10.createElement("div", { className: "thinking-block__content-wrapper" }, /* @__PURE__ */ React10.createElement("div", { className: "thinking-block__content" }, content, isStreaming && /* @__PURE__ */ React10.createElement("span", { className: "thinking-block__cursor" }, "|")))
|
|
@@ -801,6 +806,11 @@ var ChatPanel = ({
|
|
|
801
806
|
}, []);
|
|
802
807
|
const [iframeUrl, setIframeUrl] = useState5(null);
|
|
803
808
|
const responseAreaRef = useRef5(null);
|
|
809
|
+
const [userHasScrolled, setUserHasScrolled] = useState5(false);
|
|
810
|
+
const lastScrollTopRef = useRef5(0);
|
|
811
|
+
const prevResponseLengthRef = useRef5(0);
|
|
812
|
+
const userHasScrolledRef = useRef5(false);
|
|
813
|
+
const idleRef = useRef5(true);
|
|
804
814
|
const THINKING_PATTERNS = useMemo(
|
|
805
815
|
() => ({
|
|
806
816
|
thinking: /<thinking>([\s\S]*?)<\/thinking>/gi,
|
|
@@ -2133,32 +2143,67 @@ var ChatPanel = ({
|
|
|
2133
2143
|
}
|
|
2134
2144
|
}
|
|
2135
2145
|
}, [initialPrompt]);
|
|
2146
|
+
useEffect6(() => {
|
|
2147
|
+
userHasScrolledRef.current = userHasScrolled;
|
|
2148
|
+
}, [userHasScrolled]);
|
|
2149
|
+
useEffect6(() => {
|
|
2150
|
+
idleRef.current = idle;
|
|
2151
|
+
}, [idle]);
|
|
2136
2152
|
useEffect6(() => {
|
|
2137
2153
|
var _a2;
|
|
2138
|
-
if (
|
|
2154
|
+
if (idle) return;
|
|
2155
|
+
const currentResponseLength = response.length;
|
|
2156
|
+
const responseGotLonger = currentResponseLength > prevResponseLengthRef.current;
|
|
2157
|
+
prevResponseLengthRef.current = currentResponseLength;
|
|
2158
|
+
const shouldAutoScroll = scrollToEnd || !userHasScrolledRef.current;
|
|
2159
|
+
if (shouldAutoScroll && response && responseGotLonger) {
|
|
2139
2160
|
if (window.top !== window.self) {
|
|
2140
2161
|
const responseArea = responseAreaRef.current;
|
|
2141
|
-
responseArea
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2162
|
+
if (responseArea) {
|
|
2163
|
+
responseArea.scrollTo({
|
|
2164
|
+
top: responseArea.scrollHeight,
|
|
2165
|
+
behavior: "auto"
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2145
2168
|
} else {
|
|
2146
|
-
(_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "
|
|
2169
|
+
(_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "auto", block: "end" });
|
|
2147
2170
|
}
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
responseArea.addEventListener("scroll", handleScroll);
|
|
2158
|
-
return () => responseArea.removeEventListener("scroll", handleScroll);
|
|
2171
|
+
}
|
|
2172
|
+
}, [response, idle, scrollToEnd]);
|
|
2173
|
+
useEffect6(() => {
|
|
2174
|
+
const responseArea = responseAreaRef.current;
|
|
2175
|
+
if (!responseArea) return;
|
|
2176
|
+
const handleWheel = (e) => {
|
|
2177
|
+
if (idleRef.current) return;
|
|
2178
|
+
if (e.deltaY < 0 && !userHasScrolledRef.current) {
|
|
2179
|
+
setUserHasScrolled(true);
|
|
2159
2180
|
}
|
|
2181
|
+
};
|
|
2182
|
+
const handleScroll = () => {
|
|
2183
|
+
setHasScroll(hasVerticalScrollbar(responseArea));
|
|
2184
|
+
const isScrolledToBottom = responseArea.scrollHeight - responseArea.scrollTop - responseArea.clientHeight < 5;
|
|
2185
|
+
setIsAtBottom(isScrolledToBottom);
|
|
2186
|
+
if (!idleRef.current && userHasScrolledRef.current) {
|
|
2187
|
+
const isNearBottom = responseArea.scrollHeight - responseArea.scrollTop - responseArea.clientHeight < 50;
|
|
2188
|
+
if (isNearBottom) {
|
|
2189
|
+
setUserHasScrolled(false);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
handleScroll();
|
|
2194
|
+
responseArea.addEventListener("wheel", handleWheel, { passive: true });
|
|
2195
|
+
responseArea.addEventListener("scroll", handleScroll, { passive: true });
|
|
2196
|
+
return () => {
|
|
2197
|
+
responseArea.removeEventListener("wheel", handleWheel);
|
|
2198
|
+
responseArea.removeEventListener("scroll", handleScroll);
|
|
2199
|
+
};
|
|
2200
|
+
}, []);
|
|
2201
|
+
useEffect6(() => {
|
|
2202
|
+
const responseArea = responseAreaRef.current;
|
|
2203
|
+
if (responseArea) {
|
|
2204
|
+
setHasScroll(hasVerticalScrollbar(responseArea));
|
|
2160
2205
|
}
|
|
2161
|
-
}, [
|
|
2206
|
+
}, [history]);
|
|
2162
2207
|
useEffect6(() => {
|
|
2163
2208
|
if (historyCallbackRef.current) {
|
|
2164
2209
|
historyCallbackRef.current(history);
|
|
@@ -2281,6 +2326,8 @@ var ChatPanel = ({
|
|
|
2281
2326
|
hasAutoCollapsedRef.current = false;
|
|
2282
2327
|
prevBlockCountRef.current = 0;
|
|
2283
2328
|
setError(null);
|
|
2329
|
+
setUserHasScrolled(false);
|
|
2330
|
+
prevResponseLengthRef.current = 0;
|
|
2284
2331
|
setResponse("");
|
|
2285
2332
|
if (emailInput && isEmailAddress(emailInput) && !emailInputSet) {
|
|
2286
2333
|
const newId = (currentCustomer == null ? void 0 : currentCustomer.customer_id) && currentCustomer.customer_id !== "" && currentCustomer.customer_id !== (currentCustomer == null ? void 0 : currentCustomer.customer_user_email) ? currentCustomer.customer_id : emailInput;
|
|
@@ -4620,7 +4667,7 @@ var AIChatPanel = ({
|
|
|
4620
4667
|
cancelAnimationFrame(scrollRAFRef.current);
|
|
4621
4668
|
}
|
|
4622
4669
|
if (!force && responseAreaRef.current) {
|
|
4623
|
-
const scrollViewport = responseAreaRef.current.querySelector("
|
|
4670
|
+
const scrollViewport = responseAreaRef.current.querySelector(".ai-scroll-area-viewport");
|
|
4624
4671
|
const scrollElement = scrollViewport || responseAreaRef.current;
|
|
4625
4672
|
const scrollTop = scrollElement.scrollTop;
|
|
4626
4673
|
const scrollHeight = scrollElement.scrollHeight;
|
|
@@ -4923,7 +4970,7 @@ var AIChatPanel = ({
|
|
|
4923
4970
|
prevResponseLengthRef.current = currentResponseLength;
|
|
4924
4971
|
const shouldAutoScroll = scrollToEndRef.current || !userHasScrolledRef.current;
|
|
4925
4972
|
if (shouldAutoScroll && response && responseGotLonger) {
|
|
4926
|
-
scrollToBottom(
|
|
4973
|
+
scrollToBottom(true);
|
|
4927
4974
|
}
|
|
4928
4975
|
}, [response, idle]);
|
|
4929
4976
|
const idleRef = useRef6(idle);
|
|
@@ -4931,21 +4978,30 @@ var AIChatPanel = ({
|
|
|
4931
4978
|
useEffect8(() => {
|
|
4932
4979
|
const scrollArea = responseAreaRef.current;
|
|
4933
4980
|
if (!scrollArea) return;
|
|
4934
|
-
const scrollViewport = scrollArea.querySelector("
|
|
4981
|
+
const scrollViewport = scrollArea.querySelector(".ai-scroll-area-viewport");
|
|
4935
4982
|
const scrollElement = scrollViewport || scrollArea;
|
|
4983
|
+
const handleWheel = (e) => {
|
|
4984
|
+
if (idleRef.current) return;
|
|
4985
|
+
if (e.deltaY < 0 && !userHasScrolledRef.current) {
|
|
4986
|
+
setUserHasScrolled(true);
|
|
4987
|
+
}
|
|
4988
|
+
};
|
|
4936
4989
|
const handleScroll = () => {
|
|
4937
|
-
if (idleRef.current || userHasScrolledRef.current) return;
|
|
4938
|
-
const currentScrollTop = scrollElement.scrollTop;
|
|
4990
|
+
if (idleRef.current || !userHasScrolledRef.current) return;
|
|
4939
4991
|
const scrollHeight = scrollElement.scrollHeight;
|
|
4992
|
+
const currentScrollTop = scrollElement.scrollTop;
|
|
4940
4993
|
const clientHeight = scrollElement.clientHeight;
|
|
4941
|
-
const isNearBottom = scrollHeight - currentScrollTop - clientHeight <
|
|
4942
|
-
if (
|
|
4943
|
-
setUserHasScrolled(
|
|
4994
|
+
const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 50;
|
|
4995
|
+
if (isNearBottom) {
|
|
4996
|
+
setUserHasScrolled(false);
|
|
4944
4997
|
}
|
|
4945
|
-
lastScrollTopRef.current = currentScrollTop;
|
|
4946
4998
|
};
|
|
4999
|
+
scrollElement.addEventListener("wheel", handleWheel, { passive: true });
|
|
4947
5000
|
scrollElement.addEventListener("scroll", handleScroll, { passive: true });
|
|
4948
|
-
return () =>
|
|
5001
|
+
return () => {
|
|
5002
|
+
scrollElement.removeEventListener("wheel", handleWheel);
|
|
5003
|
+
scrollElement.removeEventListener("scroll", handleScroll);
|
|
5004
|
+
};
|
|
4949
5005
|
}, []);
|
|
4950
5006
|
useEffect8(() => {
|
|
4951
5007
|
setFollowOnQuestionsState(followOnQuestions);
|
package/package.json
CHANGED
package/src/AIChatPanel.css
CHANGED
|
@@ -2499,6 +2499,23 @@
|
|
|
2499
2499
|
gap: 8px;
|
|
2500
2500
|
}
|
|
2501
2501
|
|
|
2502
|
+
/* Icon Wrapper - for spinning animation */
|
|
2503
|
+
.thinking-block__icon-wrapper {
|
|
2504
|
+
display: flex;
|
|
2505
|
+
align-items: center;
|
|
2506
|
+
justify-content: center;
|
|
2507
|
+
flex-shrink: 0;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
.thinking-block__icon-wrapper--spinning {
|
|
2511
|
+
animation: thinkingIconSpin 1.5s linear infinite;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
@keyframes thinkingIconSpin {
|
|
2515
|
+
from { transform: rotate(0deg); }
|
|
2516
|
+
to { transform: rotate(360deg); }
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2502
2519
|
/* Icon */
|
|
2503
2520
|
.thinking-block__icon {
|
|
2504
2521
|
width: 16px;
|
|
@@ -2512,6 +2529,33 @@
|
|
|
2512
2529
|
font-weight: 600;
|
|
2513
2530
|
font-size: 13px;
|
|
2514
2531
|
color: var(--thinking-block-text, var(--ai-chat-thinking-text));
|
|
2532
|
+
position: relative;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
/* Shimmer effect on title when streaming */
|
|
2536
|
+
.thinking-block__title--shimmer {
|
|
2537
|
+
background: linear-gradient(
|
|
2538
|
+
90deg,
|
|
2539
|
+
var(--thinking-block-text, var(--ai-chat-thinking-text)) 0%,
|
|
2540
|
+
var(--thinking-block-text, var(--ai-chat-thinking-text)) 40%,
|
|
2541
|
+
var(--thinking-block-accent, var(--ai-chat-thinking-icon)) 50%,
|
|
2542
|
+
var(--thinking-block-text, var(--ai-chat-thinking-text)) 60%,
|
|
2543
|
+
var(--thinking-block-text, var(--ai-chat-thinking-text)) 100%
|
|
2544
|
+
);
|
|
2545
|
+
background-size: 200% 100%;
|
|
2546
|
+
-webkit-background-clip: text;
|
|
2547
|
+
background-clip: text;
|
|
2548
|
+
-webkit-text-fill-color: transparent;
|
|
2549
|
+
animation: titleShimmer 2s ease-in-out infinite;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
@keyframes titleShimmer {
|
|
2553
|
+
0% {
|
|
2554
|
+
background-position: 100% 0;
|
|
2555
|
+
}
|
|
2556
|
+
100% {
|
|
2557
|
+
background-position: -100% 0;
|
|
2558
|
+
}
|
|
2515
2559
|
}
|
|
2516
2560
|
|
|
2517
2561
|
/* Chevron (collapse indicator) */
|
|
@@ -2703,6 +2747,140 @@
|
|
|
2703
2747
|
border-left-width: 3px;
|
|
2704
2748
|
}
|
|
2705
2749
|
|
|
2750
|
+
/* ============================================================================
|
|
2751
|
+
Collapsed State - Compact Line Style with Preview
|
|
2752
|
+
============================================================================ */
|
|
2753
|
+
|
|
2754
|
+
.thinking-block--collapsed {
|
|
2755
|
+
margin-bottom: 4px;
|
|
2756
|
+
border-radius: 6px;
|
|
2757
|
+
border-width: 0;
|
|
2758
|
+
background-color: transparent;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
.thinking-block--collapsed .thinking-block__header {
|
|
2762
|
+
padding: 4px 8px;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
.thinking-block--collapsed .thinking-block__header-left {
|
|
2766
|
+
gap: 6px;
|
|
2767
|
+
flex: 1;
|
|
2768
|
+
min-width: 0;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
.thinking-block--collapsed .thinking-block__icon-wrapper {
|
|
2772
|
+
flex-shrink: 0;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
.thinking-block--collapsed .thinking-block__icon {
|
|
2776
|
+
width: 14px;
|
|
2777
|
+
height: 14px;
|
|
2778
|
+
opacity: 0.6;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
.thinking-block--collapsed .thinking-block__title {
|
|
2782
|
+
font-size: 12px;
|
|
2783
|
+
font-weight: 500;
|
|
2784
|
+
opacity: 0.7;
|
|
2785
|
+
flex-shrink: 0;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
/* Preview text in collapsed state */
|
|
2789
|
+
.thinking-block__preview {
|
|
2790
|
+
font-size: 11px;
|
|
2791
|
+
font-weight: 400;
|
|
2792
|
+
opacity: 0.5;
|
|
2793
|
+
color: var(--thinking-block-text, var(--ai-chat-thinking-text));
|
|
2794
|
+
white-space: nowrap;
|
|
2795
|
+
overflow: hidden;
|
|
2796
|
+
text-overflow: ellipsis;
|
|
2797
|
+
flex: 1;
|
|
2798
|
+
min-width: 0;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
.thinking-block--collapsed .thinking-block__chevron {
|
|
2802
|
+
width: 12px;
|
|
2803
|
+
height: 12px;
|
|
2804
|
+
opacity: 0.4;
|
|
2805
|
+
flex-shrink: 0;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
/* Hover state for collapsed - subtle highlight */
|
|
2809
|
+
.thinking-block--collapsed:hover {
|
|
2810
|
+
background-color: rgba(0, 0, 0, 0.04);
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
.thinking-block--collapsed:hover .thinking-block__icon,
|
|
2814
|
+
.thinking-block--collapsed:hover .thinking-block__title {
|
|
2815
|
+
opacity: 0.9;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
.thinking-block--collapsed:hover .thinking-block__preview {
|
|
2819
|
+
opacity: 0.7;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
.thinking-block--collapsed:hover .thinking-block__chevron {
|
|
2823
|
+
opacity: 0.6;
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
.dark-theme .thinking-block--collapsed:hover {
|
|
2827
|
+
background-color: rgba(255, 255, 255, 0.06);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
/* ============================================================================
|
|
2831
|
+
Expanded State - Subtle, Transient Content
|
|
2832
|
+
============================================================================ */
|
|
2833
|
+
|
|
2834
|
+
.thinking-block:not(.thinking-block--collapsed) {
|
|
2835
|
+
margin-bottom: 8px;
|
|
2836
|
+
border-radius: 6px;
|
|
2837
|
+
border: none;
|
|
2838
|
+
background-color: transparent;
|
|
2839
|
+
opacity: 0.75;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
.thinking-block:not(.thinking-block--collapsed):hover {
|
|
2843
|
+
opacity: 0.9;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__header {
|
|
2847
|
+
padding: 4px 10px;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__icon {
|
|
2851
|
+
width: 14px;
|
|
2852
|
+
height: 14px;
|
|
2853
|
+
opacity: 0.7;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__title {
|
|
2857
|
+
font-size: 12px;
|
|
2858
|
+
opacity: 0.8;
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__chevron {
|
|
2862
|
+
opacity: 0.5;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__content {
|
|
2866
|
+
padding: 0 10px 6px 10px;
|
|
2867
|
+
font-size: 11px;
|
|
2868
|
+
line-height: 1.5;
|
|
2869
|
+
opacity: 0.7;
|
|
2870
|
+
max-height: 120px;
|
|
2871
|
+
overflow-y: auto;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
/* Streaming state gets slightly more presence */
|
|
2875
|
+
.thinking-block--streaming:not(.thinking-block--collapsed) {
|
|
2876
|
+
opacity: 0.85;
|
|
2877
|
+
background-color: rgba(0, 0, 0, 0.015);
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
.dark-theme .thinking-block--streaming:not(.thinking-block--collapsed) {
|
|
2881
|
+
background-color: rgba(255, 255, 255, 0.02);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2706
2884
|
/* Animation when block first appears */
|
|
2707
2885
|
.thinking-block {
|
|
2708
2886
|
animation: thinkingBlockAppear 0.2s ease-out;
|
package/src/AIChatPanel.tsx
CHANGED
|
@@ -1585,7 +1585,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1585
1585
|
|
|
1586
1586
|
// Check if we should scroll - only if user is near bottom or force is true
|
|
1587
1587
|
if (!force && responseAreaRef.current) {
|
|
1588
|
-
const scrollViewport = responseAreaRef.current.querySelector('
|
|
1588
|
+
const scrollViewport = responseAreaRef.current.querySelector('.ai-scroll-area-viewport') as HTMLElement;
|
|
1589
1589
|
const scrollElement = scrollViewport || responseAreaRef.current;
|
|
1590
1590
|
|
|
1591
1591
|
const scrollTop = scrollElement.scrollTop;
|
|
@@ -2038,8 +2038,9 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2038
2038
|
// Only scroll if response actually grew - use refs to avoid unnecessary effect runs
|
|
2039
2039
|
const shouldAutoScroll = scrollToEndRef.current || !userHasScrolledRef.current;
|
|
2040
2040
|
if (shouldAutoScroll && response && responseGotLonger) {
|
|
2041
|
-
// Use
|
|
2042
|
-
scrollToBottom
|
|
2041
|
+
// Use forced scroll since userHasScrolled is our gatekeeper for user intent
|
|
2042
|
+
// The isNearBottom check in scrollToBottom is for layout changes, not streaming
|
|
2043
|
+
scrollToBottom(true);
|
|
2043
2044
|
}
|
|
2044
2045
|
}, [response, idle]); // ONLY response and idle - no other dependencies!
|
|
2045
2046
|
|
|
@@ -2047,39 +2048,48 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2047
2048
|
const idleRef = useRef(idle);
|
|
2048
2049
|
idleRef.current = idle;
|
|
2049
2050
|
|
|
2050
|
-
// Detect user scroll
|
|
2051
|
+
// Detect user scroll intent via wheel event (fires before scroll position changes)
|
|
2051
2052
|
useEffect(() => {
|
|
2052
2053
|
const scrollArea = responseAreaRef.current;
|
|
2053
2054
|
if (!scrollArea) return;
|
|
2054
2055
|
|
|
2055
2056
|
// Get the actual scrollable element (ScrollArea wraps a viewport)
|
|
2056
|
-
const scrollViewport = scrollArea.querySelector('
|
|
2057
|
+
const scrollViewport = scrollArea.querySelector('.ai-scroll-area-viewport') as HTMLElement;
|
|
2057
2058
|
const scrollElement = scrollViewport || scrollArea;
|
|
2058
2059
|
|
|
2060
|
+
// Wheel event detects user intent immediately (before scroll position changes)
|
|
2061
|
+
const handleWheel = (e: WheelEvent) => {
|
|
2062
|
+
// Skip if not streaming
|
|
2063
|
+
if (idleRef.current) return;
|
|
2064
|
+
|
|
2065
|
+
// deltaY < 0 means scrolling UP (toward top of document)
|
|
2066
|
+
if (e.deltaY < 0 && !userHasScrolledRef.current) {
|
|
2067
|
+
setUserHasScrolled(true);
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
|
|
2071
|
+
// Scroll event for detecting when user returns to bottom
|
|
2059
2072
|
const handleScroll = () => {
|
|
2060
|
-
// Skip if not streaming or
|
|
2061
|
-
if (idleRef.current || userHasScrolledRef.current) return;
|
|
2073
|
+
// Skip if not streaming or user hasn't scrolled up
|
|
2074
|
+
if (idleRef.current || !userHasScrolledRef.current) return;
|
|
2062
2075
|
|
|
2063
|
-
const currentScrollTop = scrollElement.scrollTop;
|
|
2064
2076
|
const scrollHeight = scrollElement.scrollHeight;
|
|
2077
|
+
const currentScrollTop = scrollElement.scrollTop;
|
|
2065
2078
|
const clientHeight = scrollElement.clientHeight;
|
|
2066
2079
|
|
|
2067
|
-
//
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
// User scrolled up if:
|
|
2072
|
-
// 1. They scrolled upward (currentScrollTop < lastScrollTopRef.current)
|
|
2073
|
-
// 2. AND they're not near the bottom
|
|
2074
|
-
if (currentScrollTop < lastScrollTopRef.current && !isNearBottom) {
|
|
2075
|
-
setUserHasScrolled(true);
|
|
2080
|
+
// Re-enable auto-scroll when user scrolls back near bottom
|
|
2081
|
+
const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 50;
|
|
2082
|
+
if (isNearBottom) {
|
|
2083
|
+
setUserHasScrolled(false);
|
|
2076
2084
|
}
|
|
2077
|
-
|
|
2078
|
-
lastScrollTopRef.current = currentScrollTop;
|
|
2079
2085
|
};
|
|
2080
2086
|
|
|
2087
|
+
scrollElement.addEventListener('wheel', handleWheel, { passive: true });
|
|
2081
2088
|
scrollElement.addEventListener('scroll', handleScroll, { passive: true });
|
|
2082
|
-
return () =>
|
|
2089
|
+
return () => {
|
|
2090
|
+
scrollElement.removeEventListener('wheel', handleWheel);
|
|
2091
|
+
scrollElement.removeEventListener('scroll', handleScroll);
|
|
2092
|
+
};
|
|
2083
2093
|
}, []); // Empty deps - handler uses refs to get current values
|
|
2084
2094
|
|
|
2085
2095
|
// Update follow-on questions from props
|
package/src/ChatPanel.css
CHANGED
|
@@ -1359,6 +1359,23 @@ button[data-pending="true"]::after {
|
|
|
1359
1359
|
gap: 8px;
|
|
1360
1360
|
}
|
|
1361
1361
|
|
|
1362
|
+
/* Icon Wrapper - for spinning animation */
|
|
1363
|
+
.thinking-block__icon-wrapper {
|
|
1364
|
+
display: flex;
|
|
1365
|
+
align-items: center;
|
|
1366
|
+
justify-content: center;
|
|
1367
|
+
flex-shrink: 0;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.thinking-block__icon-wrapper--spinning {
|
|
1371
|
+
animation: thinkingIconSpin 1.5s linear infinite;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
@keyframes thinkingIconSpin {
|
|
1375
|
+
from { transform: rotate(0deg); }
|
|
1376
|
+
to { transform: rotate(360deg); }
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1362
1379
|
/* Icon */
|
|
1363
1380
|
.thinking-block__icon {
|
|
1364
1381
|
width: 16px;
|
|
@@ -1372,6 +1389,33 @@ button[data-pending="true"]::after {
|
|
|
1372
1389
|
font-weight: 600;
|
|
1373
1390
|
font-size: 13px;
|
|
1374
1391
|
color: var(--thinking-block-text, var(--title-text-color));
|
|
1392
|
+
position: relative;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/* Shimmer effect on title when streaming */
|
|
1396
|
+
.thinking-block__title--shimmer {
|
|
1397
|
+
background: linear-gradient(
|
|
1398
|
+
90deg,
|
|
1399
|
+
var(--thinking-block-text, var(--title-text-color)) 0%,
|
|
1400
|
+
var(--thinking-block-text, var(--title-text-color)) 40%,
|
|
1401
|
+
var(--thinking-block-accent, var(--ai-chat-thinking-icon)) 50%,
|
|
1402
|
+
var(--thinking-block-text, var(--title-text-color)) 60%,
|
|
1403
|
+
var(--thinking-block-text, var(--title-text-color)) 100%
|
|
1404
|
+
);
|
|
1405
|
+
background-size: 200% 100%;
|
|
1406
|
+
-webkit-background-clip: text;
|
|
1407
|
+
background-clip: text;
|
|
1408
|
+
-webkit-text-fill-color: transparent;
|
|
1409
|
+
animation: titleShimmer 2s ease-in-out infinite;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
@keyframes titleShimmer {
|
|
1413
|
+
0% {
|
|
1414
|
+
background-position: 100% 0;
|
|
1415
|
+
}
|
|
1416
|
+
100% {
|
|
1417
|
+
background-position: -100% 0;
|
|
1418
|
+
}
|
|
1375
1419
|
}
|
|
1376
1420
|
|
|
1377
1421
|
/* Chevron (collapse indicator) */
|
|
@@ -1563,6 +1607,140 @@ button[data-pending="true"]::after {
|
|
|
1563
1607
|
border-left-width: 3px;
|
|
1564
1608
|
}
|
|
1565
1609
|
|
|
1610
|
+
/* ============================================================================
|
|
1611
|
+
Collapsed State - Compact Line Style with Preview
|
|
1612
|
+
============================================================================ */
|
|
1613
|
+
|
|
1614
|
+
.thinking-block--collapsed {
|
|
1615
|
+
margin-bottom: 4px;
|
|
1616
|
+
border-radius: 6px;
|
|
1617
|
+
border-width: 0;
|
|
1618
|
+
background-color: transparent;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
.thinking-block--collapsed .thinking-block__header {
|
|
1622
|
+
padding: 4px 8px;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
.thinking-block--collapsed .thinking-block__header-left {
|
|
1626
|
+
gap: 6px;
|
|
1627
|
+
flex: 1;
|
|
1628
|
+
min-width: 0;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
.thinking-block--collapsed .thinking-block__icon-wrapper {
|
|
1632
|
+
flex-shrink: 0;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
.thinking-block--collapsed .thinking-block__icon {
|
|
1636
|
+
width: 14px;
|
|
1637
|
+
height: 14px;
|
|
1638
|
+
opacity: 0.6;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
.thinking-block--collapsed .thinking-block__title {
|
|
1642
|
+
font-size: 12px;
|
|
1643
|
+
font-weight: 500;
|
|
1644
|
+
opacity: 0.7;
|
|
1645
|
+
flex-shrink: 0;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/* Preview text in collapsed state */
|
|
1649
|
+
.thinking-block__preview {
|
|
1650
|
+
font-size: 11px;
|
|
1651
|
+
font-weight: 400;
|
|
1652
|
+
opacity: 0.5;
|
|
1653
|
+
color: var(--thinking-block-text, var(--title-text-color));
|
|
1654
|
+
white-space: nowrap;
|
|
1655
|
+
overflow: hidden;
|
|
1656
|
+
text-overflow: ellipsis;
|
|
1657
|
+
flex: 1;
|
|
1658
|
+
min-width: 0;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
.thinking-block--collapsed .thinking-block__chevron {
|
|
1662
|
+
width: 12px;
|
|
1663
|
+
height: 12px;
|
|
1664
|
+
opacity: 0.4;
|
|
1665
|
+
flex-shrink: 0;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/* Hover state for collapsed - subtle highlight */
|
|
1669
|
+
.thinking-block--collapsed:hover {
|
|
1670
|
+
background-color: rgba(0, 0, 0, 0.04);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
.thinking-block--collapsed:hover .thinking-block__icon,
|
|
1674
|
+
.thinking-block--collapsed:hover .thinking-block__title {
|
|
1675
|
+
opacity: 0.9;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
.thinking-block--collapsed:hover .thinking-block__preview {
|
|
1679
|
+
opacity: 0.7;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
.thinking-block--collapsed:hover .thinking-block__chevron {
|
|
1683
|
+
opacity: 0.6;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.dark-theme .thinking-block--collapsed:hover {
|
|
1687
|
+
background-color: rgba(255, 255, 255, 0.06);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/* ============================================================================
|
|
1691
|
+
Expanded State - Subtle, Transient Content
|
|
1692
|
+
============================================================================ */
|
|
1693
|
+
|
|
1694
|
+
.thinking-block:not(.thinking-block--collapsed) {
|
|
1695
|
+
margin-bottom: 8px;
|
|
1696
|
+
border-radius: 6px;
|
|
1697
|
+
border: none;
|
|
1698
|
+
background-color: transparent;
|
|
1699
|
+
opacity: 0.75;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
.thinking-block:not(.thinking-block--collapsed):hover {
|
|
1703
|
+
opacity: 0.9;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__header {
|
|
1707
|
+
padding: 4px 10px;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__icon {
|
|
1711
|
+
width: 14px;
|
|
1712
|
+
height: 14px;
|
|
1713
|
+
opacity: 0.7;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__title {
|
|
1717
|
+
font-size: 12px;
|
|
1718
|
+
opacity: 0.8;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__chevron {
|
|
1722
|
+
opacity: 0.5;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
.thinking-block:not(.thinking-block--collapsed) .thinking-block__content {
|
|
1726
|
+
padding: 0 10px 6px 10px;
|
|
1727
|
+
font-size: 11px;
|
|
1728
|
+
line-height: 1.5;
|
|
1729
|
+
opacity: 0.7;
|
|
1730
|
+
max-height: 120px;
|
|
1731
|
+
overflow-y: auto;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/* Streaming state gets slightly more presence */
|
|
1735
|
+
.thinking-block--streaming:not(.thinking-block--collapsed) {
|
|
1736
|
+
opacity: 0.85;
|
|
1737
|
+
background-color: rgba(0, 0, 0, 0.015);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
.dark-theme .thinking-block--streaming:not(.thinking-block--collapsed) {
|
|
1741
|
+
background-color: rgba(255, 255, 255, 0.02);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1566
1744
|
/* Animation when block first appears */
|
|
1567
1745
|
.thinking-block {
|
|
1568
1746
|
animation: thinkingBlockAppear 0.2s ease-out;
|