@portablecore/chat 0.2.16 → 0.3.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/chat-interface-core.d.ts +5 -0
- package/dist/chat-interface-core.d.ts.map +1 -1
- package/dist/chat-interface-core.js +83 -15
- package/dist/chat-interface-core.js.map +1 -1
- package/dist/components/density-control.d.ts +14 -0
- package/dist/components/density-control.d.ts.map +1 -0
- package/dist/components/density-control.js +18 -0
- package/dist/components/density-control.js.map +1 -0
- package/dist/components/focus-mode-control.d.ts +3 -10
- package/dist/components/focus-mode-control.d.ts.map +1 -1
- package/dist/components/focus-mode-control.js +5 -10
- package/dist/components/focus-mode-control.js.map +1 -1
- package/dist/components/message-bubble.d.ts.map +1 -1
- package/dist/components/message-bubble.js +2 -2
- package/dist/components/message-bubble.js.map +1 -1
- package/dist/components/message-list.d.ts +7 -1
- package/dist/components/message-list.d.ts.map +1 -1
- package/dist/components/message-list.js +25 -16
- package/dist/components/message-list.js.map +1 -1
- package/dist/hooks/use-chat-scroll.d.ts +6 -0
- package/dist/hooks/use-chat-scroll.d.ts.map +1 -1
- package/dist/hooks/use-chat-scroll.js +48 -14
- package/dist/hooks/use-chat-scroll.js.map +1 -1
- package/dist/hooks/use-content-density.d.ts +35 -0
- package/dist/hooks/use-content-density.d.ts.map +1 -0
- package/dist/hooks/use-content-density.js +96 -0
- package/dist/hooks/use-content-density.js.map +1 -0
- package/dist/hooks/use-focus-mode.d.ts +4 -26
- package/dist/hooks/use-focus-mode.d.ts.map +1 -1
- package/dist/hooks/use-focus-mode.js +5 -75
- package/dist/hooks/use-focus-mode.js.map +1 -1
- package/dist/hooks/use-virtual-messages.d.ts +14 -1
- package/dist/hooks/use-virtual-messages.d.ts.map +1 -1
- package/dist/hooks/use-virtual-messages.js +111 -27
- package/dist/hooks/use-virtual-messages.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -10,22 +10,29 @@ exports.useChatScroll = useChatScroll;
|
|
|
10
10
|
* - A new message arrives (smooth, if user was near bottom)
|
|
11
11
|
* - Streaming content grows (RAF-gated, if user was near bottom)
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Scroll guards (ported from legacy ChatInterface):
|
|
14
|
+
* - 600ms user scroll debounce for mobile momentum scrolling
|
|
15
|
+
* - 200ms programmatic scroll guard to prevent auto-scroll events
|
|
16
|
+
* from being misinterpreted as user scrolls
|
|
17
|
+
* - userTookOverDuringStreaming flag that persists until streaming ends,
|
|
18
|
+
* preventing the RAF loop from fighting user intent
|
|
16
19
|
*
|
|
17
20
|
* Supports an optional virtualizer ref for large message lists.
|
|
18
21
|
* The ref is read at call time (not a dependency) to avoid circular hooks.
|
|
19
22
|
*/
|
|
20
23
|
const react_1 = require("react");
|
|
21
24
|
const NEAR_BOTTOM_THRESHOLD = 100;
|
|
22
|
-
const PROGRAMMATIC_SCROLL_GUARD_MS =
|
|
25
|
+
const PROGRAMMATIC_SCROLL_GUARD_MS = 200;
|
|
26
|
+
const USER_SCROLL_DEBOUNCE_MS = 600;
|
|
23
27
|
function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef, }) {
|
|
24
28
|
const scrollRef = (0, react_1.useRef)(null);
|
|
25
29
|
const wasNearBottom = (0, react_1.useRef)(true);
|
|
26
30
|
const prevThreadIdRef = (0, react_1.useRef)(undefined);
|
|
27
|
-
const lastProgrammaticScroll = (0, react_1.useRef)(0);
|
|
28
31
|
const rafRef = (0, react_1.useRef)(null);
|
|
32
|
+
const isUserScrolling = (0, react_1.useRef)(false);
|
|
33
|
+
const userScrollTimeout = (0, react_1.useRef)(null);
|
|
34
|
+
const lastProgrammaticScroll = (0, react_1.useRef)(0);
|
|
35
|
+
const userTookOverDuringStreaming = (0, react_1.useRef)(false);
|
|
29
36
|
const isNearBottom = (0, react_1.useCallback)(() => {
|
|
30
37
|
const el = scrollRef.current;
|
|
31
38
|
if (!el)
|
|
@@ -48,26 +55,44 @@ function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef,
|
|
|
48
55
|
el.scrollTo({ top: el.scrollHeight, behavior });
|
|
49
56
|
}
|
|
50
57
|
},
|
|
51
|
-
// virtualizerRef is a stable ref, not a dependency
|
|
52
58
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
59
|
[messages.length]);
|
|
54
|
-
//
|
|
60
|
+
// Unified scroll listener: tracks near-bottom state + user scroll guards
|
|
55
61
|
(0, react_1.useEffect)(() => {
|
|
56
62
|
const el = scrollRef.current;
|
|
57
63
|
if (!el)
|
|
58
64
|
return;
|
|
59
|
-
const
|
|
65
|
+
const handleScroll = () => {
|
|
60
66
|
const elapsed = Date.now() - lastProgrammaticScroll.current;
|
|
61
67
|
if (elapsed < PROGRAMMATIC_SCROLL_GUARD_MS)
|
|
62
68
|
return;
|
|
63
69
|
wasNearBottom.current = isNearBottom();
|
|
70
|
+
isUserScrolling.current = true;
|
|
71
|
+
if (streaming) {
|
|
72
|
+
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_THRESHOLD;
|
|
73
|
+
if (!nearBottom) {
|
|
74
|
+
userTookOverDuringStreaming.current = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (userScrollTimeout.current) {
|
|
78
|
+
clearTimeout(userScrollTimeout.current);
|
|
79
|
+
}
|
|
80
|
+
userScrollTimeout.current = setTimeout(() => {
|
|
81
|
+
isUserScrolling.current = false;
|
|
82
|
+
}, USER_SCROLL_DEBOUNCE_MS);
|
|
64
83
|
};
|
|
65
|
-
el.addEventListener("scroll",
|
|
66
|
-
return () => el.removeEventListener("scroll",
|
|
67
|
-
}, [isNearBottom]);
|
|
84
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
85
|
+
return () => el.removeEventListener("scroll", handleScroll);
|
|
86
|
+
}, [isNearBottom, streaming]);
|
|
87
|
+
// Reset userTookOver when streaming ends
|
|
88
|
+
(0, react_1.useEffect)(() => {
|
|
89
|
+
if (!streaming) {
|
|
90
|
+
userTookOverDuringStreaming.current = false;
|
|
91
|
+
}
|
|
92
|
+
}, [streaming]);
|
|
68
93
|
// Auto-scroll on new messages
|
|
69
94
|
(0, react_1.useEffect)(() => {
|
|
70
|
-
if (wasNearBottom.current) {
|
|
95
|
+
if (wasNearBottom.current && !isUserScrolling.current) {
|
|
71
96
|
scrollToBottom("smooth");
|
|
72
97
|
}
|
|
73
98
|
}, [messages.length, scrollToBottom]);
|
|
@@ -81,7 +106,9 @@ function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef,
|
|
|
81
106
|
return;
|
|
82
107
|
}
|
|
83
108
|
const tick = () => {
|
|
84
|
-
if (wasNearBottom.current
|
|
109
|
+
if (wasNearBottom.current &&
|
|
110
|
+
!userTookOverDuringStreaming.current &&
|
|
111
|
+
!isUserScrolling.current) {
|
|
85
112
|
scrollToBottom("smooth");
|
|
86
113
|
}
|
|
87
114
|
rafRef.current = requestAnimationFrame(tick);
|
|
@@ -123,6 +150,13 @@ function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef,
|
|
|
123
150
|
}
|
|
124
151
|
});
|
|
125
152
|
}, [selectedThreadId, scrollToBottom]);
|
|
126
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
scrollRef,
|
|
155
|
+
scrollToBottom,
|
|
156
|
+
isNearBottom,
|
|
157
|
+
isUserScrolling,
|
|
158
|
+
userTookOverDuringStreaming,
|
|
159
|
+
lastProgrammaticScroll,
|
|
160
|
+
};
|
|
127
161
|
}
|
|
128
162
|
//# sourceMappingURL=use-chat-scroll.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat-scroll.js","sourceRoot":"","sources":["../../src/hooks/use-chat-scroll.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;;;
|
|
1
|
+
{"version":3,"file":"use-chat-scroll.js","sourceRoot":"","sources":["../../src/hooks/use-chat-scroll.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LiteMessage, MessageViewMode } from "@portablecore/types/chat";
|
|
2
|
+
interface UseContentDensityOptions {
|
|
3
|
+
messages: LiteMessage[];
|
|
4
|
+
initialViewMode?: MessageViewMode;
|
|
5
|
+
currentUserId: string;
|
|
6
|
+
onViewModeChange?: (mode: MessageViewMode) => void;
|
|
7
|
+
/** How many recent messages stay full in Smart mode (default 10) */
|
|
8
|
+
recentMessageCount?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ContentDensityState {
|
|
11
|
+
viewMode: MessageViewMode;
|
|
12
|
+
setViewMode: (mode: MessageViewMode) => void;
|
|
13
|
+
/** True when the message should render shortContent instead of content */
|
|
14
|
+
shouldShowShort: (messageId: string) => boolean;
|
|
15
|
+
/** Toggle a single message between expanded/collapsed */
|
|
16
|
+
toggleExpanded: (messageId: string, currentlyShowingFull: boolean) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Auto-determine the default view mode based on chat characteristics.
|
|
20
|
+
* Group chats and long chats default to Smart; short chats default to Full.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getDefaultViewMode(chatLength: number, chatType?: string): MessageViewMode;
|
|
23
|
+
/**
|
|
24
|
+
* Manages Content Density state: view mode, per-message expansion overrides,
|
|
25
|
+
* and the core logic for deciding whether to show short or full content.
|
|
26
|
+
*
|
|
27
|
+
* Mode behaviors:
|
|
28
|
+
* - "full": all messages expanded
|
|
29
|
+
* - "brief": all messages with shortContent are compressed
|
|
30
|
+
* - "smart": recency-weighted auto-selection. Recent N messages stay full,
|
|
31
|
+
* older messages that the current user prompted stay full, all others brief.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useContentDensity({ messages, initialViewMode, currentUserId, onViewModeChange, recentMessageCount, }: UseContentDensityOptions): ContentDensityState;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=use-content-density.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-content-density.d.ts","sourceRoot":"","sources":["../../src/hooks/use-content-density.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAE5E,UAAU,wBAAwB;IAChC,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IAClD,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,eAAe,CAAA;IACzB,WAAW,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IAC5C,0EAA0E;IAC1E,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IAC/C,yDAAyD;IACzD,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,KAAK,IAAI,CAAA;CAC3E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,eAAe,CAIjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,eAAyB,EACzB,aAAa,EACb,gBAAgB,EAChB,kBAAuB,GACxB,EAAE,wBAAwB,GAAG,mBAAmB,CAkFhD"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.getDefaultViewMode = getDefaultViewMode;
|
|
5
|
+
exports.useContentDensity = useContentDensity;
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
/**
|
|
8
|
+
* Auto-determine the default view mode based on chat characteristics.
|
|
9
|
+
* Group chats and long chats default to Smart; short chats default to Full.
|
|
10
|
+
*/
|
|
11
|
+
function getDefaultViewMode(chatLength, chatType) {
|
|
12
|
+
if (chatType === "group")
|
|
13
|
+
return "smart";
|
|
14
|
+
if (chatLength > 50)
|
|
15
|
+
return "smart";
|
|
16
|
+
return "full";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Manages Content Density state: view mode, per-message expansion overrides,
|
|
20
|
+
* and the core logic for deciding whether to show short or full content.
|
|
21
|
+
*
|
|
22
|
+
* Mode behaviors:
|
|
23
|
+
* - "full": all messages expanded
|
|
24
|
+
* - "brief": all messages with shortContent are compressed
|
|
25
|
+
* - "smart": recency-weighted auto-selection. Recent N messages stay full,
|
|
26
|
+
* older messages that the current user prompted stay full, all others brief.
|
|
27
|
+
*/
|
|
28
|
+
function useContentDensity({ messages, initialViewMode = "smart", currentUserId, onViewModeChange, recentMessageCount = 10, }) {
|
|
29
|
+
const [viewMode, setViewModeInternal] = (0, react_1.useState)(initialViewMode);
|
|
30
|
+
const [overrides, setOverrides] = (0, react_1.useState)(() => new Map());
|
|
31
|
+
const initialViewModeRef = (0, react_1.useRef)(initialViewMode);
|
|
32
|
+
const setViewMode = (0, react_1.useCallback)((mode) => {
|
|
33
|
+
setViewModeInternal(mode);
|
|
34
|
+
setOverrides(new Map());
|
|
35
|
+
onViewModeChange?.(mode);
|
|
36
|
+
}, [onViewModeChange]);
|
|
37
|
+
(0, react_1.useEffect)(() => {
|
|
38
|
+
if (viewMode !== initialViewModeRef.current) {
|
|
39
|
+
onViewModeChange?.(viewMode);
|
|
40
|
+
}
|
|
41
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
42
|
+
const messageIndex = (0, react_1.useMemo)(() => {
|
|
43
|
+
const map = new Map();
|
|
44
|
+
for (let i = 0; i < messages.length; i++) {
|
|
45
|
+
map.set(messages[i].id, i);
|
|
46
|
+
}
|
|
47
|
+
return map;
|
|
48
|
+
}, [messages]);
|
|
49
|
+
const shouldShowShort = (0, react_1.useCallback)((messageId) => {
|
|
50
|
+
const idx = messageIndex.get(messageId);
|
|
51
|
+
if (idx === undefined)
|
|
52
|
+
return false;
|
|
53
|
+
const msg = messages[idx];
|
|
54
|
+
if (!msg || msg.role !== "assistant" || !msg.shortContent)
|
|
55
|
+
return false;
|
|
56
|
+
const override = overrides.get(messageId);
|
|
57
|
+
if (override === "expanded")
|
|
58
|
+
return false;
|
|
59
|
+
if (override === "collapsed")
|
|
60
|
+
return true;
|
|
61
|
+
if (viewMode === "full")
|
|
62
|
+
return false;
|
|
63
|
+
if (viewMode === "brief")
|
|
64
|
+
return true;
|
|
65
|
+
// Smart mode: recency + authorship
|
|
66
|
+
if (viewMode === "smart") {
|
|
67
|
+
const totalMessages = messages.length;
|
|
68
|
+
const isRecent = totalMessages - idx <= recentMessageCount;
|
|
69
|
+
if (isRecent)
|
|
70
|
+
return false;
|
|
71
|
+
// Check if the current user prompted this response
|
|
72
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
73
|
+
const prevMsg = messages[i];
|
|
74
|
+
if (prevMsg && prevMsg.role === "user") {
|
|
75
|
+
const promptingUserId = prevMsg.senderUserId || currentUserId;
|
|
76
|
+
return promptingUserId !== currentUserId;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}, [messages, messageIndex, overrides, viewMode, currentUserId, recentMessageCount]);
|
|
82
|
+
const toggleExpanded = (0, react_1.useCallback)((messageId, currentlyShowingFull) => {
|
|
83
|
+
setOverrides((prev) => {
|
|
84
|
+
const next = new Map(prev);
|
|
85
|
+
next.set(messageId, currentlyShowingFull ? "collapsed" : "expanded");
|
|
86
|
+
return next;
|
|
87
|
+
});
|
|
88
|
+
}, []);
|
|
89
|
+
return {
|
|
90
|
+
viewMode,
|
|
91
|
+
setViewMode,
|
|
92
|
+
shouldShowShort,
|
|
93
|
+
toggleExpanded,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=use-content-density.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-content-density.js","sourceRoot":"","sources":["../../src/hooks/use-content-density.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,29 +1,7 @@
|
|
|
1
|
-
import type { LiteMessage, MessageViewMode } from "@portablecore/types/chat";
|
|
2
|
-
interface UseFocusModeOptions {
|
|
3
|
-
messages: LiteMessage[];
|
|
4
|
-
enabled: boolean;
|
|
5
|
-
initialViewMode?: MessageViewMode;
|
|
6
|
-
currentUserId: string;
|
|
7
|
-
onViewModeChange?: (mode: MessageViewMode) => void;
|
|
8
|
-
}
|
|
9
|
-
export interface FocusModeState {
|
|
10
|
-
viewMode: MessageViewMode;
|
|
11
|
-
setViewMode: (mode: MessageViewMode) => void;
|
|
12
|
-
/** True when the message should render shortContent instead of content */
|
|
13
|
-
shouldShowShort: (messageId: string) => boolean;
|
|
14
|
-
/** Toggle a single message between expanded/collapsed */
|
|
15
|
-
toggleExpanded: (messageId: string, currentlyShowingFull: boolean) => void;
|
|
16
|
-
enabled: boolean;
|
|
17
|
-
}
|
|
18
1
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Mirrors the legacy ChatInterface focus mode exactly:
|
|
23
|
-
* - "long": all messages expanded
|
|
24
|
-
* - "short": all messages with shortContent are compressed
|
|
25
|
-
* - "focus": messages you prompted are expanded, others' compressed
|
|
2
|
+
* @deprecated Use useContentDensity from ./use-content-density.ts instead.
|
|
3
|
+
* This file re-exports the new hook for backward compatibility.
|
|
26
4
|
*/
|
|
27
|
-
export
|
|
28
|
-
export {};
|
|
5
|
+
export { useContentDensity as useFocusMode } from "./use-content-density.js";
|
|
6
|
+
export type { ContentDensityState as FocusModeState } from "./use-content-density.js";
|
|
29
7
|
//# sourceMappingURL=use-focus-mode.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-focus-mode.d.ts","sourceRoot":"","sources":["../../src/hooks/use-focus-mode.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-focus-mode.d.ts","sourceRoot":"","sources":["../../src/hooks/use-focus-mode.ts"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,IAAI,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5E,YAAY,EAAE,mBAAmB,IAAI,cAAc,EAAE,MAAM,0BAA0B,CAAA"}
|
|
@@ -1,81 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.useFocusMode =
|
|
5
|
-
const react_1 = require("react");
|
|
4
|
+
exports.useFocusMode = void 0;
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Mirrors the legacy ChatInterface focus mode exactly:
|
|
11
|
-
* - "long": all messages expanded
|
|
12
|
-
* - "short": all messages with shortContent are compressed
|
|
13
|
-
* - "focus": messages you prompted are expanded, others' compressed
|
|
6
|
+
* @deprecated Use useContentDensity from ./use-content-density.ts instead.
|
|
7
|
+
* This file re-exports the new hook for backward compatibility.
|
|
14
8
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const [overrides, setOverrides] = (0, react_1.useState)(() => new Map());
|
|
18
|
-
const initialViewModeRef = (0, react_1.useRef)(initialViewMode);
|
|
19
|
-
const setViewMode = (0, react_1.useCallback)((mode) => {
|
|
20
|
-
setViewModeInternal(mode);
|
|
21
|
-
setOverrides(new Map());
|
|
22
|
-
onViewModeChange?.(mode);
|
|
23
|
-
}, [onViewModeChange]);
|
|
24
|
-
(0, react_1.useEffect)(() => {
|
|
25
|
-
if (viewMode !== initialViewModeRef.current) {
|
|
26
|
-
onViewModeChange?.(viewMode);
|
|
27
|
-
}
|
|
28
|
-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
29
|
-
const messageIndex = (0, react_1.useMemo)(() => {
|
|
30
|
-
const map = new Map();
|
|
31
|
-
for (let i = 0; i < messages.length; i++) {
|
|
32
|
-
map.set(messages[i].id, i);
|
|
33
|
-
}
|
|
34
|
-
return map;
|
|
35
|
-
}, [messages]);
|
|
36
|
-
const shouldShowShort = (0, react_1.useCallback)((messageId) => {
|
|
37
|
-
if (!enabled)
|
|
38
|
-
return false;
|
|
39
|
-
const idx = messageIndex.get(messageId);
|
|
40
|
-
if (idx === undefined)
|
|
41
|
-
return false;
|
|
42
|
-
const msg = messages[idx];
|
|
43
|
-
if (!msg || msg.role !== "assistant" || !msg.shortContent)
|
|
44
|
-
return false;
|
|
45
|
-
const override = overrides.get(messageId);
|
|
46
|
-
if (override === "expanded")
|
|
47
|
-
return false;
|
|
48
|
-
if (override === "collapsed")
|
|
49
|
-
return true;
|
|
50
|
-
if (viewMode === "long")
|
|
51
|
-
return false;
|
|
52
|
-
if (viewMode === "short")
|
|
53
|
-
return true;
|
|
54
|
-
// Focus mode: expand if current user prompted this response
|
|
55
|
-
if (viewMode === "focus") {
|
|
56
|
-
for (let i = idx - 1; i >= 0; i--) {
|
|
57
|
-
const prevMsg = messages[i];
|
|
58
|
-
if (prevMsg && prevMsg.role === "user") {
|
|
59
|
-
const promptingUserId = prevMsg.senderUserId || currentUserId;
|
|
60
|
-
return promptingUserId !== currentUserId;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return false;
|
|
65
|
-
}, [enabled, messages, messageIndex, overrides, viewMode, currentUserId]);
|
|
66
|
-
const toggleExpanded = (0, react_1.useCallback)((messageId, currentlyShowingFull) => {
|
|
67
|
-
setOverrides((prev) => {
|
|
68
|
-
const next = new Map(prev);
|
|
69
|
-
next.set(messageId, currentlyShowingFull ? "collapsed" : "expanded");
|
|
70
|
-
return next;
|
|
71
|
-
});
|
|
72
|
-
}, []);
|
|
73
|
-
return {
|
|
74
|
-
viewMode,
|
|
75
|
-
setViewMode,
|
|
76
|
-
shouldShowShort,
|
|
77
|
-
toggleExpanded,
|
|
78
|
-
enabled,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
9
|
+
var use_content_density_js_1 = require("./use-content-density.js");
|
|
10
|
+
Object.defineProperty(exports, "useFocusMode", { enumerable: true, get: function () { return use_content_density_js_1.useContentDensity; } });
|
|
81
11
|
//# sourceMappingURL=use-focus-mode.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-focus-mode.js","sourceRoot":"","sources":["../../src/hooks/use-focus-mode.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;;;
|
|
1
|
+
{"version":3,"file":"use-focus-mode.js","sourceRoot":"","sources":["../../src/hooks/use-focus-mode.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;;;;AAEZ;;;GAGG;AAEH,mEAA4E;AAAnE,sHAAA,iBAAiB,OAAgB"}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { type Virtualizer } from "@tanstack/react-virtual";
|
|
2
2
|
import type { RenderableMessage } from "../types.js";
|
|
3
3
|
export declare const VIRTUALIZATION_THRESHOLD = 100;
|
|
4
|
+
export declare const PRE_MEASURE_THRESHOLD = 100;
|
|
5
|
+
export type MessageHeights = number | {
|
|
6
|
+
short: number;
|
|
7
|
+
long: number;
|
|
8
|
+
};
|
|
4
9
|
interface UseVirtualMessagesOptions {
|
|
5
10
|
renderableMessages: RenderableMessage[];
|
|
6
11
|
scrollRef: React.RefObject<HTMLDivElement>;
|
|
7
12
|
/** Increment to force re-measurement (focus mode toggle, thread switch) */
|
|
8
13
|
measureVersion?: number;
|
|
9
14
|
enabled?: boolean;
|
|
15
|
+
/** Pre-measured heights from hidden containers (optional) */
|
|
16
|
+
preMeasuredHeights?: Map<string, MessageHeights>;
|
|
17
|
+
/** Returns true if message is showing full content (for short/long estimation) */
|
|
18
|
+
getMessageIsExpanded?: (messageId: string, index: number) => boolean;
|
|
10
19
|
}
|
|
11
20
|
export type VirtualState = {
|
|
12
21
|
active: false;
|
|
@@ -15,7 +24,11 @@ export type VirtualState = {
|
|
|
15
24
|
virtualizer: Virtualizer<HTMLDivElement, Element>;
|
|
16
25
|
virtualItems: ReturnType<Virtualizer<HTMLDivElement, Element>["getVirtualItems"]>;
|
|
17
26
|
totalSize: number;
|
|
27
|
+
/** True once layout has stabilized after initial render */
|
|
28
|
+
isSettled: boolean;
|
|
29
|
+
/** True during mode transition (virtualizer count briefly set to 0) */
|
|
30
|
+
isResetting: boolean;
|
|
18
31
|
};
|
|
19
|
-
export declare function useVirtualMessages({ renderableMessages, scrollRef, measureVersion, enabled, }: UseVirtualMessagesOptions): VirtualState;
|
|
32
|
+
export declare function useVirtualMessages({ renderableMessages, scrollRef, measureVersion, enabled, preMeasuredHeights, getMessageIsExpanded, }: UseVirtualMessagesOptions): VirtualState;
|
|
20
33
|
export {};
|
|
21
34
|
//# sourceMappingURL=use-virtual-messages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-virtual-messages.d.ts","sourceRoot":"","sources":["../../src/hooks/use-virtual-messages.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-virtual-messages.d.ts","sourceRoot":"","sources":["../../src/hooks/use-virtual-messages.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAC3C,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAErE,UAAU,yBAAyB;IACjC,kBAAkB,EAAE,iBAAiB,EAAE,CAAA;IACvC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IAC1C,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,6DAA6D;IAC7D,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAChD,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;CACrE;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IACE,MAAM,EAAE,IAAI,CAAA;IACZ,WAAW,EAAE,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,CAAA;IACjD,YAAY,EAAE,UAAU,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAA;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAA;IAClB,uEAAuE;IACvE,WAAW,EAAE,OAAO,CAAA;CACrB,CAAA;AAuCL,wBAAgB,kBAAkB,CAAC,EACjC,kBAAkB,EAClB,SAAS,EACT,cAAkB,EAClB,OAAc,EACd,kBAAkB,EAClB,oBAAoB,GACrB,EAAE,yBAAyB,GAAG,YAAY,CAkH1C"}
|
|
@@ -1,57 +1,139 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.VIRTUALIZATION_THRESHOLD = void 0;
|
|
4
|
+
exports.PRE_MEASURE_THRESHOLD = exports.VIRTUALIZATION_THRESHOLD = void 0;
|
|
5
5
|
exports.useVirtualMessages = useVirtualMessages;
|
|
6
|
+
/**
|
|
7
|
+
* useVirtualMessages — Battle-tested virtualizer for large chat histories.
|
|
8
|
+
*
|
|
9
|
+
* Ported from the legacy ChatInterface's tanstack/react-virtual integration,
|
|
10
|
+
* which included workarounds for variable-height messages, mode transitions,
|
|
11
|
+
* and mobile scrolling. Gated behind config.features.jsVirtualization.
|
|
12
|
+
*
|
|
13
|
+
* Key patterns ported from legacy:
|
|
14
|
+
* - Overscan 100 (pre-render many items above/below viewport)
|
|
15
|
+
* - Content-length-based height estimation with short/long awareness
|
|
16
|
+
* - Mode version in getItemKey for cache invalidation on view mode changes
|
|
17
|
+
* - isResetting state (count-to-0) for mode transitions
|
|
18
|
+
* - Settlement polling (50ms, 4-stable, 40-max) with 2.5s fallback
|
|
19
|
+
* - Pre-measured heights map integration for accurate initial positioning
|
|
20
|
+
*/
|
|
6
21
|
const react_1 = require("react");
|
|
7
22
|
const react_virtual_1 = require("@tanstack/react-virtual");
|
|
8
23
|
exports.VIRTUALIZATION_THRESHOLD = 100;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
exports.PRE_MEASURE_THRESHOLD = 100;
|
|
25
|
+
function estimateMessageHeight(rm, preMeasuredHeights, isExpanded) {
|
|
26
|
+
const msg = rm.message;
|
|
27
|
+
const PADDING = 24;
|
|
28
|
+
if (preMeasuredHeights) {
|
|
29
|
+
const cached = preMeasuredHeights.get(msg.id);
|
|
30
|
+
if (cached) {
|
|
31
|
+
if (typeof cached === "object") {
|
|
32
|
+
return isExpanded ? cached.long : cached.short;
|
|
33
|
+
}
|
|
34
|
+
return cached;
|
|
35
|
+
}
|
|
20
36
|
}
|
|
21
|
-
|
|
37
|
+
let base = 0;
|
|
38
|
+
if (rm.showDateSeparator)
|
|
22
39
|
base += 48;
|
|
40
|
+
if (msg.role === "user") {
|
|
41
|
+
const hasDocuments = msg.metadata?.hasDocuments;
|
|
42
|
+
return base + (hasDocuments ? 150 : 80) + PADDING;
|
|
23
43
|
}
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
44
|
+
const relevantContent = !isExpanded && msg.shortContent ? msg.shortContent : msg.content;
|
|
45
|
+
const contentLength = relevantContent?.length || 0;
|
|
46
|
+
if (contentLength < 200)
|
|
47
|
+
return base + 100 + PADDING;
|
|
48
|
+
if (contentLength < 500)
|
|
49
|
+
return base + 150 + PADDING;
|
|
50
|
+
if (contentLength < 1000)
|
|
51
|
+
return base + 250 + PADDING;
|
|
52
|
+
if (contentLength < 2000)
|
|
53
|
+
return base + 400 + PADDING;
|
|
54
|
+
return base + 600 + PADDING;
|
|
27
55
|
}
|
|
28
|
-
function useVirtualMessages({ renderableMessages, scrollRef, measureVersion = 0, enabled = true, }) {
|
|
56
|
+
function useVirtualMessages({ renderableMessages, scrollRef, measureVersion = 0, enabled = true, preMeasuredHeights, getMessageIsExpanded, }) {
|
|
29
57
|
const count = renderableMessages.length;
|
|
30
58
|
const shouldVirtualize = enabled && count >= exports.VIRTUALIZATION_THRESHOLD;
|
|
31
59
|
const messagesRef = (0, react_1.useRef)(renderableMessages);
|
|
32
60
|
messagesRef.current = renderableMessages;
|
|
61
|
+
const preMeasuredRef = (0, react_1.useRef)(preMeasuredHeights);
|
|
62
|
+
preMeasuredRef.current = preMeasuredHeights;
|
|
63
|
+
const getExpandedRef = (0, react_1.useRef)(getMessageIsExpanded);
|
|
64
|
+
getExpandedRef.current = getMessageIsExpanded;
|
|
65
|
+
const [modeVersion, setModeVersion] = (0, react_1.useState)(0);
|
|
66
|
+
const [isResetting, setIsResetting] = (0, react_1.useState)(false);
|
|
67
|
+
const [isSettled, setIsSettled] = (0, react_1.useState)(!shouldVirtualize);
|
|
68
|
+
const prevMeasureVersion = (0, react_1.useRef)(measureVersion);
|
|
69
|
+
(0, react_1.useEffect)(() => {
|
|
70
|
+
if (prevMeasureVersion.current !== measureVersion && shouldVirtualize) {
|
|
71
|
+
prevMeasureVersion.current = measureVersion;
|
|
72
|
+
setIsResetting(true);
|
|
73
|
+
setIsSettled(false);
|
|
74
|
+
setModeVersion((v) => v + 1);
|
|
75
|
+
requestAnimationFrame(() => {
|
|
76
|
+
setIsResetting(false);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}, [measureVersion, shouldVirtualize]);
|
|
33
80
|
const virtualizer = (0, react_virtual_1.useVirtualizer)({
|
|
34
|
-
count: shouldVirtualize ? count : 0,
|
|
81
|
+
count: isResetting ? 0 : shouldVirtualize ? count : 0,
|
|
35
82
|
getScrollElement: () => scrollRef.current,
|
|
36
83
|
estimateSize: (0, react_1.useCallback)((index) => {
|
|
37
84
|
const rm = messagesRef.current[index];
|
|
38
85
|
if (!rm)
|
|
39
86
|
return 100;
|
|
40
|
-
|
|
87
|
+
const isExpanded = getExpandedRef.current?.(rm.message.id, index) ?? true;
|
|
88
|
+
return estimateMessageHeight(rm, preMeasuredRef.current, isExpanded);
|
|
41
89
|
}, []),
|
|
42
90
|
getItemKey: (0, react_1.useCallback)((index) => {
|
|
43
91
|
const rm = messagesRef.current[index];
|
|
44
|
-
return rm?.message.id ?? index
|
|
45
|
-
}, []),
|
|
46
|
-
overscan:
|
|
92
|
+
return `${rm?.message.id ?? index}-v${modeVersion}`;
|
|
93
|
+
}, [modeVersion]),
|
|
94
|
+
overscan: 100,
|
|
47
95
|
});
|
|
48
|
-
|
|
96
|
+
// Settlement polling: watch for scrollHeight stability after initial render.
|
|
97
|
+
// 50ms polling, need 4 consecutive stable checks (~200ms), give up after 40 (~2s).
|
|
49
98
|
(0, react_1.useEffect)(() => {
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
99
|
+
if (!shouldVirtualize || isResetting || isSettled)
|
|
100
|
+
return;
|
|
101
|
+
const container = scrollRef.current;
|
|
102
|
+
if (!container) {
|
|
103
|
+
setIsSettled(true);
|
|
104
|
+
return;
|
|
53
105
|
}
|
|
54
|
-
|
|
106
|
+
let lastScrollHeight = container.scrollHeight;
|
|
107
|
+
let stableCount = 0;
|
|
108
|
+
const STABLE_THRESHOLD = 4;
|
|
109
|
+
const MAX_CHECKS = 40;
|
|
110
|
+
let checkCount = 0;
|
|
111
|
+
let pollTimer;
|
|
112
|
+
const checkSettlement = () => {
|
|
113
|
+
checkCount++;
|
|
114
|
+
const currentScrollHeight = container.scrollHeight;
|
|
115
|
+
if (currentScrollHeight !== lastScrollHeight) {
|
|
116
|
+
lastScrollHeight = currentScrollHeight;
|
|
117
|
+
stableCount = 0;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
stableCount++;
|
|
121
|
+
}
|
|
122
|
+
if (stableCount >= STABLE_THRESHOLD || checkCount >= MAX_CHECKS) {
|
|
123
|
+
setIsSettled(true);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
pollTimer = setTimeout(checkSettlement, 50);
|
|
127
|
+
};
|
|
128
|
+
pollTimer = setTimeout(checkSettlement, 50);
|
|
129
|
+
const fallbackTimer = setTimeout(() => {
|
|
130
|
+
setIsSettled(true);
|
|
131
|
+
}, 2500);
|
|
132
|
+
return () => {
|
|
133
|
+
clearTimeout(pollTimer);
|
|
134
|
+
clearTimeout(fallbackTimer);
|
|
135
|
+
};
|
|
136
|
+
}, [shouldVirtualize, isResetting, isSettled, scrollRef]);
|
|
55
137
|
if (!shouldVirtualize) {
|
|
56
138
|
return { active: false };
|
|
57
139
|
}
|
|
@@ -60,6 +142,8 @@ function useVirtualMessages({ renderableMessages, scrollRef, measureVersion = 0,
|
|
|
60
142
|
virtualizer,
|
|
61
143
|
virtualItems: virtualizer.getVirtualItems(),
|
|
62
144
|
totalSize: virtualizer.getTotalSize(),
|
|
145
|
+
isSettled,
|
|
146
|
+
isResetting,
|
|
63
147
|
};
|
|
64
148
|
}
|
|
65
149
|
//# sourceMappingURL=use-virtual-messages.js.map
|