255 lines · 8.5 KB · TypeScript
RawDownload
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 (    <Sheet      open={props.diffOpen}      onOpenChange={(open) => {        if (!open) {          props.onCloseDiff();        }      }}    >      <SheetPopup        side="right"        showCloseButton={false}        keepMounted        className="w-[min(88vw,820px)] max-w-[820px] p-0"      >        {props.children}      </SheetPopup>    </Sheet>  );}; const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => {  return (    <DiffPanelShell mode={props.mode} header={<DiffPanelHeaderSkeleton />}>      <DiffPanelLoadingState label="Loading diff viewer..." />    </DiffPanelShell>  );}; const LazyDiffPanel = (props: { mode: DiffPanelMode }) => {  return (    <DiffWorkerPoolProvider>      <Suspense fallback={<DiffLoadingFallback mode={props.mode} />}>        <DiffPanel mode={props.mode} />      </Suspense>    </DiffWorkerPoolProvider>  );}; 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<HTMLElement>("[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<HTMLElement>(        "[data-chat-composer-footer='true']",      );      const composerRightActions = composerForm.querySelector<HTMLElement>(        "[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 (    <SidebarProvider      defaultOpen={false}      open={diffOpen}      onOpenChange={onOpenChange}      className="w-auto min-h-0 flex-none bg-transparent"      style={{ "--sidebar-width": DIFF_INLINE_DEFAULT_WIDTH } as React.CSSProperties}    >      <Sidebar        side="right"        collapsible="offcanvas"        className="border-l border-border bg-card text-foreground"        resizable={{          minWidth: DIFF_INLINE_SIDEBAR_MIN_WIDTH,          shouldAcceptWidth: shouldAcceptInlineSidebarWidth,          storageKey: DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY,        }}      >        {renderDiffContent ? <LazyDiffPanel mode="sidebar" /> : null}        <SidebarRail />      </Sidebar>    </SidebarProvider>  );}; 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 (      <>        <SidebarInset className="h-dvh  min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">          <ChatView key={threadId} threadId={threadId} />        </SidebarInset>        <DiffPanelInlineSidebar          diffOpen={diffOpen}          onCloseDiff={closeDiff}          onOpenDiff={openDiff}          renderDiffContent={shouldRenderDiffContent}        />      </>    );  }   return (    <>      <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">        <ChatView key={threadId} threadId={threadId} />      </SidebarInset>      <DiffPanelSheet diffOpen={diffOpen} onCloseDiff={closeDiff}>        {shouldRenderDiffContent ? <LazyDiffPanel mode="sheet" /> : null}      </DiffPanelSheet>    </>  );} export const Route = createFileRoute("/_chat/$threadId")({  validateSearch: (search) => parseDiffRouteSearch(search),  search: {    middlewares: [retainSearchParams<DiffRouteSearch>(["diff"])],  },  component: ChatThreadRouteView,});