292 lines · 10.0 KB · TypeScript
RawDownload
import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router";import { Suspense, lazy, type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import ChatView from "../components/ChatView";import { threadHasStarted } from "../components/ChatView.logic";import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider";import {  DiffPanelHeaderSkeleton,  DiffPanelLoadingState,  DiffPanelShell,  type DiffPanelMode,} from "../components/DiffPanelShell";import { finalizePromotedDraftThreadByRef, useComposerDraftStore } from "../composerDraftStore";import {  type DiffRouteSearch,  parseDiffRouteSearch,  stripDiffSearchParams,} from "../diffRouteSearch";import { useMediaQuery } from "../hooks/useMediaQuery";import { selectEnvironmentState, selectThreadByRef, useStore } from "../store";import { createThreadSelectorByRef } from "../storeSelectors";import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes";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 navigate = useNavigate();  const threadRef = Route.useParams({    select: (params) => resolveThreadRouteRef(params),  });  const search = Route.useSearch();  const bootstrapComplete = useStore(    (store) => selectEnvironmentState(store, threadRef?.environmentId ?? null).bootstrapComplete,  );  const serverThread = useStore(useMemo(() => createThreadSelectorByRef(threadRef), [threadRef]));  const threadExists = useStore((store) => selectThreadByRef(store, threadRef) !== undefined);  const environmentHasServerThreads = useStore(    (store) => selectEnvironmentState(store, threadRef?.environmentId ?? null).threadIds.length > 0,  );  const draftThreadExists = useComposerDraftStore((store) =>    threadRef ? store.getDraftThreadByRef(threadRef) !== null : false,  );  const draftThread = useComposerDraftStore((store) =>    threadRef ? store.getDraftThreadByRef(threadRef) : null,  );  const environmentHasDraftThreads = useComposerDraftStore((store) => {    if (!threadRef) {      return false;    }    return store.hasDraftThreadsInEnvironment(threadRef.environmentId);  });  const routeThreadExists = threadExists || draftThreadExists;  const serverThreadStarted = threadHasStarted(serverThread);  const environmentHasAnyThreads = environmentHasServerThreads || environmentHasDraftThreads;  const diffOpen = search.diff === "1";  const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY);  const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen);  const closeDiff = useCallback(() => {    if (!threadRef) {      return;    }    void navigate({      to: "/$environmentId/$threadId",      params: buildThreadRouteParams(threadRef),      search: { diff: undefined },    });  }, [navigate, threadRef]);  const openDiff = useCallback(() => {    if (!threadRef) {      return;    }    void navigate({      to: "/$environmentId/$threadId",      params: buildThreadRouteParams(threadRef),      search: (previous) => {        const rest = stripDiffSearchParams(previous);        return { ...rest, diff: "1" };      },    });  }, [navigate, threadRef]);   useEffect(() => {    if (diffOpen) {      setHasOpenedDiff(true);    }  }, [diffOpen]);   useEffect(() => {    if (!threadRef || !bootstrapComplete) {      return;    }     if (!routeThreadExists && environmentHasAnyThreads) {      void navigate({ to: "/", replace: true });    }  }, [bootstrapComplete, environmentHasAnyThreads, navigate, routeThreadExists, threadRef]);   useEffect(() => {    if (!threadRef || !serverThreadStarted || !draftThread?.promotedTo) {      return;    }    finalizePromotedDraftThreadByRef(threadRef);  }, [draftThread?.promotedTo, serverThreadStarted, threadRef]);   if (!threadRef || !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            environmentId={threadRef.environmentId}            threadId={threadRef.threadId}            routeKind="server"          />        </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          environmentId={threadRef.environmentId}          threadId={threadRef.threadId}          routeKind="server"        />      </SidebarInset>      <DiffPanelSheet diffOpen={diffOpen} onCloseDiff={closeDiff}>        {shouldRenderDiffContent ? <LazyDiffPanel mode="sheet" /> : null}      </DiffPanelSheet>    </>  );} export const Route = createFileRoute("/_chat/$environmentId/$threadId")({  validateSearch: (search) => parseDiffRouteSearch(search),  search: {    middlewares: [retainSearchParams<DiffRouteSearch>(["diff"])],  },  component: ChatThreadRouteView,});