refactor: update document processing status handling and improve sidebar components

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-03-31 21:29:46 -07:00
parent d8f403efba
commit 0201fd319d
7 changed files with 141 additions and 49 deletions

View file

@ -3,18 +3,13 @@
import { useAtomValue } from "jotai";
import { usePathname } from "next/navigation";
import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
import { hitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { reportPanelAtom } from "@/atoms/chat/report-panel.atom";
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
import { editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { activeTabAtom, tabsAtom } from "@/atoms/tabs/tabs.atom";
import { ChatHeader } from "@/components/new-chat/chat-header";
import { ChatShareButton } from "@/components/new-chat/chat-share-button";
import { useIsMobile } from "@/hooks/use-mobile";
import type { ChatVisibility, ThreadRecord } from "@/lib/chat/thread-persistence";
import { cn } from "@/lib/utils";
import { RightPanelExpandButton } from "../right-panel/RightPanel";
interface HeaderProps {
mobileMenuTrigger?: React.ReactNode;
@ -26,19 +21,9 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
const isMobile = useIsMobile();
const activeTab = useAtomValue(activeTabAtom);
const tabs = useAtomValue(tabsAtom);
const collapsed = useAtomValue(rightPanelCollapsedAtom);
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
const reportState = useAtomValue(reportPanelAtom);
const editorState = useAtomValue(editorPanelAtom);
const hitlEditState = useAtomValue(hitlEditPanelAtom);
const isChatPage = pathname?.includes("/new-chat") ?? false;
const isDocumentTab = activeTab?.type === "document";
const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen = editorState.isOpen && !!editorState.documentId;
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
const showExpandButton =
!isMobile && collapsed && (documentsOpen || reportOpen || editorOpen || hitlEditOpen);
const hasTabBar = tabs.length > 1;
const currentThreadState = useAtomValue(currentThreadAtom);
@ -72,12 +57,11 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
</div>
{/* Right side - Actions */}
<div
className={cn("ml-auto flex items-center gap-2", showExpandButton && !hasTabBar && "mr-10")}
>
<div className="ml-auto flex items-center gap-2">
{hasThread && (
<ChatShareButton thread={threadForButton} onVisibilityChange={handleVisibilityChange} />
)}
{!isMobile && !hasTabBar && <RightPanelExpandButton />}
</div>
</header>
);

View file

@ -21,7 +21,6 @@ import type { DocumentNodeDoc } from "@/components/documents/DocumentNode";
import type { FolderDisplay } from "@/components/documents/FolderNode";
import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog";
import { FolderTreeView } from "@/components/documents/FolderTreeView";
import { MarkdownViewer } from "@/components/markdown-viewer";
import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems";
import {
AlertDialog,

View file

@ -1,6 +1,6 @@
"use client";
import { CheckCircle2, CircleAlert } from "lucide-react";
import { CheckCircle2, CircleAlert, RefreshCw } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils";
import type { NavItem } from "../../types/layout.types";
@ -12,6 +12,46 @@ interface NavSectionProps {
isCollapsed?: boolean;
}
function getStatusInfo(status: NavItem["statusIndicator"]) {
switch (status) {
case "processing":
return {
tooltip: "New or updated documents are still being prepared for search.",
};
case "background_sync":
return {
pillLabel: "Background sync",
tooltip:
"Periodic sync is checking for updates in the background. Existing documents stay searchable while this runs.",
};
case "success":
return {
tooltip: "All document updates are fully synced.",
};
case "error":
return {
pillLabel: "Needs attention",
tooltip: "Some documents failed to sync. Open Documents or Inbox for details.",
};
default:
return {};
}
}
function StatusPill({ status }: { status: NavItem["statusIndicator"] }) {
const { pillLabel } = getStatusInfo(status);
if (!pillLabel) {
return null;
}
return (
<span className="inline-flex items-center rounded-full border border-border/60 bg-background/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
{pillLabel}
</span>
);
}
function StatusBadge({ status }: { status: NavItem["statusIndicator"] }) {
if (status === "processing") {
return (
@ -20,6 +60,13 @@ function StatusBadge({ status }: { status: NavItem["statusIndicator"] }) {
</span>
);
}
if (status === "background_sync") {
return (
<span className="absolute top-0.5 right-0.5 inline-flex items-center justify-center h-[14px] w-[14px] rounded-full bg-primary/15">
<RefreshCw className="h-[9px] w-[9px] text-primary animate-[spin_3s_linear_infinite]" />
</span>
);
}
if (status === "success") {
return (
<span className="absolute top-0.5 right-0.5 inline-flex items-center justify-center h-[14px] w-[14px] rounded-full bg-emerald-500/15 animate-in fade-in duration-300">
@ -49,6 +96,16 @@ function StatusIcon({
if (status === "processing") {
return <Spinner size="sm" className={cn("shrink-0 text-primary", className)} />;
}
if (status === "background_sync") {
return (
<RefreshCw
className={cn(
"shrink-0 text-primary animate-[spin_3s_linear_infinite]",
className
)}
/>
);
}
if (status === "success") {
return (
<CheckCircle2
@ -89,6 +146,7 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
item.title === "Inbox" || item.title.toLowerCase().includes("inbox")
? { "data-joyride": "inbox-sidebar" as const }
: {};
const { tooltip } = getStatusInfo(item.statusIndicator);
return (
<SidebarButton
@ -107,6 +165,8 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
className="h-4 w-4"
/>
}
trailingContent={<StatusPill status={item.statusIndicator} />}
tooltipContent={tooltip}
buttonProps={joyrideAttr}
/>
);

View file

@ -16,6 +16,10 @@ interface SidebarButtonProps {
collapsedOverlay?: React.ReactNode;
/** Custom icon node for expanded mode — overrides the default <Icon> rendering */
expandedIconNode?: React.ReactNode;
/** Optional inline trailing content shown in expanded mode */
trailingContent?: React.ReactNode;
/** Optional tooltip content that replaces the default label tooltip */
tooltipContent?: React.ReactNode;
className?: string;
/** Extra attributes spread onto the inner <button> (e.g. data-joyride) */
buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
@ -42,6 +46,8 @@ export function SidebarButton({
badge,
collapsedOverlay,
expandedIconNode,
trailingContent,
tooltipContent,
className,
buttonProps,
}: SidebarButtonProps) {
@ -62,15 +68,19 @@ export function SidebarButton({
<span className="sr-only">{label}</span>
</button>
</TooltipTrigger>
<TooltipContent side="right">
{label}
{typeof badge === "string" && ` (${badge})`}
<TooltipContent side="right" className="max-w-xs">
{tooltipContent ?? (
<>
{label}
{typeof badge === "string" && ` (${badge})`}
</>
)}
</TooltipContent>
</Tooltip>
);
}
return (
const button = (
<button
type="button"
onClick={onClick}
@ -79,6 +89,7 @@ export function SidebarButton({
>
{expandedIconNode ?? <Icon className="h-4 w-4 shrink-0" />}
<span className="flex-1 truncate">{label}</span>
{trailingContent}
{badge && typeof badge !== "string" ? badge : null}
{badge && typeof badge === "string" ? (
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
@ -87,4 +98,17 @@ export function SidebarButton({
) : null}
</button>
);
if (!tooltipContent) {
return button;
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
{tooltipContent}
</TooltipContent>
</Tooltip>
);
}