|
@@ -229,6 +229,8 @@ function ChatPanel({
|
|
|
}) {
|
|
}) {
|
|
|
const [replyText, setReplyText] = useState("");
|
|
const [replyText, setReplyText] = useState("");
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
+ const scrollWrapperRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
+ const prevMsgCountRef = useRef(0);
|
|
|
const utils = trpc.useUtils();
|
|
const utils = trpc.useUtils();
|
|
|
|
|
|
|
|
const { data: messagesData, isLoading } = trpc.agent.messages.useQuery(
|
|
const { data: messagesData, isLoading } = trpc.agent.messages.useQuery(
|
|
@@ -251,8 +253,33 @@ function ChatPanel({
|
|
|
},
|
|
},
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Initial open: scroll to bottom immediately when conversation changes
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
|
|
|
|
|
+ prevMsgCountRef.current = 0;
|
|
|
|
|
+ const timer = setTimeout(() => {
|
|
|
|
|
+ messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
|
|
|
|
|
+ }, 80);
|
|
|
|
|
+ return () => clearTimeout(timer);
|
|
|
|
|
+ }, [conversationId]);
|
|
|
|
|
+
|
|
|
|
|
+ // Polling update: only scroll to bottom if user is already near the bottom
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (!messagesData) return;
|
|
|
|
|
+ const count = messagesData.length;
|
|
|
|
|
+ if (count > prevMsgCountRef.current && prevMsgCountRef.current > 0) {
|
|
|
|
|
+ // New message arrived — check if user is near bottom before scrolling
|
|
|
|
|
+ const viewport = scrollWrapperRef.current?.querySelector(
|
|
|
|
|
+ "[data-radix-scroll-area-viewport]"
|
|
|
|
|
+ ) as HTMLElement | null;
|
|
|
|
|
+ if (viewport) {
|
|
|
|
|
+ const { scrollTop, scrollHeight, clientHeight } = viewport;
|
|
|
|
|
+ const isNearBottom = scrollHeight - scrollTop - clientHeight < 120;
|
|
|
|
|
+ if (isNearBottom) {
|
|
|
|
|
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ prevMsgCountRef.current = count;
|
|
|
}, [messagesData]);
|
|
}, [messagesData]);
|
|
|
|
|
|
|
|
const handleSend = () => {
|
|
const handleSend = () => {
|
|
@@ -332,7 +359,8 @@ function ChatPanel({
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Messages */}
|
|
{/* Messages */}
|
|
|
- <ScrollArea className="flex-1 p-4">
|
|
|
|
|
|
|
+ <div ref={scrollWrapperRef} className="flex-1 min-h-0">
|
|
|
|
|
+ <ScrollArea className="h-full p-4">
|
|
|
<div className="space-y-3">
|
|
<div className="space-y-3">
|
|
|
{isLoading ? (
|
|
{isLoading ? (
|
|
|
<div className="text-center py-8 text-sm" style={{ color: "#a8a29e" }}>Loading messages...</div>
|
|
<div className="text-center py-8 text-sm" style={{ color: "#a8a29e" }}>Loading messages...</div>
|
|
@@ -342,6 +370,7 @@ function ChatPanel({
|
|
|
<div ref={messagesEndRef} />
|
|
<div ref={messagesEndRef} />
|
|
|
</div>
|
|
</div>
|
|
|
</ScrollArea>
|
|
</ScrollArea>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
{/* Reply input */}
|
|
{/* Reply input */}
|
|
|
{conversation?.status !== "closed" && (
|
|
{conversation?.status !== "closed" && (
|