@portablecore/chat 0.2.15 → 0.3.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/chat-interface-core.d.ts +5 -0
- package/dist/chat-interface-core.d.ts.map +1 -1
- package/dist/chat-interface-core.js +96 -9
- 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 +16 -2
- package/dist/components/message-list.d.ts.map +1 -1
- package/dist/components/message-list.js +44 -6
- package/dist/components/message-list.js.map +1 -1
- package/dist/hooks/use-chat-scroll.d.ts +10 -1
- package/dist/hooks/use-chat-scroll.d.ts.map +1 -1
- package/dist/hooks/use-chat-scroll.js +99 -32
- 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 +34 -0
- package/dist/hooks/use-virtual-messages.d.ts.map +1 -0
- package/dist/hooks/use-virtual-messages.js +149 -0
- package/dist/hooks/use-virtual-messages.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -6
- package/dist/index.js.map +1 -1
- package/package.json +20 -19
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import type { LiteMessage } from "@portablecore/types/chat";
|
|
2
|
+
import type { Virtualizer } from "@tanstack/react-virtual";
|
|
2
3
|
interface UseChatScrollOptions {
|
|
3
4
|
messages: LiteMessage[];
|
|
4
5
|
streaming: boolean;
|
|
5
6
|
/** When provided, drives scroll-on-thread-change: top for a thread, bottom for all */
|
|
6
7
|
selectedThreadId?: string | null;
|
|
8
|
+
/** Mutable ref to the virtualizer; read at scroll time to avoid hook ordering issues */
|
|
9
|
+
virtualizerRef?: React.RefObject<Virtualizer<HTMLDivElement, Element> | null>;
|
|
7
10
|
}
|
|
8
11
|
interface UseChatScrollReturn {
|
|
9
12
|
scrollRef: React.RefObject<HTMLDivElement>;
|
|
10
13
|
scrollToBottom: (behavior?: ScrollBehavior) => void;
|
|
11
14
|
isNearBottom: () => boolean;
|
|
15
|
+
/** True when user is actively scrolling (600ms debounce for mobile momentum) */
|
|
16
|
+
isUserScrolling: React.MutableRefObject<boolean>;
|
|
17
|
+
/** True when user scrolled away during streaming; resets when streaming ends */
|
|
18
|
+
userTookOverDuringStreaming: React.MutableRefObject<boolean>;
|
|
19
|
+
/** Timestamp of the last programmatic scroll (for guard checks) */
|
|
20
|
+
lastProgrammaticScroll: React.MutableRefObject<number>;
|
|
12
21
|
}
|
|
13
|
-
export declare function useChatScroll({ messages, streaming, selectedThreadId, }: UseChatScrollOptions): UseChatScrollReturn;
|
|
22
|
+
export declare function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef, }: UseChatScrollOptions): UseChatScrollReturn;
|
|
14
23
|
export {};
|
|
15
24
|
//# sourceMappingURL=use-chat-scroll.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat-scroll.d.ts","sourceRoot":"","sources":["../../src/hooks/use-chat-scroll.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-chat-scroll.d.ts","sourceRoot":"","sources":["../../src/hooks/use-chat-scroll.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAE1D,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,SAAS,EAAE,OAAO,CAAA;IAClB,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,wFAAwF;IACxF,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;CAC9E;AAED,UAAU,mBAAmB;IAC3B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IAC1C,cAAc,EAAE,CAAC,QAAQ,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IACnD,YAAY,EAAE,MAAM,OAAO,CAAA;IAC3B,gFAAgF;IAChF,eAAe,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAChD,gFAAgF;IAChF,2BAA2B,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC5D,mEAAmE;IACnE,sBAAsB,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAA;CACvD;AAMD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,cAAc,GACf,EAAE,oBAAoB,GAAG,mBAAmB,CAwJ5C"}
|
|
@@ -3,64 +3,123 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.useChatScroll = useChatScroll;
|
|
5
5
|
/**
|
|
6
|
-
* useChatScroll —
|
|
6
|
+
* useChatScroll — Reliable scroll management for chat.
|
|
7
7
|
*
|
|
8
8
|
* Auto-scrolls to bottom when:
|
|
9
9
|
* - Component mounts (instant)
|
|
10
10
|
* - A new message arrives (smooth, if user was near bottom)
|
|
11
|
-
* - Streaming content grows (
|
|
11
|
+
* - Streaming content grows (RAF-gated, if user was near bottom)
|
|
12
12
|
*
|
|
13
|
-
*
|
|
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
|
|
14
19
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* be ported together with virtualization.
|
|
20
|
+
* Supports an optional virtualizer ref for large message lists.
|
|
21
|
+
* The ref is read at call time (not a dependency) to avoid circular hooks.
|
|
18
22
|
*/
|
|
19
23
|
const react_1 = require("react");
|
|
20
|
-
|
|
24
|
+
const NEAR_BOTTOM_THRESHOLD = 100;
|
|
25
|
+
const PROGRAMMATIC_SCROLL_GUARD_MS = 200;
|
|
26
|
+
const USER_SCROLL_DEBOUNCE_MS = 600;
|
|
27
|
+
function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef, }) {
|
|
21
28
|
const scrollRef = (0, react_1.useRef)(null);
|
|
22
29
|
const wasNearBottom = (0, react_1.useRef)(true);
|
|
23
30
|
const prevThreadIdRef = (0, react_1.useRef)(undefined);
|
|
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);
|
|
24
36
|
const isNearBottom = (0, react_1.useCallback)(() => {
|
|
25
37
|
const el = scrollRef.current;
|
|
26
38
|
if (!el)
|
|
27
39
|
return true;
|
|
28
|
-
|
|
29
|
-
return el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
|
40
|
+
return el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_THRESHOLD;
|
|
30
41
|
}, []);
|
|
31
42
|
const scrollToBottom = (0, react_1.useCallback)((behavior = "smooth") => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
lastProgrammaticScroll.current = Date.now();
|
|
44
|
+
const v = virtualizerRef?.current;
|
|
45
|
+
if (v && messages.length > 0) {
|
|
46
|
+
v.scrollToIndex(messages.length - 1, {
|
|
47
|
+
align: "end",
|
|
48
|
+
behavior,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const el = scrollRef.current;
|
|
53
|
+
if (!el)
|
|
54
|
+
return;
|
|
55
|
+
el.scrollTo({ top: el.scrollHeight, behavior });
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
|
+
[messages.length]);
|
|
60
|
+
// Unified scroll listener: tracks near-bottom state + user scroll guards
|
|
38
61
|
(0, react_1.useEffect)(() => {
|
|
39
62
|
const el = scrollRef.current;
|
|
40
63
|
if (!el)
|
|
41
64
|
return;
|
|
42
|
-
const
|
|
65
|
+
const handleScroll = () => {
|
|
66
|
+
const elapsed = Date.now() - lastProgrammaticScroll.current;
|
|
67
|
+
if (elapsed < PROGRAMMATIC_SCROLL_GUARD_MS)
|
|
68
|
+
return;
|
|
43
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);
|
|
44
83
|
};
|
|
45
|
-
el.addEventListener("scroll",
|
|
46
|
-
return () => el.removeEventListener("scroll",
|
|
47
|
-
}, [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]);
|
|
48
93
|
// Auto-scroll on new messages
|
|
49
94
|
(0, react_1.useEffect)(() => {
|
|
50
|
-
if (wasNearBottom.current) {
|
|
95
|
+
if (wasNearBottom.current && !isUserScrolling.current) {
|
|
51
96
|
scrollToBottom("smooth");
|
|
52
97
|
}
|
|
53
98
|
}, [messages.length, scrollToBottom]);
|
|
54
|
-
//
|
|
99
|
+
// RAF-gated auto-scroll during streaming
|
|
55
100
|
(0, react_1.useEffect)(() => {
|
|
56
|
-
if (!streaming)
|
|
101
|
+
if (!streaming) {
|
|
102
|
+
if (rafRef.current != null) {
|
|
103
|
+
cancelAnimationFrame(rafRef.current);
|
|
104
|
+
rafRef.current = null;
|
|
105
|
+
}
|
|
57
106
|
return;
|
|
58
|
-
|
|
59
|
-
|
|
107
|
+
}
|
|
108
|
+
const tick = () => {
|
|
109
|
+
if (wasNearBottom.current &&
|
|
110
|
+
!userTookOverDuringStreaming.current &&
|
|
111
|
+
!isUserScrolling.current) {
|
|
60
112
|
scrollToBottom("smooth");
|
|
61
113
|
}
|
|
62
|
-
|
|
63
|
-
|
|
114
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
115
|
+
};
|
|
116
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
117
|
+
return () => {
|
|
118
|
+
if (rafRef.current != null) {
|
|
119
|
+
cancelAnimationFrame(rafRef.current);
|
|
120
|
+
rafRef.current = null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
64
123
|
}, [streaming, scrollToBottom]);
|
|
65
124
|
// Instant scroll on mount
|
|
66
125
|
(0, react_1.useEffect)(() => {
|
|
@@ -79,17 +138,25 @@ function useChatScroll({ messages, streaming, selectedThreadId, }) {
|
|
|
79
138
|
return;
|
|
80
139
|
prevThreadIdRef.current = newId;
|
|
81
140
|
requestAnimationFrame(() => {
|
|
82
|
-
const el = scrollRef.current;
|
|
83
|
-
if (!el)
|
|
84
|
-
return;
|
|
85
141
|
if (newId) {
|
|
142
|
+
const el = scrollRef.current;
|
|
143
|
+
if (!el)
|
|
144
|
+
return;
|
|
145
|
+
lastProgrammaticScroll.current = Date.now();
|
|
86
146
|
el.scrollTo({ top: 0, behavior: "instant" });
|
|
87
147
|
}
|
|
88
148
|
else {
|
|
89
|
-
|
|
149
|
+
scrollToBottom("instant");
|
|
90
150
|
}
|
|
91
151
|
});
|
|
92
|
-
}, [selectedThreadId]);
|
|
93
|
-
return {
|
|
152
|
+
}, [selectedThreadId, scrollToBottom]);
|
|
153
|
+
return {
|
|
154
|
+
scrollRef,
|
|
155
|
+
scrollToBottom,
|
|
156
|
+
isNearBottom,
|
|
157
|
+
isUserScrolling,
|
|
158
|
+
userTookOverDuringStreaming,
|
|
159
|
+
lastProgrammaticScroll,
|
|
160
|
+
};
|
|
94
161
|
}
|
|
95
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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Virtualizer } from "@tanstack/react-virtual";
|
|
2
|
+
import type { RenderableMessage } from "../types.js";
|
|
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
|
+
};
|
|
9
|
+
interface UseVirtualMessagesOptions {
|
|
10
|
+
renderableMessages: RenderableMessage[];
|
|
11
|
+
scrollRef: React.RefObject<HTMLDivElement>;
|
|
12
|
+
/** Increment to force re-measurement (focus mode toggle, thread switch) */
|
|
13
|
+
measureVersion?: number;
|
|
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;
|
|
19
|
+
}
|
|
20
|
+
export type VirtualState = {
|
|
21
|
+
active: false;
|
|
22
|
+
} | {
|
|
23
|
+
active: true;
|
|
24
|
+
virtualizer: Virtualizer<HTMLDivElement, Element>;
|
|
25
|
+
virtualItems: ReturnType<Virtualizer<HTMLDivElement, Element>["getVirtualItems"]>;
|
|
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;
|
|
31
|
+
};
|
|
32
|
+
export declare function useVirtualMessages({ renderableMessages, scrollRef, measureVersion, enabled, preMeasuredHeights, getMessageIsExpanded, }: UseVirtualMessagesOptions): VirtualState;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=use-virtual-messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|