@townco/ui 0.1.67 → 0.1.69
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/core/hooks/use-chat-messages.d.ts +5 -0
- package/dist/core/hooks/use-tool-calls.d.ts +5 -0
- package/dist/core/hooks/use-tool-calls.js +2 -1
- package/dist/core/schemas/chat.d.ts +10 -0
- package/dist/core/schemas/tool-call.d.ts +96 -0
- package/dist/core/schemas/tool-call.js +12 -0
- package/dist/core/utils/tool-call-state.d.ts +30 -0
- package/dist/core/utils/tool-call-state.js +73 -0
- package/dist/core/utils/tool-summary.d.ts +13 -0
- package/dist/core/utils/tool-summary.js +172 -0
- package/dist/core/utils/tool-verbiage.d.ts +28 -0
- package/dist/core/utils/tool-verbiage.js +185 -0
- package/dist/gui/components/AppSidebar.d.ts +22 -0
- package/dist/gui/components/AppSidebar.js +22 -0
- package/dist/gui/components/ChatLayout.d.ts +5 -0
- package/dist/gui/components/ChatLayout.js +239 -132
- package/dist/gui/components/ChatView.js +42 -118
- package/dist/gui/components/MessageContent.js +199 -49
- package/dist/gui/components/SessionHistory.d.ts +10 -0
- package/dist/gui/components/SessionHistory.js +101 -0
- package/dist/gui/components/SessionHistoryItem.d.ts +11 -0
- package/dist/gui/components/SessionHistoryItem.js +24 -0
- package/dist/gui/components/Sheet.d.ts +25 -0
- package/dist/gui/components/Sheet.js +36 -0
- package/dist/gui/components/Sidebar.d.ts +65 -0
- package/dist/gui/components/Sidebar.js +231 -0
- package/dist/gui/components/SidebarToggle.d.ts +3 -0
- package/dist/gui/components/SidebarToggle.js +9 -0
- package/dist/gui/components/SubAgentDetails.d.ts +13 -6
- package/dist/gui/components/SubAgentDetails.js +29 -14
- package/dist/gui/components/ToolCallList.js +3 -3
- package/dist/gui/components/ToolOperation.d.ts +11 -0
- package/dist/gui/components/ToolOperation.js +289 -0
- package/dist/gui/components/WorkProgress.d.ts +20 -0
- package/dist/gui/components/WorkProgress.js +79 -0
- package/dist/gui/components/index.d.ts +8 -1
- package/dist/gui/components/index.js +9 -1
- package/dist/gui/hooks/index.d.ts +1 -0
- package/dist/gui/hooks/index.js +1 -0
- package/dist/gui/hooks/use-mobile.d.ts +1 -0
- package/dist/gui/hooks/use-mobile.js +15 -0
- package/dist/gui/index.d.ts +1 -0
- package/dist/gui/index.js +2 -0
- package/dist/gui/lib/motion.d.ts +55 -0
- package/dist/gui/lib/motion.js +217 -0
- package/dist/sdk/schemas/session.d.ts +102 -6
- package/dist/sdk/transports/http.js +105 -37
- package/dist/sdk/transports/types.d.ts +5 -0
- package/package.json +8 -7
- package/src/styles/global.css +128 -1
- package/dist/gui/components/InvokingGroup.d.ts +0 -9
- package/dist/gui/components/InvokingGroup.js +0 -16
- package/dist/gui/components/ToolCall.d.ts +0 -8
- package/dist/gui/components/ToolCall.js +0 -226
- package/dist/gui/components/ToolCallGroup.d.ts +0 -8
- package/dist/gui/components/ToolCallGroup.js +0 -29
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AnimatePresence, motion, useMotionValue } from "framer-motion";
|
|
2
3
|
import { ArrowDown } from "lucide-react";
|
|
3
4
|
import * as React from "react";
|
|
5
|
+
import { motionEasing } from "../lib/motion.js";
|
|
4
6
|
import { cn } from "../lib/utils.js";
|
|
5
|
-
import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "./resizable.js";
|
|
6
7
|
import { Toaster } from "./Sonner.js";
|
|
7
8
|
const ChatLayoutContext = React.createContext(undefined);
|
|
8
9
|
const useChatLayoutContext = () => {
|
|
@@ -16,157 +17,232 @@ const ChatLayoutRoot = React.forwardRef(({ defaultSidebarOpen = false, defaultPa
|
|
|
16
17
|
const [sidebarOpen, setSidebarOpen] = React.useState(defaultSidebarOpen);
|
|
17
18
|
const [panelSize, setPanelSize] = React.useState(defaultPanelSize);
|
|
18
19
|
const [activeTab, setActiveTab] = React.useState(defaultActiveTab);
|
|
20
|
+
// Right panel state (derived from panelSize for backwards compatibility)
|
|
21
|
+
const [panelOpen, setPanelOpen] = React.useState(defaultPanelSize !== "hidden");
|
|
22
|
+
// Helper to toggle the right panel
|
|
23
|
+
const togglePanel = React.useCallback(() => {
|
|
24
|
+
setPanelSize((size) => (size === "hidden" ? "large" : "hidden"));
|
|
25
|
+
setPanelOpen((open) => !open);
|
|
26
|
+
}, []);
|
|
27
|
+
// Sync panelOpen with panelSize
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
setPanelOpen(panelSize !== "hidden");
|
|
30
|
+
}, [panelSize]);
|
|
19
31
|
return (_jsx(ChatLayoutContext.Provider, { value: {
|
|
20
32
|
sidebarOpen,
|
|
21
33
|
setSidebarOpen,
|
|
22
34
|
panelSize,
|
|
23
|
-
setPanelSize
|
|
35
|
+
setPanelSize: (size) => {
|
|
36
|
+
setPanelSize(size);
|
|
37
|
+
setPanelOpen(size !== "hidden");
|
|
38
|
+
},
|
|
24
39
|
activeTab,
|
|
25
40
|
setActiveTab,
|
|
26
|
-
|
|
41
|
+
panelOpen,
|
|
42
|
+
setPanelOpen,
|
|
43
|
+
togglePanel,
|
|
44
|
+
}, children: _jsx("div", { ref: ref, "data-panel-state": panelOpen ? "expanded" : "collapsed", className: cn("flex h-screen flex-row bg-background text-foreground overflow-hidden", className), ...props, children: children }) }));
|
|
27
45
|
});
|
|
28
46
|
ChatLayoutRoot.displayName = "ChatLayout.Root";
|
|
29
47
|
const ChatLayoutHeader = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
30
48
|
return (_jsx("div", { ref: ref, className: cn("relative z-10 border-b border-border bg-card shrink-0", className), ...props, children: children }));
|
|
31
49
|
});
|
|
32
50
|
ChatLayoutHeader.displayName = "ChatLayout.Header";
|
|
33
|
-
const ChatLayoutMain = React.forwardRef(({ className, children
|
|
34
|
-
return (_jsx(
|
|
51
|
+
const ChatLayoutMain = React.forwardRef(({ className, children }, ref) => {
|
|
52
|
+
return (_jsx(motion.div, { ref: ref, layout: true, transition: {
|
|
53
|
+
duration: 0.5,
|
|
54
|
+
ease: motionEasing.smooth,
|
|
55
|
+
}, className: cn("flex flex-1 flex-col overflow-hidden h-full min-w-0", className), children: children }));
|
|
35
56
|
});
|
|
36
57
|
ChatLayoutMain.displayName = "ChatLayout.Main";
|
|
37
58
|
const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, children, ...props }, ref) => {
|
|
38
59
|
return (_jsxs("div", { ref: ref, className: cn("relative flex flex-1 flex-col overflow-hidden", className), ...props, children: [children, showToaster && _jsx(Toaster, {})] }));
|
|
39
60
|
});
|
|
40
61
|
ChatLayoutBody.displayName = "ChatLayout.Body";
|
|
41
|
-
const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, ...props }, ref) => {
|
|
62
|
+
const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, initialScrollToBottom = true, ...props }, ref) => {
|
|
63
|
+
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
|
42
64
|
const scrollContainerRef = React.useRef(null);
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const lastScrollHeightRef = React.useRef(0);
|
|
47
|
-
const lastMutationTimeRef = React.useRef(0);
|
|
48
|
-
const isSmoothScrollingRef = React.useRef(false); // Protect smooth scrolls from interruption
|
|
49
|
-
// Keep ref in sync with state
|
|
50
|
-
React.useEffect(() => {
|
|
51
|
-
isAtBottomRef.current = isAtBottom;
|
|
52
|
-
}, [isAtBottom]);
|
|
65
|
+
const wasAtBottomRef = React.useRef(true); // Track if user was at bottom before content update
|
|
66
|
+
const isAutoScrollingRef = React.useRef(false); // Track if we're programmatically scrolling
|
|
67
|
+
const hasInitialScrolledRef = React.useRef(false); // Track if initial scroll has happened
|
|
53
68
|
// Merge refs
|
|
54
69
|
React.useImperativeHandle(ref, () => scrollContainerRef.current);
|
|
55
|
-
// Check if at bottom
|
|
56
|
-
const
|
|
70
|
+
// Check if user is at bottom of scroll
|
|
71
|
+
const checkScrollPosition = React.useCallback(() => {
|
|
57
72
|
const container = scrollContainerRef.current;
|
|
58
73
|
if (!container)
|
|
59
|
-
return
|
|
74
|
+
return false;
|
|
60
75
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
77
|
+
const isAtBottom = distanceFromBottom < 100; // 100px threshold
|
|
78
|
+
setShowScrollButton(!isAtBottom && showScrollToBottom);
|
|
79
|
+
onScrollChange?.(isAtBottom);
|
|
80
|
+
return isAtBottom;
|
|
81
|
+
}, [onScrollChange, showScrollToBottom]);
|
|
82
|
+
// Handle scroll events
|
|
83
|
+
const handleScroll = React.useCallback(() => {
|
|
84
|
+
// If this is a programmatic scroll, don't update wasAtBottomRef
|
|
85
|
+
if (isAutoScrollingRef.current) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// This is a user-initiated scroll, update the position
|
|
89
|
+
const isAtBottom = checkScrollPosition();
|
|
90
|
+
wasAtBottomRef.current = isAtBottom;
|
|
91
|
+
}, [checkScrollPosition]);
|
|
92
|
+
// Scroll to bottom function
|
|
93
|
+
const scrollToBottom = React.useCallback((smooth = true) => {
|
|
65
94
|
const container = scrollContainerRef.current;
|
|
66
95
|
if (!container)
|
|
67
96
|
return;
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
if (behavior === "smooth") {
|
|
73
|
-
isSmoothScrollingRef.current = true;
|
|
74
|
-
// Clear the smooth scroll protection after animation completes
|
|
75
|
-
setTimeout(() => {
|
|
76
|
-
isSmoothScrollingRef.current = false;
|
|
77
|
-
}, 500);
|
|
78
|
-
}
|
|
97
|
+
// Mark that we're about to programmatically scroll
|
|
98
|
+
isAutoScrollingRef.current = true;
|
|
99
|
+
wasAtBottomRef.current = true; // Set immediately for instant scrolls
|
|
79
100
|
container.scrollTo({
|
|
80
101
|
top: container.scrollHeight,
|
|
81
|
-
behavior,
|
|
102
|
+
behavior: smooth ? "smooth" : "auto",
|
|
82
103
|
});
|
|
104
|
+
// Clear the flag after scroll completes
|
|
105
|
+
// For instant scrolling, clear immediately; for smooth, wait
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
isAutoScrollingRef.current = false;
|
|
108
|
+
}, smooth ? 300 : 0);
|
|
83
109
|
}, []);
|
|
84
|
-
//
|
|
110
|
+
// Auto-scroll when content changes if user was at bottom
|
|
85
111
|
React.useEffect(() => {
|
|
86
112
|
const container = scrollContainerRef.current;
|
|
87
113
|
if (!container)
|
|
88
114
|
return;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
// If user was at the bottom, scroll to new content
|
|
116
|
+
if (wasAtBottomRef.current && !isAutoScrollingRef.current) {
|
|
117
|
+
// Use requestAnimationFrame to ensure DOM has updated
|
|
118
|
+
requestAnimationFrame(() => {
|
|
119
|
+
scrollToBottom(false); // Use instant scroll for streaming to avoid jarring smooth animations
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Update scroll position state (but don't change wasAtBottomRef if we're auto-scrolling)
|
|
123
|
+
if (!isAutoScrollingRef.current) {
|
|
124
|
+
checkScrollPosition();
|
|
125
|
+
}
|
|
126
|
+
}, [children, scrollToBottom, checkScrollPosition]);
|
|
127
|
+
// Track last scroll height to detect when content stops loading
|
|
128
|
+
const lastScrollHeightRef = React.useRef(0);
|
|
129
|
+
const scrollStableCountRef = React.useRef(0);
|
|
130
|
+
// Scroll to bottom on initial mount and during session loading
|
|
131
|
+
// Keep scrolling until content stabilizes (no more changes)
|
|
132
|
+
React.useEffect(() => {
|
|
133
|
+
if (!initialScrollToBottom)
|
|
134
|
+
return;
|
|
135
|
+
const container = scrollContainerRef.current;
|
|
136
|
+
if (!container)
|
|
137
|
+
return;
|
|
138
|
+
const scrollToBottomInstant = () => {
|
|
139
|
+
container.scrollTop = container.scrollHeight;
|
|
140
|
+
wasAtBottomRef.current = true;
|
|
108
141
|
};
|
|
109
|
-
|
|
110
|
-
|
|
142
|
+
// Check if content has stabilized (scrollHeight hasn't changed)
|
|
143
|
+
const currentHeight = container.scrollHeight;
|
|
144
|
+
if (currentHeight === lastScrollHeightRef.current) {
|
|
145
|
+
scrollStableCountRef.current++;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
scrollStableCountRef.current = 0;
|
|
149
|
+
lastScrollHeightRef.current = currentHeight;
|
|
150
|
+
}
|
|
151
|
+
// If content is still loading (height changing) or we haven't scrolled yet,
|
|
152
|
+
// keep auto-scrolling. Stop after content is stable for a few renders.
|
|
153
|
+
if (scrollStableCountRef.current < 3) {
|
|
154
|
+
isAutoScrollingRef.current = true;
|
|
155
|
+
scrollToBottomInstant();
|
|
156
|
+
hasInitialScrolledRef.current = true;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Content is stable, stop auto-scrolling
|
|
160
|
+
isAutoScrollingRef.current = false;
|
|
161
|
+
}
|
|
162
|
+
}, [initialScrollToBottom, children]);
|
|
163
|
+
// Also use a timer-based approach as backup for session replay
|
|
164
|
+
// which may not trigger children changes
|
|
111
165
|
React.useEffect(() => {
|
|
166
|
+
if (!initialScrollToBottom)
|
|
167
|
+
return;
|
|
112
168
|
const container = scrollContainerRef.current;
|
|
113
169
|
if (!container)
|
|
114
170
|
return;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
171
|
+
// Keep scrolling to bottom for the first 2 seconds of session load
|
|
172
|
+
// to catch async message replay
|
|
173
|
+
let cancelled = false;
|
|
174
|
+
const scrollInterval = setInterval(() => {
|
|
175
|
+
if (cancelled)
|
|
118
176
|
return;
|
|
177
|
+
if (container.scrollHeight > container.clientHeight) {
|
|
178
|
+
isAutoScrollingRef.current = true;
|
|
179
|
+
container.scrollTop = container.scrollHeight;
|
|
180
|
+
wasAtBottomRef.current = true;
|
|
181
|
+
hasInitialScrolledRef.current = true;
|
|
119
182
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
183
|
+
}, 100);
|
|
184
|
+
// Stop after 2 seconds
|
|
185
|
+
const timeout = setTimeout(() => {
|
|
186
|
+
clearInterval(scrollInterval);
|
|
187
|
+
isAutoScrollingRef.current = false;
|
|
188
|
+
}, 2000);
|
|
189
|
+
return () => {
|
|
190
|
+
cancelled = true;
|
|
191
|
+
clearInterval(scrollInterval);
|
|
192
|
+
clearTimeout(timeout);
|
|
193
|
+
};
|
|
194
|
+
}, [initialScrollToBottom]); // Only run once on mount
|
|
195
|
+
// Check scroll position on mount
|
|
196
|
+
React.useEffect(() => {
|
|
197
|
+
if (!isAutoScrollingRef.current) {
|
|
198
|
+
const isAtBottom = checkScrollPosition();
|
|
199
|
+
wasAtBottomRef.current = isAtBottom;
|
|
200
|
+
}
|
|
201
|
+
}, [checkScrollPosition]);
|
|
202
|
+
// Detect user interaction with scroll area (wheel, touch) - IMMEDIATELY break auto-scroll
|
|
203
|
+
const handleUserInteraction = React.useCallback(() => {
|
|
204
|
+
// Immediately mark that user is interacting
|
|
205
|
+
isAutoScrollingRef.current = false;
|
|
206
|
+
// For wheel/touch events, temporarily break auto-scroll
|
|
207
|
+
// The actual scroll event will update wasAtBottomRef properly
|
|
208
|
+
// This prevents the race condition where content updates before scroll completes
|
|
209
|
+
const container = scrollContainerRef.current;
|
|
210
|
+
if (!container)
|
|
211
|
+
return;
|
|
212
|
+
// Check current position BEFORE the scroll happens
|
|
213
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
214
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
215
|
+
// If user is not currently at the bottom, definitely break auto-scroll
|
|
216
|
+
if (distanceFromBottom >= 100) {
|
|
217
|
+
wasAtBottomRef.current = false;
|
|
218
|
+
}
|
|
219
|
+
// If they are at bottom, the scroll event will determine if they stay there
|
|
220
|
+
}, []);
|
|
221
|
+
// Handle keyboard navigation
|
|
222
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
223
|
+
// If user presses arrow keys, page up/down, home/end - they're scrolling
|
|
224
|
+
const scrollKeys = [
|
|
225
|
+
"ArrowUp",
|
|
226
|
+
"ArrowDown",
|
|
227
|
+
"PageUp",
|
|
228
|
+
"PageDown",
|
|
229
|
+
"Home",
|
|
230
|
+
"End",
|
|
231
|
+
];
|
|
232
|
+
if (scrollKeys.includes(e.key)) {
|
|
233
|
+
isAutoScrollingRef.current = false;
|
|
234
|
+
// Check position on next frame after the scroll happens
|
|
129
235
|
requestAnimationFrame(() => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
setIsAtBottom(true);
|
|
137
|
-
isAtBottomRef.current = true;
|
|
236
|
+
const container = scrollContainerRef.current;
|
|
237
|
+
if (!container)
|
|
238
|
+
return;
|
|
239
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
240
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
241
|
+
wasAtBottomRef.current = distanceFromBottom < 100;
|
|
138
242
|
});
|
|
139
|
-
};
|
|
140
|
-
// Watch for DOM changes (new messages, content updates)
|
|
141
|
-
const mutationObserver = new MutationObserver(scrollIfNeeded);
|
|
142
|
-
mutationObserver.observe(container, {
|
|
143
|
-
childList: true,
|
|
144
|
-
subtree: true,
|
|
145
|
-
characterData: true,
|
|
146
|
-
});
|
|
147
|
-
// Watch for size changes (images loading, content expanding)
|
|
148
|
-
const resizeObserver = new ResizeObserver(scrollIfNeeded);
|
|
149
|
-
resizeObserver.observe(container);
|
|
150
|
-
// Also observe direct children for size changes
|
|
151
|
-
for (const child of Array.from(container.children)) {
|
|
152
|
-
resizeObserver.observe(child);
|
|
153
243
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return () => {
|
|
157
|
-
mutationObserver.disconnect();
|
|
158
|
-
resizeObserver.disconnect();
|
|
159
|
-
};
|
|
160
|
-
}, [scrollToBottom]);
|
|
161
|
-
// Button click handler - smooth scroll and re-engage auto-scroll
|
|
162
|
-
const handleScrollToBottomClick = React.useCallback(() => {
|
|
163
|
-
setIsAtBottom(true);
|
|
164
|
-
isAtBottomRef.current = true;
|
|
165
|
-
scrollToBottom("smooth");
|
|
166
|
-
}, [scrollToBottom]);
|
|
167
|
-
return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), tabIndex: 0, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollToBottom && (_jsx("button", { type: "button", onClick: handleScrollToBottomClick, className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", isAtBottom
|
|
168
|
-
? "pointer-events-none scale-0 opacity-0"
|
|
169
|
-
: "pointer-events-auto scale-100 opacity-100"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
|
|
244
|
+
}, []);
|
|
245
|
+
return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), onScroll: handleScroll, onWheel: handleUserInteraction, onTouchStart: handleUserInteraction, onKeyDown: handleKeyDown, tabIndex: 0, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollButton && (_jsx("button", { type: "button", onClick: () => scrollToBottom(true), className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
|
|
170
246
|
});
|
|
171
247
|
ChatLayoutMessages.displayName = "ChatLayout.Messages";
|
|
172
248
|
const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
@@ -180,32 +256,63 @@ const ChatLayoutSidebar = React.forwardRef(({ className, children, ...props }, r
|
|
|
180
256
|
return (_jsx("div", { ref: ref, className: cn("border-r border-border bg-card w-64 overflow-y-auto", className), ...props, children: children }));
|
|
181
257
|
});
|
|
182
258
|
ChatLayoutSidebar.displayName = "ChatLayout.Sidebar";
|
|
183
|
-
const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children
|
|
259
|
+
const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children }, ref) => {
|
|
184
260
|
const { panelSize } = useChatLayoutContext();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
window.addEventListener("resize", updateMinSize);
|
|
195
|
-
return () => {
|
|
196
|
-
window.removeEventListener("resize", updateMinSize);
|
|
197
|
-
};
|
|
198
|
-
}, []);
|
|
261
|
+
// State for committed width (persisted between renders)
|
|
262
|
+
const [committedWidth, setCommittedWidth] = React.useState(450);
|
|
263
|
+
// Motion value for real-time drag updates
|
|
264
|
+
const widthMotionValue = useMotionValue(committedWidth);
|
|
265
|
+
// Track if currently dragging for visual feedback
|
|
266
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
267
|
+
// Track drag start position and width
|
|
268
|
+
const dragStartX = React.useRef(0);
|
|
269
|
+
const dragStartWidth = React.useRef(committedWidth);
|
|
199
270
|
// Hidden state - don't render
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
271
|
+
const isVisible = panelSize !== "hidden";
|
|
272
|
+
// Sync motion value when committed width changes
|
|
273
|
+
React.useEffect(() => {
|
|
274
|
+
widthMotionValue.set(committedWidth);
|
|
275
|
+
}, [committedWidth, widthMotionValue]);
|
|
276
|
+
// Handle pointer down to start drag
|
|
277
|
+
const handlePointerDown = React.useCallback((e) => {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
setIsDragging(true);
|
|
280
|
+
dragStartX.current = e.clientX;
|
|
281
|
+
dragStartWidth.current = committedWidth;
|
|
282
|
+
// Capture pointer for smooth dragging
|
|
283
|
+
e.target.setPointerCapture(e.pointerId);
|
|
284
|
+
}, [committedWidth]);
|
|
285
|
+
// Handle pointer move during drag
|
|
286
|
+
const handlePointerMove = React.useCallback((e) => {
|
|
287
|
+
if (!isDragging)
|
|
288
|
+
return;
|
|
289
|
+
const deltaX = e.clientX - dragStartX.current;
|
|
290
|
+
const newWidth = Math.max(250, Math.min(800, dragStartWidth.current - deltaX));
|
|
291
|
+
widthMotionValue.set(newWidth);
|
|
292
|
+
}, [isDragging, widthMotionValue]);
|
|
293
|
+
// Handle pointer up to end drag
|
|
294
|
+
const handlePointerUp = React.useCallback((e) => {
|
|
295
|
+
if (!isDragging)
|
|
296
|
+
return;
|
|
297
|
+
const deltaX = e.clientX - dragStartX.current;
|
|
298
|
+
const newWidth = Math.max(250, Math.min(800, dragStartWidth.current - deltaX));
|
|
299
|
+
setCommittedWidth(newWidth);
|
|
300
|
+
widthMotionValue.set(newWidth);
|
|
301
|
+
setIsDragging(false);
|
|
302
|
+
// Release pointer capture
|
|
303
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
304
|
+
}, [isDragging, widthMotionValue]);
|
|
305
|
+
return (_jsx(AnimatePresence, { initial: false, mode: "wait", children: isVisible && (_jsxs(motion.div, { ref: ref, layout: true, initial: { width: 0, opacity: 0 }, animate: { width: committedWidth, opacity: 1 }, exit: { width: 0, opacity: 0 }, transition: {
|
|
306
|
+
duration: 0.5,
|
|
307
|
+
ease: motionEasing.smooth,
|
|
308
|
+
}, style: {
|
|
309
|
+
// Use motion value during drag for instant updates
|
|
310
|
+
width: isDragging ? widthMotionValue : undefined,
|
|
311
|
+
}, className: cn(
|
|
312
|
+
// Hidden by default, visible at breakpoint
|
|
313
|
+
"hidden h-full border-l border-border bg-card overflow-hidden relative",
|
|
314
|
+
// Breakpoint visibility
|
|
315
|
+
breakpoint === "md" && "md:block", breakpoint === "lg" && "lg:block", breakpoint === "xl" && "xl:block", breakpoint === "2xl" && "2xl:block", className), children: [_jsx("div", { onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, className: cn("absolute left-0 inset-y-0 w-2 -ml-1 cursor-col-resize z-10", "group flex items-center justify-center", "hover:bg-primary/5 transition-colors delay-300", isDragging && "bg-primary/10") }), _jsx("div", { className: "h-full overflow-y-auto", style: { width: committedWidth }, children: children })] }, "aside-panel")) }));
|
|
209
316
|
});
|
|
210
317
|
ChatLayoutAside.displayName = "ChatLayout.Aside";
|
|
211
318
|
/* -------------------------------------------------------------------------------------------------
|