Merge pull request #872 from MODSetter/dev

refactor: fixed firefox rendering issues
This commit is contained in:
Rohan Verma 2026-03-11 16:44:40 -07:00 committed by GitHub
commit c74553219e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 55 additions and 279 deletions

View file

@ -276,7 +276,7 @@ export default function NewChatPage() {
const initializeThread = useCallback(async () => { const initializeThread = useCallback(async () => {
setIsInitializing(true); setIsInitializing(true);
// Reset all state when switching between chats to prevent stale data // Reset all state when switching between chats/search spaces to prevent stale data
setMessages([]); setMessages([]);
setThreadId(null); setThreadId(null);
setCurrentThread(null); setCurrentThread(null);
@ -284,8 +284,8 @@ export default function NewChatPage() {
setMentionedDocuments([]); setMentionedDocuments([]);
setSidebarDocuments([]); setSidebarDocuments([]);
setMessageDocumentsMap({}); setMessageDocumentsMap({});
clearPlanOwnerRegistry(); // Reset plan ownership for new chat clearPlanOwnerRegistry();
closeReportPanel(); // Close report panel when switching chats closeReportPanel();
try { try {
if (urlChatId > 0) { if (urlChatId > 0) {
@ -346,6 +346,7 @@ export default function NewChatPage() {
} }
}, [ }, [
urlChatId, urlChatId,
searchSpaceId,
setMessageDocumentsMap, setMessageDocumentsMap,
setMentionedDocuments, setMentionedDocuments,
setSidebarDocuments, setSidebarDocuments,
@ -1671,7 +1672,7 @@ export default function NewChatPage() {
<DeleteGoogleDriveFileToolUI /> <DeleteGoogleDriveFileToolUI />
<SandboxExecuteToolUI /> <SandboxExecuteToolUI />
{/* <WriteTodosToolUI /> Disabled for now */} {/* <WriteTodosToolUI /> Disabled for now */}
<div className="flex h-[calc(100dvh-64px)] overflow-hidden"> <div key={searchSpaceId} className="flex h-[calc(100dvh-64px)] overflow-hidden">
<div className="flex-1 flex flex-col min-w-0 overflow-hidden"> <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<Thread messageThinkingSteps={messageThinkingSteps} /> <Thread messageThinkingSteps={messageThinkingSteps} />
</div> </div>

View file

@ -220,14 +220,12 @@ const ThreadWelcome: FC = () => {
return ( return (
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative"> <div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
{/* Greeting positioned above the composer - fixed position */} {/* Greeting positioned above the composer */}
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center"> <div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-3xl md:text-5xl delay-100 duration-500 ease-out fill-mode-both"> <h1 className="aui-thread-welcome-message-inner text-3xl md:text-5xl">{greeting}</h1>
{greeting}
</h1>
</div> </div>
{/* Composer - top edge fixed, expands downward only */} {/* Composer - top edge fixed, expands downward only */}
<div className="fade-in slide-in-from-bottom-3 animate-in delay-200 duration-500 ease-out fill-mode-both w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0"> <div className="w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0">
<Composer /> <Composer />
</div> </div>
</div> </div>

View file

@ -1,174 +1,10 @@
"use client"; "use client";
import { IconBrandGithub } from "@tabler/icons-react"; import { IconBrandGithub } from "@tabler/icons-react";
import { StarIcon } from "lucide-react"; import { motion, useMotionValue, useSpring } from "motion/react";
import type { HTMLMotionProps, UseInViewOptions } from "motion/react";
import {
AnimatePresence,
motion,
useInView,
useMotionValue,
useSpring,
useTransform,
} from "motion/react";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Utilities
// ---------------------------------------------------------------------------
function getStrictContext<T>(name?: string) {
const Context = React.createContext<T | undefined>(undefined);
const Provider = ({ value, children }: { value: T; children?: React.ReactNode }) => (
<Context.Provider value={value}>{children}</Context.Provider>
);
const useSafeContext = () => {
const ctx = React.useContext(Context);
if (ctx === undefined) {
throw new Error(`useContext must be used within ${name ?? "a Provider"}`);
}
return ctx;
};
return [Provider, useSafeContext] as const;
}
interface UseIsInViewOptions {
inView?: boolean;
inViewOnce?: boolean;
inViewMargin?: UseInViewOptions["margin"];
}
function useIsInView<T extends HTMLElement = HTMLElement>(
ref: React.Ref<T>,
options: UseIsInViewOptions = {}
) {
const { inView, inViewOnce = false, inViewMargin = "0px" } = options;
const localRef = React.useRef<T>(null);
React.useImperativeHandle(ref, () => localRef.current as T);
const inViewResult = useInView(localRef, {
once: inViewOnce,
margin: inViewMargin,
});
const isInView = !inView || inViewResult;
return { ref: localRef, isInView };
}
// ---------------------------------------------------------------------------
// Particles (for star burst effect on completion)
// ---------------------------------------------------------------------------
type ParticlesContextType = { animate: boolean; isInView: boolean };
const [ParticlesProvider, useParticles] =
getStrictContext<ParticlesContextType>("ParticlesContext");
function Particles({
ref,
animate = true,
inView = false,
inViewMargin = "0px",
inViewOnce = true,
children,
style,
...props
}: Omit<HTMLMotionProps<"div">, "children"> & {
animate?: boolean;
children: React.ReactNode;
} & UseIsInViewOptions) {
const { ref: localRef, isInView } = useIsInView(ref as React.Ref<HTMLDivElement>, {
inView,
inViewOnce,
inViewMargin,
});
return (
<ParticlesProvider value={{ animate, isInView }}>
<motion.div ref={localRef} style={{ position: "relative", ...style }} {...props}>
{children}
</motion.div>
</ParticlesProvider>
);
}
function ParticlesEffect({
side = "top",
align = "center",
count = 6,
radius = 30,
spread = 360,
duration = 0.8,
holdDelay = 0.05,
sideOffset = 0,
alignOffset = 0,
delay = 0,
transition,
style,
...props
}: Omit<HTMLMotionProps<"div">, "children"> & {
side?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
count?: number;
radius?: number;
spread?: number;
duration?: number;
holdDelay?: number;
sideOffset?: number;
alignOffset?: number;
delay?: number;
}) {
const { animate, isInView } = useParticles();
const isVertical = side === "top" || side === "bottom";
const alignPct = align === "start" ? "0%" : align === "end" ? "100%" : "50%";
const top = isVertical
? side === "top"
? `calc(0% - ${sideOffset}px)`
: `calc(100% + ${sideOffset}px)`
: `calc(${alignPct} + ${alignOffset}px)`;
const left = isVertical
? `calc(${alignPct} + ${alignOffset}px)`
: side === "left"
? `calc(0% - ${sideOffset}px)`
: `calc(100% + ${sideOffset}px)`;
const containerStyle: React.CSSProperties = {
position: "absolute",
top,
left,
transform: "translate(-50%, -50%)",
};
const angleStep = (spread * (Math.PI / 180)) / Math.max(1, count - 1);
return (
<AnimatePresence>
{animate &&
isInView &&
[...Array(count)].map((_, i) => {
const angle = i * angleStep;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
return (
<motion.div
key={`particle-${angle}`}
style={{ ...containerStyle, ...style }}
initial={{ scale: 0, opacity: 0 }}
animate={{
x: `${x}px`,
y: `${y}px`,
scale: [0, 1, 0],
opacity: [0, 1, 0],
}}
transition={{
duration,
delay: delay + i * holdDelay,
ease: "easeOut",
...transition,
}}
{...props}
/>
);
})}
</AnimatePresence>
);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Per-digit scrolling wheel // Per-digit scrolling wheel
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -409,11 +245,6 @@ function NavbarGitHubStars({
}: NavbarGitHubStarsProps) { }: NavbarGitHubStarsProps) {
const [stars, setStars] = React.useState(0); const [stars, setStars] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(true); const [isLoading, setIsLoading] = React.useState(true);
const [isCompleted, setIsCompleted] = React.useState(false);
const fillRaw = useMotionValue(0);
const fillSpring = useSpring(fillRaw, { stiffness: 12, damping: 14 });
const clipPath = useTransform(fillSpring, (v) => `inset(${100 - v * 100}% 0 0 0)`);
React.useEffect(() => { React.useEffect(() => {
const abortController = new AbortController(); const abortController = new AbortController();
@ -424,7 +255,6 @@ function NavbarGitHubStars({
.then((data) => { .then((data) => {
if (data && typeof data.stargazers_count === "number") { if (data && typeof data.stargazers_count === "number") {
setStars(data.stargazers_count); setStars(data.stargazers_count);
fillRaw.set(1);
} }
}) })
.catch((err) => { .catch((err) => {
@ -434,7 +264,7 @@ function NavbarGitHubStars({
}) })
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
return () => abortController.abort(); return () => abortController.abort();
}, [username, repo, fillRaw]); }, [username, repo]);
return ( return (
<a <a
@ -442,38 +272,17 @@ function NavbarGitHubStars({
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={cn( className={cn(
"group flex items-center gap-2 rounded-full px-3 py-1.5 transition-colors", "group flex items-center gap-1.5 rounded-full px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors",
className className
)} )}
> >
<IconBrandGithub className="h-5 w-5 text-neutral-600 dark:text-neutral-300 shrink-0" /> <IconBrandGithub className="h-5 w-5 text-neutral-600 dark:text-neutral-300 shrink-0" />
<div className="flex items-center gap-1 rounded-md bg-neutral-100 dark:bg-neutral-800 group-hover:bg-neutral-200 dark:group-hover:bg-neutral-700 px-2 py-0.5 transition-colors"> <AnimatedStarCount
<AnimatedStarCount value={isLoading ? 10000 : stars}
value={isLoading ? 10000 : stars} itemSize={ITEM_SIZE}
itemSize={ITEM_SIZE} isRolling={isLoading}
isRolling={isLoading} className="text-sm font-semibold tabular-nums text-neutral-600 dark:text-neutral-400 group-hover:text-neutral-800 dark:group-hover:text-neutral-200 transition-colors"
className="text-sm font-semibold tabular-nums text-neutral-500 dark:text-neutral-400 group-hover:text-neutral-800 dark:group-hover:text-neutral-200 transition-colors" />
onComplete={() => setIsCompleted(true)}
/>
<Particles animate={isCompleted}>
<div className="relative size-4">
<StarIcon
aria-hidden="true"
className="absolute inset-0 size-4 fill-neutral-400 stroke-neutral-400 dark:fill-neutral-700 dark:stroke-neutral-700 group-hover:fill-neutral-600 group-hover:stroke-neutral-600 dark:group-hover:fill-neutral-300 dark:group-hover:stroke-neutral-300 transition-colors"
/>
<motion.div className="absolute inset-0" style={{ clipPath }}>
<StarIcon
aria-hidden="true"
className="size-4 fill-neutral-300 stroke-neutral-300 dark:fill-neutral-400 dark:stroke-neutral-400 group-hover:fill-neutral-500 group-hover:stroke-neutral-500 dark:group-hover:fill-neutral-200 dark:group-hover:stroke-neutral-200 transition-colors"
/>
</motion.div>
</div>
<ParticlesEffect
delay={0.3}
className="size-1 rounded-full bg-neutral-300 dark:bg-neutral-400"
/>
</Particles>
</div>
</a> </a>
); );
} }

View file

@ -262,6 +262,15 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
} }
}, [pendingNewChat, params?.chat_id, router, searchSpaceId, resetCurrentThread]); }, [pendingNewChat, params?.chat_id, router, searchSpaceId, resetCurrentThread]);
// Reset transient slide-out panels when switching search spaces.
// Some browsers can retain overlay/backdrop state across route transitions.
useEffect(() => {
setIsInboxSidebarOpen(false);
setIsAllSharedChatsSidebarOpen(false);
setIsAllPrivateChatsSidebarOpen(false);
setIsAnnouncementsSidebarOpen(false);
}, [searchSpaceId]);
const searchSpaces: SearchSpace[] = useMemo(() => { const searchSpaces: SearchSpace[] = useMemo(() => {
if (!searchSpacesData || !Array.isArray(searchSpacesData)) return []; if (!searchSpacesData || !Array.isArray(searchSpacesData)) return [];
return searchSpacesData.map((space) => ({ return searchSpacesData.map((space) => ({

View file

@ -2,7 +2,6 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { PanelRight, PanelRightClose } from "lucide-react"; import { PanelRight, PanelRightClose } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { startTransition, useEffect } from "react"; import { startTransition, useEffect } from "react";
import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom"; import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms"; import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
@ -99,70 +98,35 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
const targetWidth = PANEL_WIDTHS[effectiveTab]; const targetWidth = PANEL_WIDTHS[effectiveTab];
const collapseButton = <CollapseButton onClick={() => setCollapsed(true)} />; const collapseButton = <CollapseButton onClick={() => setCollapsed(true)} />;
const contentKey = if (!isVisible) return null;
effectiveTab === "sources" && documentsOpen
? "sources"
: effectiveTab === "report" && reportOpen
? "report"
: null;
return ( return (
<AnimatePresence> <aside
{isVisible && ( style={{ width: targetWidth }}
<motion.aside className="flex h-full shrink-0 flex-col border-l bg-background overflow-hidden transition-[width] duration-200 ease-out"
key="right-panel" >
initial={{ width: 0, opacity: 0 }} <div className="relative flex-1 min-h-0 overflow-hidden">
animate={{ width: targetWidth, opacity: 1 }} {effectiveTab === "sources" && documentsOpen && documentsPanel && (
exit={{ width: 0, opacity: 0 }} <div className="h-full">
transition={{ <DocumentsSidebar
width: { type: "spring", stiffness: 400, damping: 35, mass: 0.8 }, open={documentsPanel.open}
opacity: { duration: 0.2, ease: "easeOut" }, onOpenChange={documentsPanel.onOpenChange}
}} embedded
style={{ willChange: "width, opacity", contain: "layout style" }} headerAction={collapseButton}
className="flex h-full shrink-0 flex-col border-l bg-background overflow-hidden" />
>
<div className="relative flex-1 min-h-0 overflow-hidden">
<AnimatePresence mode="popLayout" initial={false}>
{contentKey === "sources" && documentsPanel && (
<motion.div
key="sources"
initial={{ opacity: 0, x: 8 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -8 }}
transition={{ duration: 0.15, ease: "easeOut" }}
className="h-full"
>
<DocumentsSidebar
open={documentsPanel.open}
onOpenChange={documentsPanel.onOpenChange}
embedded
headerAction={collapseButton}
/>
</motion.div>
)}
{contentKey === "report" && (
<motion.div
key="report"
initial={{ opacity: 0, x: 8 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -8 }}
transition={{ duration: 0.15, ease: "easeOut" }}
className="h-full"
>
<div className="flex h-full flex-col">
<ReportPanelContent
reportId={reportState.reportId!}
title={reportState.title || "Report"}
onClose={closeReport}
shareToken={reportState.shareToken}
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div> </div>
</motion.aside> )}
)} {effectiveTab === "report" && reportOpen && (
</AnimatePresence> <div className="h-full">
<ReportPanelContent
reportId={reportState.reportId!}
title={reportState.title || "Report"}
onClose={closeReport}
shareToken={reportState.shareToken}
/>
</div>
)}
</div>
</aside>
); );
} }

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import { motion } from "motion/react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import type { InboxItem } from "@/hooks/use-inbox"; import type { InboxItem } from "@/hooks/use-inbox";
@ -308,17 +307,13 @@ export function LayoutShell({
isResizing={isResizing} isResizing={isResizing}
/> />
<motion.main <main className="flex-1 flex flex-col min-w-0">
layout={isResizing ? false : "position"}
style={{ contain: "inline-size" }}
className="flex-1 flex flex-col min-w-0"
>
<Header /> <Header />
<div className={cn("flex-1", isChatPage ? "overflow-hidden" : "overflow-auto")}> <div className={cn("flex-1", isChatPage ? "overflow-hidden" : "overflow-auto")}>
{children} {children}
</div> </div>
</motion.main> </main>
{/* Right panel — tabbed Sources/Report (desktop only) */} {/* Right panel — tabbed Sources/Report (desktop only) */}
{documentsPanel && ( {documentsPanel && (