@hef2024/llmasaservice-ui 0.24.0 → 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.js +79 -28
- package/dist/index.mjs +79 -28
- package/package.json +1 -1
- package/src/AIChatPanel.tsx +30 -20
- package/src/ChatPanel.tsx +89 -21
package/dist/index.js
CHANGED
|
@@ -847,6 +847,11 @@ var ChatPanel = ({
|
|
|
847
847
|
}, []);
|
|
848
848
|
const [iframeUrl, setIframeUrl] = (0, import_react11.useState)(null);
|
|
849
849
|
const responseAreaRef = (0, import_react11.useRef)(null);
|
|
850
|
+
const [userHasScrolled, setUserHasScrolled] = (0, import_react11.useState)(false);
|
|
851
|
+
const lastScrollTopRef = (0, import_react11.useRef)(0);
|
|
852
|
+
const prevResponseLengthRef = (0, import_react11.useRef)(0);
|
|
853
|
+
const userHasScrolledRef = (0, import_react11.useRef)(false);
|
|
854
|
+
const idleRef = (0, import_react11.useRef)(true);
|
|
850
855
|
const THINKING_PATTERNS = (0, import_react11.useMemo)(
|
|
851
856
|
() => ({
|
|
852
857
|
thinking: /<thinking>([\s\S]*?)<\/thinking>/gi,
|
|
@@ -2179,32 +2184,67 @@ var ChatPanel = ({
|
|
|
2179
2184
|
}
|
|
2180
2185
|
}
|
|
2181
2186
|
}, [initialPrompt]);
|
|
2187
|
+
(0, import_react11.useEffect)(() => {
|
|
2188
|
+
userHasScrolledRef.current = userHasScrolled;
|
|
2189
|
+
}, [userHasScrolled]);
|
|
2190
|
+
(0, import_react11.useEffect)(() => {
|
|
2191
|
+
idleRef.current = idle;
|
|
2192
|
+
}, [idle]);
|
|
2182
2193
|
(0, import_react11.useEffect)(() => {
|
|
2183
2194
|
var _a2;
|
|
2184
|
-
if (
|
|
2195
|
+
if (idle) return;
|
|
2196
|
+
const currentResponseLength = response.length;
|
|
2197
|
+
const responseGotLonger = currentResponseLength > prevResponseLengthRef.current;
|
|
2198
|
+
prevResponseLengthRef.current = currentResponseLength;
|
|
2199
|
+
const shouldAutoScroll = scrollToEnd || !userHasScrolledRef.current;
|
|
2200
|
+
if (shouldAutoScroll && response && responseGotLonger) {
|
|
2185
2201
|
if (window.top !== window.self) {
|
|
2186
2202
|
const responseArea = responseAreaRef.current;
|
|
2187
|
-
responseArea
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2203
|
+
if (responseArea) {
|
|
2204
|
+
responseArea.scrollTo({
|
|
2205
|
+
top: responseArea.scrollHeight,
|
|
2206
|
+
behavior: "auto"
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2191
2209
|
} else {
|
|
2192
|
-
(_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "
|
|
2210
|
+
(_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "auto", block: "end" });
|
|
2193
2211
|
}
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2212
|
+
}
|
|
2213
|
+
}, [response, idle, scrollToEnd]);
|
|
2214
|
+
(0, import_react11.useEffect)(() => {
|
|
2215
|
+
const responseArea = responseAreaRef.current;
|
|
2216
|
+
if (!responseArea) return;
|
|
2217
|
+
const handleWheel = (e) => {
|
|
2218
|
+
if (idleRef.current) return;
|
|
2219
|
+
if (e.deltaY < 0 && !userHasScrolledRef.current) {
|
|
2220
|
+
setUserHasScrolled(true);
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
const handleScroll = () => {
|
|
2224
|
+
setHasScroll(hasVerticalScrollbar(responseArea));
|
|
2225
|
+
const isScrolledToBottom = responseArea.scrollHeight - responseArea.scrollTop - responseArea.clientHeight < 5;
|
|
2226
|
+
setIsAtBottom(isScrolledToBottom);
|
|
2227
|
+
if (!idleRef.current && userHasScrolledRef.current) {
|
|
2228
|
+
const isNearBottom = responseArea.scrollHeight - responseArea.scrollTop - responseArea.clientHeight < 50;
|
|
2229
|
+
if (isNearBottom) {
|
|
2230
|
+
setUserHasScrolled(false);
|
|
2231
|
+
}
|
|
2205
2232
|
}
|
|
2233
|
+
};
|
|
2234
|
+
handleScroll();
|
|
2235
|
+
responseArea.addEventListener("wheel", handleWheel, { passive: true });
|
|
2236
|
+
responseArea.addEventListener("scroll", handleScroll, { passive: true });
|
|
2237
|
+
return () => {
|
|
2238
|
+
responseArea.removeEventListener("wheel", handleWheel);
|
|
2239
|
+
responseArea.removeEventListener("scroll", handleScroll);
|
|
2240
|
+
};
|
|
2241
|
+
}, []);
|
|
2242
|
+
(0, import_react11.useEffect)(() => {
|
|
2243
|
+
const responseArea = responseAreaRef.current;
|
|
2244
|
+
if (responseArea) {
|
|
2245
|
+
setHasScroll(hasVerticalScrollbar(responseArea));
|
|
2206
2246
|
}
|
|
2207
|
-
}, [
|
|
2247
|
+
}, [history]);
|
|
2208
2248
|
(0, import_react11.useEffect)(() => {
|
|
2209
2249
|
if (historyCallbackRef.current) {
|
|
2210
2250
|
historyCallbackRef.current(history);
|
|
@@ -2327,6 +2367,8 @@ var ChatPanel = ({
|
|
|
2327
2367
|
hasAutoCollapsedRef.current = false;
|
|
2328
2368
|
prevBlockCountRef.current = 0;
|
|
2329
2369
|
setError(null);
|
|
2370
|
+
setUserHasScrolled(false);
|
|
2371
|
+
prevResponseLengthRef.current = 0;
|
|
2330
2372
|
setResponse("");
|
|
2331
2373
|
if (emailInput && isEmailAddress(emailInput) && !emailInputSet) {
|
|
2332
2374
|
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;
|
|
@@ -4660,7 +4702,7 @@ var AIChatPanel = ({
|
|
|
4660
4702
|
cancelAnimationFrame(scrollRAFRef.current);
|
|
4661
4703
|
}
|
|
4662
4704
|
if (!force && responseAreaRef.current) {
|
|
4663
|
-
const scrollViewport = responseAreaRef.current.querySelector("
|
|
4705
|
+
const scrollViewport = responseAreaRef.current.querySelector(".ai-scroll-area-viewport");
|
|
4664
4706
|
const scrollElement = scrollViewport || responseAreaRef.current;
|
|
4665
4707
|
const scrollTop = scrollElement.scrollTop;
|
|
4666
4708
|
const scrollHeight = scrollElement.scrollHeight;
|
|
@@ -4963,7 +5005,7 @@ var AIChatPanel = ({
|
|
|
4963
5005
|
prevResponseLengthRef.current = currentResponseLength;
|
|
4964
5006
|
const shouldAutoScroll = scrollToEndRef.current || !userHasScrolledRef.current;
|
|
4965
5007
|
if (shouldAutoScroll && response && responseGotLonger) {
|
|
4966
|
-
scrollToBottom(
|
|
5008
|
+
scrollToBottom(true);
|
|
4967
5009
|
}
|
|
4968
5010
|
}, [response, idle]);
|
|
4969
5011
|
const idleRef = (0, import_react14.useRef)(idle);
|
|
@@ -4971,21 +5013,30 @@ var AIChatPanel = ({
|
|
|
4971
5013
|
(0, import_react14.useEffect)(() => {
|
|
4972
5014
|
const scrollArea = responseAreaRef.current;
|
|
4973
5015
|
if (!scrollArea) return;
|
|
4974
|
-
const scrollViewport = scrollArea.querySelector("
|
|
5016
|
+
const scrollViewport = scrollArea.querySelector(".ai-scroll-area-viewport");
|
|
4975
5017
|
const scrollElement = scrollViewport || scrollArea;
|
|
5018
|
+
const handleWheel = (e) => {
|
|
5019
|
+
if (idleRef.current) return;
|
|
5020
|
+
if (e.deltaY < 0 && !userHasScrolledRef.current) {
|
|
5021
|
+
setUserHasScrolled(true);
|
|
5022
|
+
}
|
|
5023
|
+
};
|
|
4976
5024
|
const handleScroll = () => {
|
|
4977
|
-
if (idleRef.current || userHasScrolledRef.current) return;
|
|
4978
|
-
const currentScrollTop = scrollElement.scrollTop;
|
|
5025
|
+
if (idleRef.current || !userHasScrolledRef.current) return;
|
|
4979
5026
|
const scrollHeight = scrollElement.scrollHeight;
|
|
5027
|
+
const currentScrollTop = scrollElement.scrollTop;
|
|
4980
5028
|
const clientHeight = scrollElement.clientHeight;
|
|
4981
|
-
const isNearBottom = scrollHeight - currentScrollTop - clientHeight <
|
|
4982
|
-
if (
|
|
4983
|
-
setUserHasScrolled(
|
|
5029
|
+
const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 50;
|
|
5030
|
+
if (isNearBottom) {
|
|
5031
|
+
setUserHasScrolled(false);
|
|
4984
5032
|
}
|
|
4985
|
-
lastScrollTopRef.current = currentScrollTop;
|
|
4986
5033
|
};
|
|
5034
|
+
scrollElement.addEventListener("wheel", handleWheel, { passive: true });
|
|
4987
5035
|
scrollElement.addEventListener("scroll", handleScroll, { passive: true });
|
|
4988
|
-
return () =>
|
|
5036
|
+
return () => {
|
|
5037
|
+
scrollElement.removeEventListener("wheel", handleWheel);
|
|
5038
|
+
scrollElement.removeEventListener("scroll", handleScroll);
|
|
5039
|
+
};
|
|
4989
5040
|
}, []);
|
|
4990
5041
|
(0, import_react14.useEffect)(() => {
|
|
4991
5042
|
setFollowOnQuestionsState(followOnQuestions);
|
package/dist/index.mjs
CHANGED
|
@@ -806,6 +806,11 @@ var ChatPanel = ({
|
|
|
806
806
|
}, []);
|
|
807
807
|
const [iframeUrl, setIframeUrl] = useState5(null);
|
|
808
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);
|
|
809
814
|
const THINKING_PATTERNS = useMemo(
|
|
810
815
|
() => ({
|
|
811
816
|
thinking: /<thinking>([\s\S]*?)<\/thinking>/gi,
|
|
@@ -2138,32 +2143,67 @@ var ChatPanel = ({
|
|
|
2138
2143
|
}
|
|
2139
2144
|
}
|
|
2140
2145
|
}, [initialPrompt]);
|
|
2146
|
+
useEffect6(() => {
|
|
2147
|
+
userHasScrolledRef.current = userHasScrolled;
|
|
2148
|
+
}, [userHasScrolled]);
|
|
2149
|
+
useEffect6(() => {
|
|
2150
|
+
idleRef.current = idle;
|
|
2151
|
+
}, [idle]);
|
|
2141
2152
|
useEffect6(() => {
|
|
2142
2153
|
var _a2;
|
|
2143
|
-
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) {
|
|
2144
2160
|
if (window.top !== window.self) {
|
|
2145
2161
|
const responseArea = responseAreaRef.current;
|
|
2146
|
-
responseArea
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2162
|
+
if (responseArea) {
|
|
2163
|
+
responseArea.scrollTo({
|
|
2164
|
+
top: responseArea.scrollHeight,
|
|
2165
|
+
behavior: "auto"
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2150
2168
|
} else {
|
|
2151
|
-
(_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "
|
|
2169
|
+
(_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "auto", block: "end" });
|
|
2152
2170
|
}
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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);
|
|
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
|
+
}
|
|
2164
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));
|
|
2165
2205
|
}
|
|
2166
|
-
}, [
|
|
2206
|
+
}, [history]);
|
|
2167
2207
|
useEffect6(() => {
|
|
2168
2208
|
if (historyCallbackRef.current) {
|
|
2169
2209
|
historyCallbackRef.current(history);
|
|
@@ -2286,6 +2326,8 @@ var ChatPanel = ({
|
|
|
2286
2326
|
hasAutoCollapsedRef.current = false;
|
|
2287
2327
|
prevBlockCountRef.current = 0;
|
|
2288
2328
|
setError(null);
|
|
2329
|
+
setUserHasScrolled(false);
|
|
2330
|
+
prevResponseLengthRef.current = 0;
|
|
2289
2331
|
setResponse("");
|
|
2290
2332
|
if (emailInput && isEmailAddress(emailInput) && !emailInputSet) {
|
|
2291
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;
|
|
@@ -4625,7 +4667,7 @@ var AIChatPanel = ({
|
|
|
4625
4667
|
cancelAnimationFrame(scrollRAFRef.current);
|
|
4626
4668
|
}
|
|
4627
4669
|
if (!force && responseAreaRef.current) {
|
|
4628
|
-
const scrollViewport = responseAreaRef.current.querySelector("
|
|
4670
|
+
const scrollViewport = responseAreaRef.current.querySelector(".ai-scroll-area-viewport");
|
|
4629
4671
|
const scrollElement = scrollViewport || responseAreaRef.current;
|
|
4630
4672
|
const scrollTop = scrollElement.scrollTop;
|
|
4631
4673
|
const scrollHeight = scrollElement.scrollHeight;
|
|
@@ -4928,7 +4970,7 @@ var AIChatPanel = ({
|
|
|
4928
4970
|
prevResponseLengthRef.current = currentResponseLength;
|
|
4929
4971
|
const shouldAutoScroll = scrollToEndRef.current || !userHasScrolledRef.current;
|
|
4930
4972
|
if (shouldAutoScroll && response && responseGotLonger) {
|
|
4931
|
-
scrollToBottom(
|
|
4973
|
+
scrollToBottom(true);
|
|
4932
4974
|
}
|
|
4933
4975
|
}, [response, idle]);
|
|
4934
4976
|
const idleRef = useRef6(idle);
|
|
@@ -4936,21 +4978,30 @@ var AIChatPanel = ({
|
|
|
4936
4978
|
useEffect8(() => {
|
|
4937
4979
|
const scrollArea = responseAreaRef.current;
|
|
4938
4980
|
if (!scrollArea) return;
|
|
4939
|
-
const scrollViewport = scrollArea.querySelector("
|
|
4981
|
+
const scrollViewport = scrollArea.querySelector(".ai-scroll-area-viewport");
|
|
4940
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
|
+
};
|
|
4941
4989
|
const handleScroll = () => {
|
|
4942
|
-
if (idleRef.current || userHasScrolledRef.current) return;
|
|
4943
|
-
const currentScrollTop = scrollElement.scrollTop;
|
|
4990
|
+
if (idleRef.current || !userHasScrolledRef.current) return;
|
|
4944
4991
|
const scrollHeight = scrollElement.scrollHeight;
|
|
4992
|
+
const currentScrollTop = scrollElement.scrollTop;
|
|
4945
4993
|
const clientHeight = scrollElement.clientHeight;
|
|
4946
|
-
const isNearBottom = scrollHeight - currentScrollTop - clientHeight <
|
|
4947
|
-
if (
|
|
4948
|
-
setUserHasScrolled(
|
|
4994
|
+
const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 50;
|
|
4995
|
+
if (isNearBottom) {
|
|
4996
|
+
setUserHasScrolled(false);
|
|
4949
4997
|
}
|
|
4950
|
-
lastScrollTopRef.current = currentScrollTop;
|
|
4951
4998
|
};
|
|
4999
|
+
scrollElement.addEventListener("wheel", handleWheel, { passive: true });
|
|
4952
5000
|
scrollElement.addEventListener("scroll", handleScroll, { passive: true });
|
|
4953
|
-
return () =>
|
|
5001
|
+
return () => {
|
|
5002
|
+
scrollElement.removeEventListener("wheel", handleWheel);
|
|
5003
|
+
scrollElement.removeEventListener("scroll", handleScroll);
|
|
5004
|
+
};
|
|
4954
5005
|
}, []);
|
|
4955
5006
|
useEffect8(() => {
|
|
4956
5007
|
setFollowOnQuestionsState(followOnQuestions);
|
package/package.json
CHANGED
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.tsx
CHANGED
|
@@ -315,6 +315,13 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
|
|
|
315
315
|
const [iframeUrl, setIframeUrl] = useState<string | null>(null);
|
|
316
316
|
const responseAreaRef = useRef(null);
|
|
317
317
|
|
|
318
|
+
// Auto-scroll state - tracks if user has manually scrolled during streaming
|
|
319
|
+
const [userHasScrolled, setUserHasScrolled] = useState(false);
|
|
320
|
+
const lastScrollTopRef = useRef<number>(0);
|
|
321
|
+
const prevResponseLengthRef = useRef<number>(0);
|
|
322
|
+
const userHasScrolledRef = useRef<boolean>(false);
|
|
323
|
+
const idleRef = useRef<boolean>(true);
|
|
324
|
+
|
|
318
325
|
// Memoized regex patterns to avoid recreation on every render
|
|
319
326
|
const THINKING_PATTERNS = useMemo(
|
|
320
327
|
() => ({
|
|
@@ -2134,34 +2141,91 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
|
|
|
2134
2141
|
}
|
|
2135
2142
|
}, [initialPrompt]);
|
|
2136
2143
|
|
|
2144
|
+
// Keep refs in sync with state
|
|
2145
|
+
useEffect(() => {
|
|
2146
|
+
userHasScrolledRef.current = userHasScrolled;
|
|
2147
|
+
}, [userHasScrolled]);
|
|
2148
|
+
|
|
2149
|
+
useEffect(() => {
|
|
2150
|
+
idleRef.current = idle;
|
|
2151
|
+
}, [idle]);
|
|
2152
|
+
|
|
2153
|
+
// Auto-scroll to bottom during streaming (when user hasn't scrolled up)
|
|
2137
2154
|
useEffect(() => {
|
|
2138
|
-
if (
|
|
2155
|
+
// Skip if idle (not streaming)
|
|
2156
|
+
if (idle) return;
|
|
2157
|
+
|
|
2158
|
+
const currentResponseLength = response.length;
|
|
2159
|
+
const responseGotLonger = currentResponseLength > prevResponseLengthRef.current;
|
|
2160
|
+
prevResponseLengthRef.current = currentResponseLength;
|
|
2161
|
+
|
|
2162
|
+
// Only auto-scroll if user hasn't manually scrolled up (or scrollToEnd prop forces it)
|
|
2163
|
+
const shouldAutoScroll = scrollToEnd || !userHasScrolledRef.current;
|
|
2164
|
+
if (shouldAutoScroll && response && responseGotLonger) {
|
|
2139
2165
|
if (window.top !== window.self) {
|
|
2140
2166
|
const responseArea = responseAreaRef.current as any;
|
|
2141
|
-
responseArea
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2167
|
+
if (responseArea) {
|
|
2168
|
+
responseArea.scrollTo({
|
|
2169
|
+
top: responseArea.scrollHeight,
|
|
2170
|
+
behavior: "auto",
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2145
2173
|
} else {
|
|
2146
|
-
|
|
2147
|
-
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
|
|
2174
|
+
bottomRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
|
2148
2175
|
}
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2176
|
+
}
|
|
2177
|
+
}, [response, idle, scrollToEnd]);
|
|
2178
|
+
|
|
2179
|
+
// Detect user scroll intent via wheel event (fires before scroll position changes)
|
|
2180
|
+
useEffect(() => {
|
|
2181
|
+
const responseArea = responseAreaRef.current as any;
|
|
2182
|
+
if (!responseArea) return;
|
|
2183
|
+
|
|
2184
|
+
// Wheel event detects user intent immediately (before scroll position changes)
|
|
2185
|
+
const handleWheel = (e: WheelEvent) => {
|
|
2186
|
+
// Skip if not streaming
|
|
2187
|
+
if (idleRef.current) return;
|
|
2188
|
+
|
|
2189
|
+
// deltaY < 0 means scrolling UP (toward top of document)
|
|
2190
|
+
if (e.deltaY < 0 && !userHasScrolledRef.current) {
|
|
2191
|
+
setUserHasScrolled(true);
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
// Scroll event for hasScroll/isAtBottom UI and detecting return to bottom
|
|
2196
|
+
const handleScroll = () => {
|
|
2197
|
+
// Update hasScroll and isAtBottom states
|
|
2198
|
+
setHasScroll(hasVerticalScrollbar(responseArea));
|
|
2199
|
+
const isScrolledToBottom =
|
|
2200
|
+
responseArea.scrollHeight - responseArea.scrollTop - responseArea.clientHeight < 5;
|
|
2201
|
+
setIsAtBottom(isScrolledToBottom);
|
|
2202
|
+
|
|
2203
|
+
// Re-enable auto-scroll when user scrolls back near bottom (during streaming)
|
|
2204
|
+
if (!idleRef.current && userHasScrolledRef.current) {
|
|
2205
|
+
const isNearBottom =
|
|
2206
|
+
responseArea.scrollHeight - responseArea.scrollTop - responseArea.clientHeight < 50;
|
|
2207
|
+
if (isNearBottom) {
|
|
2208
|
+
setUserHasScrolled(false);
|
|
2209
|
+
}
|
|
2162
2210
|
}
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
handleScroll(); // Initial check
|
|
2214
|
+
responseArea.addEventListener("wheel", handleWheel, { passive: true });
|
|
2215
|
+
responseArea.addEventListener("scroll", handleScroll, { passive: true });
|
|
2216
|
+
return () => {
|
|
2217
|
+
responseArea.removeEventListener("wheel", handleWheel);
|
|
2218
|
+
responseArea.removeEventListener("scroll", handleScroll);
|
|
2219
|
+
};
|
|
2220
|
+
}, []); // Empty deps - uses refs
|
|
2221
|
+
|
|
2222
|
+
// Update hasScroll when history changes
|
|
2223
|
+
useEffect(() => {
|
|
2224
|
+
const responseArea = responseAreaRef.current as any;
|
|
2225
|
+
if (responseArea) {
|
|
2226
|
+
setHasScroll(hasVerticalScrollbar(responseArea));
|
|
2163
2227
|
}
|
|
2164
|
-
}, [
|
|
2228
|
+
}, [history]);
|
|
2165
2229
|
|
|
2166
2230
|
// Use ref to avoid infinite loops from unstable callback references
|
|
2167
2231
|
useEffect(() => {
|
|
@@ -2337,6 +2401,10 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
|
|
|
2337
2401
|
// Clear any previous errors
|
|
2338
2402
|
setError(null);
|
|
2339
2403
|
|
|
2404
|
+
// Reset scroll tracking for new message - enable auto-scroll
|
|
2405
|
+
setUserHasScrolled(false);
|
|
2406
|
+
prevResponseLengthRef.current = 0;
|
|
2407
|
+
|
|
2340
2408
|
// IMPORTANT: Clear the response BEFORE setting new lastKey
|
|
2341
2409
|
// This prevents the old response from being written to the new history entry
|
|
2342
2410
|
// when the history update effect runs
|