Compare commits

...

10 Commits

Author SHA1 Message Date
Raunak Bhagat
0cc1bdd0df Saving changes 2025-11-20 21:58:36 -08:00
Raunak Bhagat
065e687c5f Merge branch 'sidebar-fix' into sidebar-mobile 2025-11-20 16:25:30 -08:00
Raunak Bhagat
53cb92b1dd Use icon button instead 2025-11-20 16:21:25 -08:00
Raunak Bhagat
9a4d12249d Merge branch 'main' into sidebar-fix 2025-11-20 16:09:10 -08:00
Raunak Bhagat
6dcea4a894 Remove unused import 2025-11-20 15:52:02 -08:00
Raunak Bhagat
350c72426a Add mobile sidebar folding 2025-11-20 10:58:59 -08:00
Raunak Bhagat
8bf1760b06 Add comment to app-page-layout 2025-11-20 09:00:18 -08:00
Raunak Bhagat
2f64f1c037 Move layouts into their own directory 2025-11-20 08:56:07 -08:00
Raunak Bhagat
5fcc14ff34 Parallelize data fetches 2025-11-20 08:43:39 -08:00
Raunak Bhagat
a64215815d Update hierarchy of sidebar and header 2025-11-20 08:28:46 -08:00
13 changed files with 336 additions and 256 deletions

View File

@@ -1,13 +1,13 @@
import AgentsPage from "@/refresh-pages/AgentsPage";
import * as Layouts from "@/refresh-components/layouts/layouts";
import AppPageLayout from "@/layouts/AppPageLayout";
import { fetchHeaderDataSS } from "@/lib/headers/fetchHeaderDataSS";
export default async function Page() {
const headerData = await fetchHeaderDataSS();
return (
<Layouts.AppPage {...headerData}>
<AppPageLayout {...headerData}>
<AgentsPage />
</Layouts.AppPage>
</AppPageLayout>
);
}

View File

@@ -19,7 +19,6 @@ import { usePopup } from "@/components/admin/connectors/Popup";
import { SEARCH_PARAM_NAMES } from "@/app/chat/services/searchParams";
import { useFederatedConnectors, useFilters, useLlmManager } from "@/lib/hooks";
import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader";
import { FiArrowDown } from "react-icons/fi";
import { OnyxDocument, MinimalOnyxDocument } from "@/lib/search/interfaces";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import Dropzone from "react-dropzone";
@@ -74,16 +73,22 @@ import { Suggestions } from "@/sections/Suggestions";
import OnboardingFlow from "@/refresh-components/onboarding/OnboardingFlow";
import { useOnboardingState } from "@/refresh-components/onboarding/useOnboardingState";
import { OnboardingStep } from "@/refresh-components/onboarding/types";
import AppPageLayout from "@/layouts/AppPageLayout";
import { HeaderData } from "@/lib/headers/fetchHeaderDataSS";
import IconButton from "@/refresh-components/buttons/IconButton";
import SvgChevronDown from "@/icons/chevron-down";
const DEFAULT_CONTEXT_TOKENS = 120_000;
interface ChatPageProps {
documentSidebarInitialWidth?: number;
firstMessage?: string;
headerData: HeaderData;
}
export default function ChatPage({
documentSidebarInitialWidth,
firstMessage,
headerData,
}: ChatPageProps) {
// Performance tracking
// Keeping this here in case we need to track down slow renders in the future
@@ -617,6 +622,29 @@ export default function ChatPage({
setTimeout(() => updateCurrentDocumentSidebarVisible(false), 300);
}, [updateCurrentDocumentSidebarVisible]);
const desktopDocumentSidebar =
retrievalEnabled && !settings?.isMobile ? (
<div
className={cn(
"flex-shrink-0 overflow-hidden transition-all duration-300 ease-in-out",
documentSidebarVisible ? "w-[25rem]" : "w-[0rem]"
)}
>
<div className="h-full w-[25rem]">
<DocumentResults
setPresentingDocument={setPresentingDocument}
modal={false}
closeSidebar={handleDesktopDocumentSidebarClose}
selectedDocuments={selectedDocuments}
toggleDocumentSelection={toggleDocumentSelection}
clearSelectedDocuments={() => setSelectedDocuments([])}
selectedDocumentTokens={0}
maxTokens={maxTokens}
/>
</div>
</div>
) : null;
// Only show the centered hero layout when there is NO project selected
// and there are no messages yet. If a project is selected, prefer a top layout.
const showCenteredHero = currentProjectId === null && showCenteredInput;
@@ -765,193 +793,179 @@ export default function ChatPage({
<FederatedOAuthModal />
<div className="flex flex-row h-full w-full">
<div
ref={masterFlexboxRef}
className="flex h-full w-full overflow-x-hidden"
<div className="flex h-full w-full flex-row-reverse">
{desktopDocumentSidebar}
<AppPageLayout
settings={headerData.settings}
chatSession={headerData.chatSession}
className="flex flex-row h-full w-full"
>
{documentSidebarInitialWidth !== undefined && (
<Dropzone
key={chatSessionId}
onDrop={(acceptedFiles) =>
handleMessageSpecificFileUpload(acceptedFiles)
}
noClick
<div className="flex flex-row h-full w-full">
<div
ref={masterFlexboxRef}
className="flex h-full w-full overflow-x-hidden"
>
{({ getRootProps }) => (
<div
className="h-full w-full relative flex-auto min-w-0"
{...getRootProps()}
{documentSidebarInitialWidth !== undefined && (
<Dropzone
key={chatSessionId}
onDrop={(acceptedFiles) =>
handleMessageSpecificFileUpload(acceptedFiles)
}
noClick
>
<div
onScroll={handleScroll}
className="w-full h-[calc(100dvh-100px)] flex flex-col default-scrollbar overflow-y-auto overflow-x-hidden relative"
ref={scrollableDivRef}
>
<MessagesDisplay
messageHistory={messageHistory}
completeMessageTree={completeMessageTree}
liveAssistant={liveAssistant}
llmManager={llmManager}
deepResearchEnabled={deepResearchEnabled}
currentMessageFiles={currentMessageFiles}
setPresentingDocument={setPresentingDocument}
onSubmit={onSubmit}
onMessageSelection={onMessageSelection}
stopGenerating={stopGenerating}
uncaughtError={uncaughtError}
loadingError={loadingError}
handleResubmitLastMessage={handleResubmitLastMessage}
autoScrollEnabled={autoScrollEnabled}
getContainerHeight={getContainerHeight}
lastMessageRef={lastMessageRef}
endPaddingRef={endPaddingRef}
endDivRef={endDivRef}
hasPerformedInitialScroll={hasPerformedInitialScroll}
chatSessionId={chatSessionId}
enterpriseSettings={enterpriseSettings}
/>
</div>
<div
ref={inputRef}
className={cn(
"absolute pointer-events-none z-10 w-full",
showCenteredHero
? "inset-0"
: currentProjectId !== null && showCenteredInput
? "top-0 left-0 right-0"
: "bottom-0 left-0 right-0 translate-y-0"
)}
>
{!showCenteredInput && aboveHorizon && (
<div className="mx-auto w-fit !pointer-events-none flex sticky justify-center">
<button
onClick={() => clientScrollToBottom()}
className="p-1 pointer-events-auto text-text-03 rounded-2xl bg-background-neutral-02 border border-border mx-auto"
>
<FiArrowDown size={18} />
</button>
</div>
)}
{({ getRootProps }) => (
<div
className={cn(
"pointer-events-auto w-[95%] mx-auto relative text-text-04 justify-center",
showCenteredHero
? "h-full grid grid-rows-[1fr_auto_1fr]"
: "mb-8"
)}
className="h-full w-full relative flex-auto min-w-0"
{...getRootProps()}
>
{currentProjectId == null && showCenteredInput && (
<WelcomeMessage liveAssistant={liveAssistant} />
)}
<div
className={cn(
"flex flex-col items-center justify-center",
showCenteredHero && "row-start-2"
)}
onScroll={handleScroll}
className="w-full h-[calc(100dvh-100px)] flex flex-col default-scrollbar overflow-y-auto overflow-x-hidden relative"
ref={scrollableDivRef}
>
{currentProjectId !== null && projectPanelVisible && (
<ProjectContextPanel
projectTokenCount={projectContextTokenCount}
availableContextTokens={availableContextTokens}
setPresentingDocument={setPresentingDocument}
/>
)}
{(showOnboarding ||
(user?.role !== UserRole.ADMIN &&
!user?.personalization?.name)) &&
currentProjectId === null && (
<OnboardingFlow
handleHideOnboarding={() =>
setShowOnboarding(false)
}
state={onboardingState}
actions={onboardingActions}
llmDescriptors={llmDescriptors}
/>
)}
<ChatInputBar
deepResearchEnabled={deepResearchEnabled}
toggleDeepResearch={toggleDeepResearch}
toggleDocumentSidebar={toggleDocumentSidebar}
filterManager={filterManager}
<MessagesDisplay
messageHistory={messageHistory}
completeMessageTree={completeMessageTree}
liveAssistant={liveAssistant}
llmManager={llmManager}
removeDocs={() => setSelectedDocuments([])}
retrievalEnabled={retrievalEnabled}
selectedDocuments={selectedDocuments}
message={message}
setMessage={setMessage}
stopGenerating={stopGenerating}
onSubmit={handleChatInputSubmit}
chatState={currentChatState}
currentSessionFileTokenCount={
existingChatSessionId
? currentSessionFileTokenCount
: projectContextTokenCount
}
availableContextTokens={availableContextTokens}
selectedAssistant={selectedAssistant || liveAssistant}
handleFileUpload={handleMessageSpecificFileUpload}
textAreaRef={textAreaRef}
deepResearchEnabled={deepResearchEnabled}
currentMessageFiles={currentMessageFiles}
setPresentingDocument={setPresentingDocument}
disabled={
llmManager.hasAnyProvider === false ||
onboardingState.currentStep !==
OnboardingStep.Complete
}
onSubmit={onSubmit}
onMessageSelection={onMessageSelection}
stopGenerating={stopGenerating}
uncaughtError={uncaughtError}
loadingError={loadingError}
handleResubmitLastMessage={handleResubmitLastMessage}
autoScrollEnabled={autoScrollEnabled}
getContainerHeight={getContainerHeight}
lastMessageRef={lastMessageRef}
endPaddingRef={endPaddingRef}
endDivRef={endDivRef}
hasPerformedInitialScroll={hasPerformedInitialScroll}
chatSessionId={chatSessionId}
enterpriseSettings={enterpriseSettings}
/>
</div>
{currentProjectId !== null && (
<div className="transition-all duration-700 ease-out">
<ProjectChatSessionList />
</div>
)}
{liveAssistant.starter_messages &&
liveAssistant.starter_messages.length > 0 &&
messageHistory.length === 0 &&
showCenteredHero && (
<div className="mt-6 row-start-3 max-w-[50rem]">
<Suggestions onSubmit={onSubmit} />
<div
ref={inputRef}
className={cn(
"absolute z-10 w-full",
showCenteredHero
? "inset-0"
: currentProjectId !== null && showCenteredInput
? "top-0 left-0 right-0"
: "bottom-0 left-0 right-0 translate-y-0"
)}
>
{!showCenteredInput && aboveHorizon && (
<div className="mx-auto flex justify-center py-4">
<IconButton
icon={SvgChevronDown}
onClick={() => clientScrollToBottom()}
/>
</div>
)}
</div>
</div>
</div>
)}
</Dropzone>
)}
</div>
<div
className={cn(
"flex-shrink-0 overflow-hidden transition-all duration-300 ease-in-out",
documentSidebarVisible && !settings?.isMobile
? "w-[25rem]"
: "w-[0rem]"
)}
>
<div className="h-full w-[25rem]">
{/* IMPORTANT: this is a memoized component, and it's very important
for performance reasons that this stays true. MAKE SURE that all function
props are wrapped in useCallback. */}
<DocumentResults
setPresentingDocument={setPresentingDocument}
modal={false}
closeSidebar={handleDesktopDocumentSidebarClose}
selectedDocuments={selectedDocuments}
toggleDocumentSelection={toggleDocumentSelection}
clearSelectedDocuments={() => setSelectedDocuments([])}
// TODO (chris): fix
selectedDocumentTokens={0}
maxTokens={maxTokens}
/>
<div
className={cn(
"pointer-events-auto w-[95%] mx-auto relative text-text-04 justify-center",
showCenteredHero
? "h-full grid grid-rows-[1fr_auto_1fr]"
: "mb-8"
)}
>
{currentProjectId == null && showCenteredInput && (
<WelcomeMessage liveAssistant={liveAssistant} />
)}
<div
className={cn(
"flex flex-col items-center justify-center",
showCenteredHero && "row-start-2"
)}
>
{currentProjectId !== null &&
projectPanelVisible && (
<ProjectContextPanel
projectTokenCount={projectContextTokenCount}
availableContextTokens={
availableContextTokens
}
setPresentingDocument={setPresentingDocument}
/>
)}
{(showOnboarding ||
(user?.role !== UserRole.ADMIN &&
!user?.personalization?.name)) &&
currentProjectId === null && (
<OnboardingFlow
handleHideOnboarding={() =>
setShowOnboarding(false)
}
state={onboardingState}
actions={onboardingActions}
llmDescriptors={llmDescriptors}
/>
)}
<ChatInputBar
deepResearchEnabled={deepResearchEnabled}
toggleDeepResearch={toggleDeepResearch}
toggleDocumentSidebar={toggleDocumentSidebar}
filterManager={filterManager}
llmManager={llmManager}
removeDocs={() => setSelectedDocuments([])}
retrievalEnabled={retrievalEnabled}
selectedDocuments={selectedDocuments}
message={message}
setMessage={setMessage}
stopGenerating={stopGenerating}
onSubmit={handleChatInputSubmit}
chatState={currentChatState}
currentSessionFileTokenCount={
existingChatSessionId
? currentSessionFileTokenCount
: projectContextTokenCount
}
availableContextTokens={availableContextTokens}
selectedAssistant={
selectedAssistant || liveAssistant
}
handleFileUpload={handleMessageSpecificFileUpload}
textAreaRef={textAreaRef}
setPresentingDocument={setPresentingDocument}
disabled={
llmManager.hasAnyProvider === false ||
onboardingState.currentStep !==
OnboardingStep.Complete
}
/>
</div>
{currentProjectId !== null && (
<div className="transition-all duration-700 ease-out">
<ProjectChatSessionList />
</div>
)}
{liveAssistant.starter_messages &&
liveAssistant.starter_messages.length > 0 &&
messageHistory.length === 0 &&
showCenteredHero && (
<div className="mt-6 row-start-3 max-w-[50rem]">
<Suggestions onSubmit={onSubmit} />
</div>
)}
</div>
</div>
</div>
)}
</Dropzone>
)}
</div>
</div>
</div>
</AppPageLayout>
</div>
</>
);

View File

@@ -1,16 +1,16 @@
import InputPrompts from "@/app/chat/input-prompts/InputPrompts";
import { fetchHeaderDataSS } from "@/lib/headers/fetchHeaderDataSS";
import * as Layouts from "@/refresh-components/layouts/layouts";
import AppPageLayout from "@/layouts/AppPageLayout";
export default async function InputPromptsPage() {
const headerData = await fetchHeaderDataSS();
return (
<Layouts.AppPage
<AppPageLayout
{...headerData}
className="w-full px-32 py-16 mx-auto container"
>
<InputPrompts />
</Layouts.AppPage>
</AppPageLayout>
);
}

View File

@@ -4,7 +4,6 @@ import { fetchChatData } from "@/lib/chat/fetchChatData";
import { ChatProvider } from "@/refresh-components/contexts/ChatContext";
import { ProjectsProvider } from "./projects/ProjectsContext";
import AppSidebar from "@/sections/sidebar/AppSidebar";
export interface LayoutProps {
children: React.ReactNode;
}
@@ -58,9 +57,9 @@ export default async function Layout({ children }: LayoutProps) {
defaultAssistantId={defaultAssistantId}
>
<ProjectsProvider initialProjects={projects}>
<div className="flex flex-row w-full h-full">
<div className="flex flex-row w-full h-full relative">
<AppSidebar />
{children}
<div className="flex-1">{children}</div>
</div>
</ProjectsProvider>
</ChatProvider>

View File

@@ -3,7 +3,7 @@ import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { cookies } from "next/headers";
import NRFPage from "./NRFPage";
import { NRFPreferencesProvider } from "../../../components/context/NRFPreferencesContext";
import * as Layouts from "@/refresh-components/layouts/layouts";
import AppPageLayout from "@/layouts/AppPageLayout";
import { fetchHeaderDataSS } from "@/lib/headers/fetchHeaderDataSS";
export default async function Page() {
@@ -12,11 +12,11 @@ export default async function Page() {
const headerData = await fetchHeaderDataSS();
return (
<Layouts.AppPage {...headerData} className="h-full w-full">
<AppPageLayout {...headerData} className="h-full w-full">
<InstantSSRAutoRefresh />
<NRFPreferencesProvider>
<NRFPage requestCookies={requestCookies} />
</NRFPreferencesProvider>
</Layouts.AppPage>
</AppPageLayout>
);
}

View File

@@ -1,4 +1,3 @@
import * as Layouts from "@/refresh-components/layouts/layouts";
import ChatPage from "@/app/chat/components/ChatPage";
import { fetchHeaderDataSS } from "@/lib/headers/fetchHeaderDataSS";
import { SEARCH_PARAM_NAMES } from "./services/searchParams";
@@ -13,9 +12,7 @@ export default async function Page(props: PageProps) {
const chatSessionId = searchParams[SEARCH_PARAM_NAMES.CHAT_ID];
const headerData = await fetchHeaderDataSS(chatSessionId);
return (
<Layouts.AppPage {...headerData}>
<ChatPage firstMessage={firstMessage} />
</Layouts.AppPage>
);
// Other pages in `web/src/app/chat` are wrapped with `<AppPageLayout>`.
// `chat/page.tsx` is not because it also needs to handle rendering of the document-sidebar (`web/src/app/chat/components/documentSidebar/DocumentResults.tsx`).
return <ChatPage firstMessage={firstMessage} headerData={headerData} />;
}

View File

@@ -2,7 +2,7 @@ import { fetchSS } from "@/lib/utilsSS";
import { redirect } from "next/navigation";
import { requireAuth } from "@/lib/auth/requireAuth";
import { SharedChatDisplay } from "@/app/chat/shared/[chatId]/SharedChatDisplay";
import * as Layouts from "@/refresh-components/layouts/layouts";
import AppPageLayout from "@/layouts/AppPageLayout";
import { Persona } from "@/app/admin/assistants/interfaces";
import { constructMiniFiedPersona } from "@/lib/assistantIconUtils";
import { fetchHeaderDataSS } from "@/lib/headers/fetchHeaderDataSS";
@@ -43,8 +43,8 @@ export default async function Page(props: PageProps) {
const headerData = await fetchHeaderDataSS();
return (
<Layouts.AppPage {...headerData}>
<AppPageLayout {...headerData}>
<SharedChatDisplay chatSession={chatSession} persona={persona} />
</Layouts.AppPage>
</AppPageLayout>
);
}

View File

@@ -41,7 +41,7 @@ export default function AppProvider({
agents={assistants}
pinnedAgentIds={user?.preferences.pinned_assistants || []}
>
<AppSidebarProvider folded={!!folded}>
<AppSidebarProvider collapsed={!!folded}>
{children}
</AppSidebarProvider>
</AgentsProvider>

View File

@@ -6,7 +6,7 @@ import Text from "@/refresh-components/texts/Text";
import Button from "@/refresh-components/buttons/Button";
import SvgShare from "@/icons/share";
import { CombinedSettings } from "@/app/admin/settings/interfaces";
import { useMemo, useState, useEffect } from "react";
import { useMemo, useState, useEffect, useCallback } from "react";
import ShareChatSessionModal from "@/app/chat/components/modal/ShareChatSessionModal";
import { useChatPageLayout } from "@/app/chat/stores/useChatSessionStore";
import IconButton from "@/refresh-components/buttons/IconButton";
@@ -31,18 +31,32 @@ import { PopoverMenu } from "@/components/ui/popover";
import { PopoverSearchInput } from "@/sections/sidebar/ChatButton";
import SimplePopover from "@/refresh-components/SimplePopover";
import { FOLDED_SIZE } from "@/refresh-components/Logo";
import { useScreenSize } from "@/hooks/useScreenSize";
import { useAppSidebarContext } from "@/refresh-components/contexts/AppSidebarContext";
import SvgSidebar from "@/icons/sidebar";
interface AppPageProps extends React.HtmlHTMLAttributes<HTMLDivElement> {
interface AppPageLayoutProps extends React.HtmlHTMLAttributes<HTMLDivElement> {
settings: CombinedSettings | null;
chatSession: ChatSession | null;
}
export function AppPage({
// AppPageLayout wraps chat pages with the shared header/footer white-labelling chrome.
// It also provides the "Share Chat" and kebab-menu on the right side of the header (for shareable chat pages).
//
// Since this is such a ubiquitous component, it's been moved to its own `layouts` directory.
export default function AppPageLayout({
settings,
chatSession,
className,
...rest
}: AppPageProps) {
}: AppPageLayoutProps) {
const { width } = useScreenSize();
const { collapsed, setCollapsed } = useAppSidebarContext();
const isCompactViewport = width !== undefined ? width < 640 : false; // Tailwind `sm` breakpoint
const handleSidebarButtonClick = useCallback(() => {
setCollapsed((prev) => !prev);
}, [setCollapsed]);
const [showShareModal, setShowShareModal] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [showMoveCustomAgentModal, setShowMoveCustomAgentModal] =
@@ -150,7 +164,7 @@ export function AppPage({
useEffect(() => {
if (!showMoveOptions) {
const popoverItems = [
const items = [
<MenuButton
key="move"
icon={SvgFolderIn}
@@ -167,9 +181,9 @@ export function AppPage({
Delete
</MenuButton>,
];
setPopoverItems(popoverItems);
setPopoverItems(items);
} else {
const popoverItems = [
const items = [
<PopoverSearchInput
key="search"
setShowMoveOptions={setShowMoveOptions}
@@ -185,7 +199,7 @@ export function AppPage({
</MenuButton>
)),
];
setPopoverItems(popoverItems);
setPopoverItems(items);
}
}, [showMoveOptions, filteredProjects]);
@@ -233,7 +247,23 @@ export function AppPage({
<div className="flex flex-col h-full w-full">
{(customHeaderContent || !showCenteredInput) && (
<header className="w-full flex flex-row justify-center items-center py-3 px-4 h-16">
<div className="flex-1" />
<div className="flex-1 flex flex-row items-center">
<IconButton
icon={SvgSidebar}
aria-label={
isCompactViewport
? collapsed
? "Show sidebar"
: "Hide sidebar"
: collapsed
? "Expand sidebar"
: "Collapse sidebar"
}
onClick={handleSidebarButtonClick}
internal
/>
</div>
<div className="flex-1 flex flex-col items-center">
<Text text03>{customHeaderContent}</Text>
</div>

View File

@@ -11,10 +11,12 @@ export interface HeaderData {
export async function fetchHeaderDataSS(
chatSessionId?: string
): Promise<HeaderData> {
const settings = await fetchSettingsSS();
const backendChatSession = chatSessionId
? await fetchBackendChatSessionSS(chatSessionId)
: null;
const [settings, backendChatSession] = await Promise.all([
fetchSettingsSS(),
chatSessionId
? fetchBackendChatSessionSS(chatSessionId)
: Promise.resolve(null),
]);
const chatSession = backendChatSession
? toChatSession(backendChatSession)
: null;

View File

@@ -12,29 +12,31 @@ import React, {
import Cookies from "js-cookie";
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
function setFoldedCookie(folded: boolean) {
const foldedAsString = folded.toString();
Cookies.set(SIDEBAR_TOGGLED_COOKIE_NAME, foldedAsString, { expires: 365 });
function setCollapsedCookie(collapsed: boolean) {
const collapsedAsString = collapsed.toString();
Cookies.set(SIDEBAR_TOGGLED_COOKIE_NAME, collapsedAsString, {
expires: 365,
});
if (typeof window !== "undefined") {
localStorage.setItem(SIDEBAR_TOGGLED_COOKIE_NAME, foldedAsString);
localStorage.setItem(SIDEBAR_TOGGLED_COOKIE_NAME, collapsedAsString);
}
}
export interface AppSidebarProviderProps {
folded: boolean;
collapsed: boolean;
children: ReactNode;
}
export function AppSidebarProvider({
folded: initiallyFolded,
collapsed: initiallyCollapsed,
children,
}: AppSidebarProviderProps) {
const [folded, setFoldedInternal] = useState(initiallyFolded);
const [collapsed, setCollapsedInternal] = useState(initiallyCollapsed);
const setFolded: Dispatch<SetStateAction<boolean>> = (value) => {
setFoldedInternal((prev) => {
const setCollapsed: Dispatch<SetStateAction<boolean>> = (value) => {
setCollapsedInternal((prev) => {
const newState = typeof value === "function" ? value(prev) : value;
setFoldedCookie(newState);
setCollapsedCookie(newState);
return newState;
});
};
@@ -46,7 +48,7 @@ export function AppSidebarProvider({
if (!isModifierPressed || event.key !== "e") return;
event.preventDefault();
setFolded((prev) => !prev);
setCollapsed((prev) => !prev);
}
document.addEventListener("keydown", handleKeyDown);
@@ -58,8 +60,8 @@ export function AppSidebarProvider({
return (
<AppSidebarContext.Provider
value={{
folded,
setFolded,
collapsed,
setCollapsed,
}}
>
{children}
@@ -68,8 +70,8 @@ export function AppSidebarProvider({
}
export interface AppSidebarContextType {
folded: boolean;
setFolded: Dispatch<SetStateAction<boolean>>;
collapsed: boolean;
setCollapsed: Dispatch<SetStateAction<boolean>>;
}
const AppSidebarContext = createContext<AppSidebarContextType | undefined>(

View File

@@ -60,6 +60,7 @@ import { useUser } from "@/components/user/UserProvider";
import SvgSettings from "@/icons/settings";
import { useAppFocus } from "@/lib/hooks";
import { useCreateModal } from "@/refresh-components/contexts/ModalContext";
import { useScreenSize } from "@/hooks/useScreenSize";
// Visible-agents = pinned-agents + current-agent (if current-agent not in pinned-agents)
// OR Visible-agents = pinned-agents (if current-agent in pinned-agents)
@@ -123,7 +124,7 @@ function RecentsSection({ chatSessions }: RecentsSectionProps) {
function AppSidebarInner() {
const route = useAppRouter();
const { pinnedAgents, setPinnedAgents, currentAgent } = useAgentsContext();
const { folded, setFolded } = useAppSidebarContext();
const { collapsed, setCollapsed } = useAppSidebarContext();
const { chatSessions, refreshChatSessions } = useChatContext();
const combinedSettings = useSettingsContext();
const { refreshCurrentProjectDetails, fetchProjects, currentProjectId } =
@@ -317,7 +318,7 @@ function AppSidebarInner() {
<div data-testid="AppSidebar/new-session">
<SidebarTab
leftIcon={SvgEditBig}
folded={folded}
folded={collapsed}
onClick={() => {
if (
combinedSettings?.settings?.disable_default_assistant &&
@@ -336,27 +337,27 @@ function AppSidebarInner() {
</SidebarTab>
</div>
),
[folded, route, activeSidebarTab, combinedSettings, currentAgent]
[collapsed, route, activeSidebarTab, combinedSettings, currentAgent]
);
const moreAgentsButton = useMemo(
() => (
<div data-testid="AppSidebar/more-agents">
<SidebarTab
leftIcon={
folded || visibleAgents.length === 0
collapsed || visibleAgents.length === 0
? SvgOnyxOctagon
: SvgMoreHorizontal
}
href="/chat/agents"
folded={folded}
folded={collapsed}
active={activeSidebarTab === "more-agents"}
lowlight={!folded}
lowlight={!collapsed}
>
{visibleAgents.length === 0 ? "Explore Agents" : "More Agents"}
</SidebarTab>
</div>
),
[folded, activeSidebarTab, visibleAgents]
[collapsed, activeSidebarTab, visibleAgents]
);
const newProjectButton = useMemo(
() => (
@@ -364,13 +365,13 @@ function AppSidebarInner() {
leftIcon={SvgFolderPlus}
onClick={() => createProjectModal.toggle(true)}
active={createProjectModal.isOpen}
folded={folded}
lowlight={!folded}
folded={collapsed}
lowlight={!collapsed}
>
New Project
</SidebarTab>
),
[folded, createProjectModal.toggle, createProjectModal.isOpen]
[collapsed, createProjectModal.toggle, createProjectModal.isOpen]
);
const settingsButton = useMemo(
() => (
@@ -379,15 +380,15 @@ function AppSidebarInner() {
<SidebarTab
href="/admin/indexing/status"
leftIcon={SvgSettings}
folded={folded}
folded={collapsed}
>
{isAdmin ? "Admin Panel" : "Curator Panel"}
</SidebarTab>
)}
<Settings folded={folded} />
<Settings folded={collapsed} />
</div>
),
[folded, isAdmin, isCurator]
[collapsed, isAdmin, isCurator]
);
if (!combinedSettings) {
@@ -434,9 +435,9 @@ function AppSidebarInner() {
/>
)}
<SidebarWrapper folded={folded} setFolded={setFolded}>
<SidebarWrapper collapsed={collapsed} setCollapsed={setCollapsed}>
<SidebarBody footer={settingsButton} actionButton={newSessionButton}>
{folded ? (
{collapsed ? (
<>
{moreAgentsButton}
{newProjectButton}
@@ -501,7 +502,42 @@ function AppSidebarInner() {
);
}
const AppSidebar = memo(AppSidebarInner);
AppSidebar.displayName = "AppSidebar";
const MemoizedAppSidebarInner = memo(AppSidebarInner);
MemoizedAppSidebarInner.displayName = "AppSidebar";
export default AppSidebar;
const MOBILE_SIDEBAR_BREAKPOINT_PX = 640;
export default function AppSidebar() {
const { width } = useScreenSize();
const { collapsed, setCollapsed } = useAppSidebarContext();
const isCompact =
width !== undefined ? width <= MOBILE_SIDEBAR_BREAKPOINT_PX : false;
if (!isCompact) return <MemoizedAppSidebarInner />;
return (
<>
<div
className={cn(
"fixed inset-y-0 left-0 z-50 w-[min(18rem,90vw)] max-w-full bg-background-neutral-00 shadow-03 transition-transform duration-300",
collapsed ? "-translate-x-full" : "translate-x-0"
)}
>
<div className="h-full overflow-y-auto">
<MemoizedAppSidebarInner />
</div>
</div>
{/* Hitbox to close the sidebar if anything outside of it is touched */}
<div
className={cn(
"fixed inset-0 z-40 bg-black/40 transition-opacity duration-200",
collapsed
? "opacity-0 pointer-events-none"
: "opacity-100 pointer-events-auto"
)}
onClick={() => setCollapsed(true)}
/>
</>
);
}

View File

@@ -5,14 +5,14 @@ import SvgSidebar from "@/icons/sidebar";
import Logo from "@/refresh-components/Logo";
interface LogoSectionProps {
folded?: boolean;
setFolded?: Dispatch<SetStateAction<boolean>>;
collapsed?: boolean;
setCollapsed?: Dispatch<SetStateAction<boolean>>;
}
function LogoSection({ folded, setFolded }: LogoSectionProps) {
function LogoSection({ collapsed, setCollapsed }: LogoSectionProps) {
const logo = useCallback(
(className?: string) => <Logo folded={folded} className={className} />,
[folded]
(className?: string) => <Logo folded={collapsed} className={className} />,
[collapsed]
);
const closeButton = useCallback(
(shouldFold: boolean) => (
@@ -20,10 +20,10 @@ function LogoSection({ folded, setFolded }: LogoSectionProps) {
icon={SvgSidebar}
tertiary
tooltip="Close Sidebar"
onClick={() => setFolded?.(shouldFold)}
onClick={() => setCollapsed?.(shouldFold)}
/>
),
[setFolded]
[setCollapsed]
);
return (
@@ -36,12 +36,12 @@ function LogoSection({ folded, setFolded }: LogoSectionProps) {
//
// - @raunakab
"flex flex-row items-center py-1 gap-1 min-h-[3.5rem] px-3.5",
folded ? "justify-start" : "justify-between"
collapsed ? "justify-start" : "justify-between"
)}
>
{folded === undefined ? (
{collapsed === undefined ? (
logo()
) : folded ? (
) : collapsed ? (
<>
<div className="group-hover/SidebarWrapper:hidden">{logo()}</div>
<div className="w-full justify-center hidden group-hover/SidebarWrapper:flex">
@@ -59,14 +59,14 @@ function LogoSection({ folded, setFolded }: LogoSectionProps) {
}
export interface SidebarWrapperProps {
folded?: boolean;
setFolded?: Dispatch<SetStateAction<boolean>>;
collapsed?: boolean;
setCollapsed?: Dispatch<SetStateAction<boolean>>;
children?: React.ReactNode;
}
export default function SidebarWrapper({
folded,
setFolded,
collapsed,
setCollapsed,
children,
}: SidebarWrapperProps) {
return (
@@ -80,10 +80,10 @@ export default function SidebarWrapper({
// @HERE (size of sidebar)
//
// - @raunakab
folded ? "w-[3.25rem]" : "w-[15rem]"
collapsed ? "w-[3.25rem]" : "w-[15rem]"
)}
>
<LogoSection folded={folded} setFolded={setFolded} />
<LogoSection collapsed={collapsed} setCollapsed={setCollapsed} />
{children}
</div>
</div>