diff --git a/components/chat.tsx b/components/chat.tsx index 4380db1..d5a904b 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -2,7 +2,7 @@ import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; -import { useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import useSWR, { useSWRConfig } from "swr"; import { unstable_serialize } from "swr/infinite"; @@ -50,12 +50,25 @@ export function Chat({ autoResume: boolean; initialLastContext?: AppUsage; }) { + const router = useRouter(); + const { visibilityType } = useChatVisibility({ chatId: id, initialVisibilityType, }); const { mutate } = useSWRConfig(); + + // Handle browser back/forward navigation + useEffect(() => { + const handlePopState = () => { + // When user navigates back/forward, refresh to sync with URL + router.refresh(); + }; + + window.addEventListener("popstate", handlePopState); + return () => window.removeEventListener("popstate", handlePopState); + }, [router]); const { setDataStream } = useDataStream(); const [input, setInput] = useState(""); diff --git a/components/messages.tsx b/components/messages.tsx index d9a3e16..d3653d5 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -44,51 +44,57 @@ function PureMessages({ useDataStream(); return ( -
-
- {messages.length === 0 && } +
+
+
+ {messages.length === 0 && } - {messages.map((message, index) => ( - vote.messageId === message.id) - : undefined - } + {messages.map((message, index) => ( + vote.messageId === message.id) + : undefined + } + /> + ))} + + {status === "submitted" && } + +
- ))} - - {status === "submitted" && } - -
+
- {!isAtBottom && ( - - )} +
); } diff --git a/components/suggested-actions.tsx b/components/suggested-actions.tsx index e2d7ef6..8f63336 100644 --- a/components/suggested-actions.tsx +++ b/components/suggested-actions.tsx @@ -37,7 +37,7 @@ function PureSuggestedActions({ chatId, sendMessage }: SuggestedActionsProps) { { - window.history.replaceState({}, "", `/chat/${chatId}`); + window.history.pushState({}, "", `/chat/${chatId}`); sendMessage({ role: "user", parts: [{ type: "text", text: suggestion }], diff --git a/components/weather.tsx b/components/weather.tsx index 074ba69..049d80a 100644 --- a/components/weather.tsx +++ b/components/weather.tsx @@ -330,7 +330,7 @@ export function Weather({ return (
-
-
{location}
+
+
{location}
{format(new Date(weatherAtLocation.current.time), "MMM d, h:mm a")}
-
-
+
+
- {isDay ? : } + {isDay ? : }
-
+
{n(weatherAtLocation.current.temperature_2m)} - + {weatherAtLocation.current_units.temperature_2m}
-
+
H: {n(currentHigh)}°
-
L: {n(currentLow)}°
+
L: {n(currentLow)}°
-
-
+
+
Hourly Forecast
-
+
{displayTimes.map((time, index) => { const hourTime = new Date(time); const isCurrentHour = @@ -389,7 +389,7 @@ export function Weather({ return (
- +
-
+
{n(displayTemperatures[index])}°
@@ -418,7 +418,7 @@ export function Weather({
-
+
Sunrise:{" "} {format(new Date(weatherAtLocation.daily.sunrise[0]), "h:mm a")} diff --git a/hooks/use-scroll-to-bottom.tsx b/hooks/use-scroll-to-bottom.tsx index c56e3f5..6bd814a 100644 --- a/hooks/use-scroll-to-bottom.tsx +++ b/hooks/use-scroll-to-bottom.tsx @@ -1,102 +1,119 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import useSWR from "swr"; - -type ScrollFlag = ScrollBehavior | false; export function useScrollToBottom() { const containerRef = useRef(null); const endRef = useRef(null); const [isAtBottom, setIsAtBottom] = useState(true); + const isAtBottomRef = useRef(true); + const isUserScrollingRef = useRef(false); - const { data: scrollBehavior = false, mutate: setScrollBehavior } = - useSWR("messages:should-scroll", null, { fallbackData: false }); + // Keep ref in sync with state + useEffect(() => { + isAtBottomRef.current = isAtBottom; + }, [isAtBottom]); - const handleScroll = useCallback(() => { + const checkIfAtBottom = useCallback(() => { if (!containerRef.current) { - return; + return true; } const { scrollTop, scrollHeight, clientHeight } = containerRef.current; - - // Check if we are within 100px of the bottom (like v0 does) - setIsAtBottom(scrollTop + clientHeight >= scrollHeight - 100); + return scrollTop + clientHeight >= scrollHeight - 100; }, []); - useEffect(() => { + const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => { if (!containerRef.current) { return; } - - const container = containerRef.current; - - const resizeObserver = new ResizeObserver(() => { - requestAnimationFrame(() => { - handleScroll(); - }); + containerRef.current.scrollTo({ + top: containerRef.current.scrollHeight, + behavior, }); + }, []); - const mutationObserver = new MutationObserver(() => { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - handleScroll(); - }); - }); - }); - - resizeObserver.observe(container); - mutationObserver.observe(container, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ["style", "class", "data-state"], - }); - - handleScroll(); - - return () => { - resizeObserver.disconnect(); - mutationObserver.disconnect(); - }; - }, [handleScroll]); - + // Handle user scroll events useEffect(() => { const container = containerRef.current; if (!container) { return; } - container.addEventListener("scroll", handleScroll); - handleScroll(); // Check initial state + let scrollTimeout: ReturnType; + const handleScroll = () => { + // Mark as user scrolling + isUserScrollingRef.current = true; + clearTimeout(scrollTimeout); + + // Update isAtBottom state + const atBottom = checkIfAtBottom(); + setIsAtBottom(atBottom); + isAtBottomRef.current = atBottom; + + // Reset user scrolling flag after scroll ends + scrollTimeout = setTimeout(() => { + isUserScrollingRef.current = false; + }, 150); + }; + + container.addEventListener("scroll", handleScroll, { passive: true }); return () => { container.removeEventListener("scroll", handleScroll); + clearTimeout(scrollTimeout); }; - }, [handleScroll]); + }, [checkIfAtBottom]); + // Auto-scroll when content changes useEffect(() => { - if (scrollBehavior && containerRef.current) { - const container = containerRef.current; - const scrollOptions: ScrollToOptions = { - top: container.scrollHeight, - behavior: scrollBehavior, - }; - container.scrollTo(scrollOptions); - setScrollBehavior(false); + const container = containerRef.current; + if (!container) { + return; } - }, [scrollBehavior, setScrollBehavior]); - const scrollToBottom = useCallback( - (currentScrollBehavior: ScrollBehavior = "smooth") => { - setScrollBehavior(currentScrollBehavior); - }, - [setScrollBehavior] - ); + const scrollIfNeeded = () => { + // Only auto-scroll if user was at bottom and isn't actively scrolling + if (isAtBottomRef.current && !isUserScrollingRef.current) { + requestAnimationFrame(() => { + container.scrollTo({ + top: container.scrollHeight, + behavior: "instant", + }); + setIsAtBottom(true); + isAtBottomRef.current = true; + }); + } + }; + + // Watch for DOM changes + const mutationObserver = new MutationObserver(scrollIfNeeded); + mutationObserver.observe(container, { + childList: true, + subtree: true, + characterData: true, + }); + + // Watch for size changes + const resizeObserver = new ResizeObserver(scrollIfNeeded); + resizeObserver.observe(container); + + // Also observe children for size changes + for (const child of container.children) { + resizeObserver.observe(child); + } + + return () => { + mutationObserver.disconnect(); + resizeObserver.disconnect(); + }; + }, []); function onViewportEnter() { setIsAtBottom(true); + isAtBottomRef.current = true; } function onViewportLeave() { setIsAtBottom(false); + isAtBottomRef.current = false; } return { diff --git a/tsconfig.json b/tsconfig.json index 657b4a3..e11ae50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ {