mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-12 22:42:13 +02:00
feat: implement RightPanel component for tabbed navigation between Sources and Report; update report panel handling in dashboard chat page for improved user experience
This commit is contained in:
parent
8b468e06da
commit
414dceff2f
9 changed files with 259 additions and 87 deletions
|
|
@ -32,7 +32,7 @@ import { closeReportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
||||||
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
||||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||||
import { Thread } from "@/components/assistant-ui/thread";
|
import { Thread } from "@/components/assistant-ui/thread";
|
||||||
import { ReportPanel } from "@/components/report-panel/report-panel";
|
import { MobileReportPanel } from "@/components/report-panel/report-panel";
|
||||||
import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
|
import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
|
||||||
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
|
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
|
||||||
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
||||||
|
|
@ -1668,7 +1668,7 @@ export default function NewChatPage() {
|
||||||
<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>
|
||||||
<ReportPanel />
|
<MobileReportPanel />
|
||||||
</div>
|
</div>
|
||||||
</AssistantRuntimeProvider>
|
</AssistantRuntimeProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||||
|
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
|
||||||
|
|
||||||
interface ReportPanelState {
|
interface ReportPanelState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -44,15 +45,14 @@ export const openReportPanelAtom = atom(
|
||||||
wordCount: wordCount ?? null,
|
wordCount: wordCount ?? null,
|
||||||
shareToken: shareToken ?? null,
|
shareToken: shareToken ?? null,
|
||||||
});
|
});
|
||||||
set(documentsSidebarOpenAtom, false);
|
set(rightPanelTabAtom, "report");
|
||||||
|
set(rightPanelCollapsedAtom, false);
|
||||||
|
set(documentsSidebarOpenAtom, true);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Action atom to close the report panel */
|
/** Action atom to close the report panel */
|
||||||
export const closeReportPanelAtom = atom(null, (get, set) => {
|
export const closeReportPanelAtom = atom(null, (_get, set) => {
|
||||||
const wasOpen = get(reportPanelAtom).isOpen;
|
|
||||||
set(reportPanelAtom, initialState);
|
set(reportPanelAtom, initialState);
|
||||||
if (wasOpen) {
|
set(rightPanelTabAtom, "sources");
|
||||||
set(documentsSidebarOpenAtom, true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
8
surfsense_web/atoms/layout/right-panel.atom.ts
Normal file
8
surfsense_web/atoms/layout/right-panel.atom.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export type RightPanelTab = "sources" | "report";
|
||||||
|
|
||||||
|
export const rightPanelTabAtom = atom<RightPanelTab>("sources");
|
||||||
|
|
||||||
|
/** Whether the right panel is collapsed (hidden but state preserved) */
|
||||||
|
export const rightPanelCollapsedAtom = atom(false);
|
||||||
|
|
@ -22,7 +22,6 @@ import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
SquareIcon,
|
SquareIcon,
|
||||||
SquareLibrary,
|
|
||||||
Unplug,
|
Unplug,
|
||||||
Upload,
|
Upload,
|
||||||
X,
|
X,
|
||||||
|
|
@ -37,7 +36,6 @@ import {
|
||||||
} from "@/atoms/chat/mentioned-documents.atom";
|
} from "@/atoms/chat/mentioned-documents.atom";
|
||||||
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
||||||
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
|
|
||||||
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||||
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
||||||
import {
|
import {
|
||||||
|
|
@ -77,7 +75,6 @@ import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||||
|
|
@ -570,12 +567,6 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
const { openDialog: openUploadDialog } = useDocumentUploadDialog();
|
const { openDialog: openUploadDialog } = useDocumentUploadDialog();
|
||||||
const { data: connectors } = useAtomValue(connectorsAtom);
|
const { data: connectors } = useAtomValue(connectorsAtom);
|
||||||
const connectorCount = connectors?.length ?? 0;
|
const connectorCount = connectors?.length ?? 0;
|
||||||
const { data: typeCounts } = useAtomValue(documentTypeCountsAtom);
|
|
||||||
const totalDocuments = useMemo(
|
|
||||||
() => (typeCounts ? Object.values(typeCounts).reduce((sum, n) => sum + n, 0) : 0),
|
|
||||||
[typeCounts]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isComposerTextEmpty = useAssistantState(({ composer }) => {
|
const isComposerTextEmpty = useAssistantState(({ composer }) => {
|
||||||
const text = composer.text?.trim() || "";
|
const text = composer.text?.trim() || "";
|
||||||
return text.length === 0;
|
return text.length === 0;
|
||||||
|
|
@ -631,18 +622,6 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
<Upload className="size-4 shrink-0" />
|
<Upload className="size-4 shrink-0" />
|
||||||
Upload files
|
Upload files
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{totalDocuments > 0 && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setAddMenuOpen(false);
|
|
||||||
setDocumentsSidebarOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SquareLibrary className="size-4 shrink-0" />
|
|
||||||
Manage Documents
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAddMenuOpen(false);
|
setAddMenuOpen(false);
|
||||||
|
|
@ -650,7 +629,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Unplug className="size-4 shrink-0" />
|
<Unplug className="size-4 shrink-0" />
|
||||||
{connectorCount > 0 ? "Manage connectors" : "Connect your tools"}
|
{connectorCount > 0 ? "Manage tools" : "Connect your tools"}
|
||||||
{connectorCount > 0 && (
|
{connectorCount > 0 && (
|
||||||
<span className="ml-auto text-xs text-muted-foreground">{connectorCount}</span>
|
<span className="ml-auto text-xs text-muted-foreground">{connectorCount}</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
|
import { PanelRight } from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
||||||
|
import { reportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
||||||
|
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||||
|
import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
|
||||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||||
import { ChatHeader } from "@/components/new-chat/chat-header";
|
import { ChatHeader } from "@/components/new-chat/chat-header";
|
||||||
import { ChatShareButton } from "@/components/new-chat/chat-share-button";
|
import { ChatShareButton } from "@/components/new-chat/chat-share-button";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import type { ChatVisibility, ThreadRecord } from "@/lib/chat/thread-persistence";
|
import type { ChatVisibility, ThreadRecord } from "@/lib/chat/thread-persistence";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
|
|
@ -15,6 +22,7 @@ interface HeaderProps {
|
||||||
export function Header({ mobileMenuTrigger }: HeaderProps) {
|
export function Header({ mobileMenuTrigger }: HeaderProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const isChatPage = pathname?.includes("/new-chat") ?? false;
|
const isChatPage = pathname?.includes("/new-chat") ?? false;
|
||||||
|
|
||||||
|
|
@ -38,6 +46,13 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
||||||
|
|
||||||
const handleVisibilityChange = (_visibility: ChatVisibility) => {};
|
const handleVisibilityChange = (_visibility: ChatVisibility) => {};
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
|
||||||
|
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
|
||||||
|
const reportState = useAtomValue(reportPanelAtom);
|
||||||
|
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||||
|
const hasRightPanelContent = documentsOpen || reportOpen;
|
||||||
|
const showExpandButton = !isMobile && collapsed && hasRightPanelContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-4">
|
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-4">
|
||||||
{/* Left side - Mobile menu trigger + Model selector */}
|
{/* Left side - Mobile menu trigger + Model selector */}
|
||||||
|
|
@ -49,10 +64,26 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Actions */}
|
{/* Right side - Actions */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-2">
|
||||||
{hasThread && (
|
{hasThread && (
|
||||||
<ChatShareButton thread={threadForButton} onVisibilityChange={handleVisibilityChange} />
|
<ChatShareButton thread={threadForButton} onVisibilityChange={handleVisibilityChange} />
|
||||||
)}
|
)}
|
||||||
|
{showExpandButton && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setCollapsed(false)}
|
||||||
|
className="h-8 w-8 shrink-0"
|
||||||
|
>
|
||||||
|
<PanelRight className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Expand panel</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">Expand panel</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
129
surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
Normal file
129
surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
|
import { PanelRight, PanelRightClose } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
||||||
|
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||||
|
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
|
||||||
|
import { ReportPanelContent } from "@/components/report-panel/report-panel";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { DocumentsSidebar } from "../sidebar";
|
||||||
|
|
||||||
|
interface RightPanelProps {
|
||||||
|
documentsPanel?: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapseButton({ onClick }: { onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" onClick={onClick} className="h-8 w-8 shrink-0">
|
||||||
|
<PanelRightClose className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Collapse panel</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="left">Collapse panel</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolutely positioned expand button — renders at top-right of the main
|
||||||
|
* container so it occupies the same screen position as the collapse button
|
||||||
|
* inside the Documents header.
|
||||||
|
*/
|
||||||
|
export function RightPanelExpandButton() {
|
||||||
|
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
|
||||||
|
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
|
||||||
|
const reportState = useAtomValue(reportPanelAtom);
|
||||||
|
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||||
|
const hasContent = documentsOpen || reportOpen;
|
||||||
|
|
||||||
|
if (!collapsed || !hasContent) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute top-4 right-4 z-20">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setCollapsed(false)}
|
||||||
|
className="h-8 w-8 shrink-0"
|
||||||
|
>
|
||||||
|
<PanelRight className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Expand panel</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="left">Expand panel</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RightPanel({ documentsPanel }: RightPanelProps) {
|
||||||
|
const [activeTab] = useAtom(rightPanelTabAtom);
|
||||||
|
const reportState = useAtomValue(reportPanelAtom);
|
||||||
|
const closeReport = useSetAtom(closeReportPanelAtom);
|
||||||
|
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
|
||||||
|
|
||||||
|
const documentsOpen = documentsPanel?.open ?? false;
|
||||||
|
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||||
|
|
||||||
|
// Close report on Escape key (works on Windows, macOS, and Linux)
|
||||||
|
// Must be called before early returns to satisfy Rules of Hooks.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!reportOpen) return;
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeReport();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [reportOpen, closeReport]);
|
||||||
|
|
||||||
|
if (!documentsOpen && !reportOpen) return null;
|
||||||
|
if (collapsed) return null;
|
||||||
|
|
||||||
|
const effectiveTab =
|
||||||
|
activeTab === "report" && !reportOpen
|
||||||
|
? "sources"
|
||||||
|
: activeTab === "sources" && !documentsOpen
|
||||||
|
? "report"
|
||||||
|
: activeTab;
|
||||||
|
|
||||||
|
const collapseButton = <CollapseButton onClick={() => setCollapsed(true)} />;
|
||||||
|
|
||||||
|
const panelWidth = effectiveTab === "sources" ? "w-[420px]" : "w-[640px]";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className={`flex h-full ${panelWidth} shrink-0 flex-col border-l bg-background animate-in slide-in-from-right-4 duration-200 ease-out transition-[width] `}>
|
||||||
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
|
{effectiveTab === "sources" && documentsOpen && documentsPanel && (
|
||||||
|
<DocumentsSidebar
|
||||||
|
open={documentsPanel.open}
|
||||||
|
onOpenChange={documentsPanel.onOpenChange}
|
||||||
|
embedded
|
||||||
|
headerAction={collapseButton}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{effectiveTab === "report" && reportOpen && (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<ReportPanelContent
|
||||||
|
reportId={reportState.reportId!}
|
||||||
|
title={reportState.title || "Report"}
|
||||||
|
onClose={closeReport}
|
||||||
|
shareToken={reportState.shareToken}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import { useSidebarResize } from "../../hooks/useSidebarResize";
|
||||||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||||
import { Header } from "../header";
|
import { Header } from "../header";
|
||||||
import { IconRail } from "../icon-rail";
|
import { IconRail } from "../icon-rail";
|
||||||
|
import { RightPanel } from "../right-panel/RightPanel";
|
||||||
import {
|
import {
|
||||||
AllPrivateChatsSidebar,
|
AllPrivateChatsSidebar,
|
||||||
AllSharedChatsSidebar,
|
AllSharedChatsSidebar,
|
||||||
|
|
@ -275,46 +276,36 @@ export function LayoutShell({
|
||||||
|
|
||||||
{/* Main container with sidebar and content - relative for inbox positioning */}
|
{/* Main container with sidebar and content - relative for inbox positioning */}
|
||||||
<div className="relative flex flex-1 rounded-xl border bg-background overflow-hidden">
|
<div className="relative flex flex-1 rounded-xl border bg-background overflow-hidden">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
searchSpace={searchSpace}
|
searchSpace={searchSpace}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
onToggleCollapse={toggleCollapsed}
|
onToggleCollapse={toggleCollapsed}
|
||||||
navItems={navItems}
|
navItems={navItems}
|
||||||
onNavItemClick={onNavItemClick}
|
onNavItemClick={onNavItemClick}
|
||||||
chats={chats}
|
chats={chats}
|
||||||
sharedChats={sharedChats}
|
sharedChats={sharedChats}
|
||||||
activeChatId={activeChatId}
|
activeChatId={activeChatId}
|
||||||
onNewChat={onNewChat}
|
onNewChat={onNewChat}
|
||||||
onChatSelect={onChatSelect}
|
onChatSelect={onChatSelect}
|
||||||
onChatRename={onChatRename}
|
onChatRename={onChatRename}
|
||||||
onChatDelete={onChatDelete}
|
onChatDelete={onChatDelete}
|
||||||
onChatArchive={onChatArchive}
|
onChatArchive={onChatArchive}
|
||||||
onViewAllSharedChats={onViewAllSharedChats}
|
onViewAllSharedChats={onViewAllSharedChats}
|
||||||
onViewAllPrivateChats={onViewAllPrivateChats}
|
onViewAllPrivateChats={onViewAllPrivateChats}
|
||||||
user={user}
|
user={user}
|
||||||
onSettings={onSettings}
|
onSettings={onSettings}
|
||||||
onManageMembers={onManageMembers}
|
onManageMembers={onManageMembers}
|
||||||
onUserSettings={onUserSettings}
|
onUserSettings={onUserSettings}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
pageUsage={pageUsage}
|
pageUsage={pageUsage}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
setTheme={setTheme}
|
setTheme={setTheme}
|
||||||
className="hidden md:flex border-r shrink-0"
|
className="hidden md:flex border-r shrink-0"
|
||||||
isLoadingChats={isLoadingChats}
|
isLoadingChats={isLoadingChats}
|
||||||
sidebarWidth={sidebarWidth}
|
sidebarWidth={sidebarWidth}
|
||||||
onResizeMouseDown={onResizeMouseDown}
|
onResizeMouseDown={onResizeMouseDown}
|
||||||
isResizing={isResizing}
|
isResizing={isResizing}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Docked Documents Sidebar - renders as flex sibling between sidebar and content */}
|
|
||||||
{documentsPanel?.isDocked && (
|
|
||||||
<DocumentsSidebar
|
|
||||||
open={documentsPanel.open}
|
|
||||||
onOpenChange={documentsPanel.onOpenChange}
|
|
||||||
isDocked={documentsPanel.isDocked}
|
|
||||||
onDockedChange={documentsPanel.onDockedChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<main className="flex-1 flex flex-col min-w-0">
|
<main className="flex-1 flex flex-col min-w-0">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
@ -324,6 +315,16 @@ export function LayoutShell({
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* Right panel — tabbed Sources/Report (desktop only) */}
|
||||||
|
{documentsPanel && (
|
||||||
|
<RightPanel
|
||||||
|
documentsPanel={{
|
||||||
|
open: documentsPanel.open,
|
||||||
|
onOpenChange: documentsPanel.onOpenChange,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Inbox Sidebar - slide-out panel */}
|
{/* Inbox Sidebar - slide-out panel */}
|
||||||
{inbox && (
|
{inbox && (
|
||||||
<InboxSidebar
|
<InboxSidebar
|
||||||
|
|
@ -335,16 +336,6 @@ export function LayoutShell({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Documents Sidebar - floating slide-out panel (non-docked mode) */}
|
|
||||||
{documentsPanel && !documentsPanel.isDocked && (
|
|
||||||
<DocumentsSidebar
|
|
||||||
open={documentsPanel.open}
|
|
||||||
onOpenChange={documentsPanel.onOpenChange}
|
|
||||||
isDocked={false}
|
|
||||||
onDockedChange={documentsPanel.onDockedChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Announcements Sidebar */}
|
{/* Announcements Sidebar */}
|
||||||
{announcementsPanel && (
|
{announcementsPanel && (
|
||||||
<AnnouncementsSidebar
|
<AnnouncementsSidebar
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,11 @@ const SHOWCASE_CONNECTORS = [
|
||||||
{ type: "GOOGLE_GMAIL_CONNECTOR", label: "Gmail" },
|
{ type: "GOOGLE_GMAIL_CONNECTOR", label: "Gmail" },
|
||||||
{ type: "NOTION_CONNECTOR", label: "Notion" },
|
{ type: "NOTION_CONNECTOR", label: "Notion" },
|
||||||
{ type: "YOUTUBE_CONNECTOR", label: "YouTube" },
|
{ type: "YOUTUBE_CONNECTOR", label: "YouTube" },
|
||||||
|
{ type: "GOOGLE_CALENDAR_CONNECTOR", label: "Google Calendar" },
|
||||||
{ type: "SLACK_CONNECTOR", label: "Slack" },
|
{ type: "SLACK_CONNECTOR", label: "Slack" },
|
||||||
|
{ type: "LINEAR_CONNECTOR", label: "Linear" },
|
||||||
|
{ type: "JIRA_CONNECTOR", label: "Jira" },
|
||||||
|
{ type: "GITHUB_CONNECTOR", label: "GitHub" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
interface DocumentsSidebarProps {
|
interface DocumentsSidebarProps {
|
||||||
|
|
@ -38,6 +42,10 @@ interface DocumentsSidebarProps {
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
isDocked?: boolean;
|
isDocked?: boolean;
|
||||||
onDockedChange?: (docked: boolean) => void;
|
onDockedChange?: (docked: boolean) => void;
|
||||||
|
/** When true, renders content without any wrapper — parent provides the container */
|
||||||
|
embedded?: boolean;
|
||||||
|
/** Optional action element rendered in the header row (e.g. collapse button) */
|
||||||
|
headerAction?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DocumentsSidebar({
|
export function DocumentsSidebar({
|
||||||
|
|
@ -45,6 +53,8 @@ export function DocumentsSidebar({
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
isDocked = false,
|
isDocked = false,
|
||||||
onDockedChange,
|
onDockedChange,
|
||||||
|
embedded = false,
|
||||||
|
headerAction,
|
||||||
}: DocumentsSidebarProps) {
|
}: DocumentsSidebarProps) {
|
||||||
const t = useTranslations("documents");
|
const t = useTranslations("documents");
|
||||||
const tSidebar = useTranslations("sidebar");
|
const tSidebar = useTranslations("sidebar");
|
||||||
|
|
@ -168,8 +178,8 @@ export function DocumentsSidebar({
|
||||||
|
|
||||||
const documentsContent = (
|
const documentsContent = (
|
||||||
<>
|
<>
|
||||||
<div className="shrink-0 p-4 pb-10">
|
<div className="shrink-0 flex h-14 items-center px-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -214,12 +224,13 @@ export function DocumentsSidebar({
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
{headerAction}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Connected tools strip */}
|
{/* Connected tools strip */}
|
||||||
<div className="shrink-0 mx-4 mb-3 flex select-none items-center gap-2 rounded-lg border bg-muted/50 px-3 py-2">
|
<div className="shrink-0 mx-4 mt-2 mb-3 flex select-none items-center gap-2 rounded-lg border bg-muted/50 px-3 py-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setConnectorDialogOpen(true)}
|
onClick={() => setConnectorDialogOpen(true)}
|
||||||
|
|
@ -278,6 +289,14 @@ export function DocumentsSidebar({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (embedded) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col bg-sidebar text-sidebar-foreground">
|
||||||
|
{documentsContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isDocked && open && !isMobile) {
|
if (isDocked && open && !isMobile) {
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,9 @@ function ReportPanelSkeleton() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inner content component used by both desktop panel and mobile drawer
|
* Inner content component used by desktop panel, mobile drawer, and the layout right panel
|
||||||
*/
|
*/
|
||||||
function ReportPanelContent({
|
export function ReportPanelContent({
|
||||||
reportId,
|
reportId,
|
||||||
title,
|
title,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
@ -579,3 +579,18 @@ export function ReportPanel() {
|
||||||
|
|
||||||
return <MobileReportDrawer />;
|
return <MobileReportDrawer />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MobileReportPanel — mobile-only report drawer
|
||||||
|
*
|
||||||
|
* Used in the dashboard chat page where the desktop report is handled
|
||||||
|
* by the layout-level RightPanel instead.
|
||||||
|
*/
|
||||||
|
export function MobileReportPanel() {
|
||||||
|
const panelState = useAtomValue(reportPanelAtom);
|
||||||
|
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||||
|
|
||||||
|
if (isDesktop || !panelState.isOpen || !panelState.reportId) return null;
|
||||||
|
|
||||||
|
return <MobileReportDrawer />;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue