import { ThreadId } from "@t3tools/contracts"; import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router"; import { Suspense, lazy, type ReactNode, useCallback, useEffect, useState } from "react"; import ChatView from "../components/ChatView"; import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider"; import { DiffPanelHeaderSkeleton, DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode, } from "../components/DiffPanelShell"; import { useComposerDraftStore } from "../composerDraftStore"; import { type DiffRouteSearch, parseDiffRouteSearch, stripDiffSearchParams, } from "../diffRouteSearch"; import { useMediaQuery } from "../hooks/useMediaQuery"; import { useStore } from "../store"; import { Sheet, SheetPopup } from "../components/ui/sheet"; import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; const DiffPanel = lazy(() => import("../components/DiffPanel")); const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width"; const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)"; const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16; const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208; const DiffPanelSheet = (props: { children: ReactNode; diffOpen: boolean; onCloseDiff: () => void; }) => { return ( { if (!open) { props.onCloseDiff(); } }} > {props.children} ); }; const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => { return ( }> ); }; const LazyDiffPanel = (props: { mode: DiffPanelMode }) => { return ( }> ); }; const DiffPanelInlineSidebar = (props: { diffOpen: boolean; onCloseDiff: () => void; onOpenDiff: () => void; renderDiffContent: boolean; }) => { const { diffOpen, onCloseDiff, onOpenDiff, renderDiffContent } = props; const onOpenChange = useCallback( (open: boolean) => { if (open) { onOpenDiff(); return; } onCloseDiff(); }, [onCloseDiff, onOpenDiff], ); const shouldAcceptInlineSidebarWidth = useCallback( ({ nextWidth, wrapper }: { nextWidth: number; wrapper: HTMLElement }) => { const composerForm = document.querySelector("[data-chat-composer-form='true']"); if (!composerForm) return true; const composerViewport = composerForm.parentElement; if (!composerViewport) return true; const previousSidebarWidth = wrapper.style.getPropertyValue("--sidebar-width"); wrapper.style.setProperty("--sidebar-width", `${nextWidth}px`); const viewportStyle = window.getComputedStyle(composerViewport); const viewportPaddingLeft = Number.parseFloat(viewportStyle.paddingLeft) || 0; const viewportPaddingRight = Number.parseFloat(viewportStyle.paddingRight) || 0; const viewportContentWidth = Math.max( 0, composerViewport.clientWidth - viewportPaddingLeft - viewportPaddingRight, ); const formRect = composerForm.getBoundingClientRect(); const composerFooter = composerForm.querySelector( "[data-chat-composer-footer='true']", ); const composerRightActions = composerForm.querySelector( "[data-chat-composer-actions='right']", ); const composerRightActionsWidth = composerRightActions?.getBoundingClientRect().width ?? 0; const composerFooterGap = composerFooter ? Number.parseFloat(window.getComputedStyle(composerFooter).columnGap) || Number.parseFloat(window.getComputedStyle(composerFooter).gap) || 0 : 0; const minimumComposerWidth = COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX + composerRightActionsWidth + composerFooterGap; const hasComposerOverflow = composerForm.scrollWidth > composerForm.clientWidth + 0.5; const overflowsViewport = formRect.width > viewportContentWidth + 0.5; const violatesMinimumComposerWidth = composerForm.clientWidth + 0.5 < minimumComposerWidth; if (previousSidebarWidth.length > 0) { wrapper.style.setProperty("--sidebar-width", previousSidebarWidth); } else { wrapper.style.removeProperty("--sidebar-width"); } return !hasComposerOverflow && !overflowsViewport && !violatesMinimumComposerWidth; }, [], ); return ( {renderDiffContent ? : null} ); }; function ChatThreadRouteView() { const bootstrapComplete = useStore((store) => store.bootstrapComplete); const navigate = useNavigate(); const threadId = Route.useParams({ select: (params) => ThreadId.makeUnsafe(params.threadId), }); const search = Route.useSearch(); const threadExists = useStore((store) => store.threads.some((thread) => thread.id === threadId)); const draftThreadExists = useComposerDraftStore((store) => Object.hasOwn(store.draftThreadsByThreadId, threadId), ); const routeThreadExists = threadExists || draftThreadExists; const diffOpen = search.diff === "1"; const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); // TanStack Router keeps active route components mounted across param-only navigations // unless remountDeps are configured, so this stays warm across thread switches. const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen); const closeDiff = useCallback(() => { void navigate({ to: "/$threadId", params: { threadId }, search: { diff: undefined }, }); }, [navigate, threadId]); const openDiff = useCallback(() => { void navigate({ to: "/$threadId", params: { threadId }, search: (previous) => { const rest = stripDiffSearchParams(previous); return { ...rest, diff: "1" }; }, }); }, [navigate, threadId]); useEffect(() => { if (diffOpen) { setHasOpenedDiff(true); } }, [diffOpen]); useEffect(() => { if (!bootstrapComplete) { return; } if (!routeThreadExists) { void navigate({ to: "/", replace: true }); return; } }, [bootstrapComplete, navigate, routeThreadExists, threadId]); if (!bootstrapComplete || !routeThreadExists) { return null; } const shouldRenderDiffContent = diffOpen || hasOpenedDiff; if (!shouldUseDiffSheet) { return ( <> ); } return ( <> {shouldRenderDiffContent ? : null} ); } export const Route = createFileRoute("/_chat/$threadId")({ validateSearch: (search) => parseDiffRouteSearch(search), search: { middlewares: [retainSearchParams(["diff"])], }, component: ChatThreadRouteView, });