mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 08:46:22 +02:00
Merge remote-tracking branch 'upstream/dev' into feature/prompt-library
This commit is contained in:
commit
1aeb5ba645
66 changed files with 4561 additions and 139 deletions
|
|
@ -62,7 +62,7 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
|||
const handleVisibilityChange = (_visibility: ChatVisibility) => {};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 bg-main-panel/95 backdrop-blur supports-backdrop-filter:bg-main-panel/60 px-4">
|
||||
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 bg-main-panel/95 backdrop-blur supports-backdrop-filter:bg-main-panel/60 px-4">
|
||||
{/* Left side - Mobile menu trigger + Model selector */}
|
||||
<div className="flex flex-1 items-center gap-2 min-w-0">
|
||||
{mobileMenuTrigger}
|
||||
|
|
|
|||
|
|
@ -55,14 +55,14 @@ export function RightPanelExpandButton() {
|
|||
if (!collapsed || !hasContent) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 right-4 z-20 flex h-12 items-center">
|
||||
<div className="flex shrink-0 items-center px-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => startTransition(() => setCollapsed(false))}
|
||||
className="h-8 w-8 shrink-0"
|
||||
className="h-7 w-7 shrink-0"
|
||||
>
|
||||
<PanelRight className="h-4 w-4" />
|
||||
<span className="sr-only">Expand panel</span>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,6 @@
|
|||
import { useAtomValue } from "jotai";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
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 { activeTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import type { InboxItem } from "@/hooks/use-inbox";
|
||||
|
|
@ -121,42 +116,34 @@ function MainContentPanel({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
const activeTab = useAtomValue(activeTabAtom);
|
||||
const rightPanelCollapsed = useAtomValue(rightPanelCollapsedAtom);
|
||||
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
|
||||
const reportState = useAtomValue(reportPanelAtom);
|
||||
const editorState = useAtomValue(editorPanelAtom);
|
||||
const hitlEditState = useAtomValue(hitlEditPanelAtom);
|
||||
const isDocumentTab = activeTab?.type === "document";
|
||||
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||
const editorOpen = editorState.isOpen && !!editorState.documentId;
|
||||
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
||||
const showRightPanelExpandButton =
|
||||
rightPanelCollapsed && (documentsOpen || reportOpen || editorOpen || hitlEditOpen);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-1 flex-col rounded-xl border bg-main-panel overflow-hidden min-w-0">
|
||||
<RightPanelExpandButton />
|
||||
<div className="relative flex flex-1 flex-col min-w-0">
|
||||
<TabBar
|
||||
onTabSwitch={onTabSwitch}
|
||||
onNewChat={onNewChat}
|
||||
className={showRightPanelExpandButton ? "pr-14" : undefined}
|
||||
rightActions={<RightPanelExpandButton />}
|
||||
className="min-w-0"
|
||||
/>
|
||||
<Header />
|
||||
<div className="relative flex flex-1 flex-col rounded-xl border bg-main-panel overflow-hidden min-w-0">
|
||||
<Header />
|
||||
|
||||
{isDocumentTab && activeTab.documentId && activeTab.searchSpaceId ? (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<DocumentTabContent
|
||||
key={activeTab.documentId}
|
||||
documentId={activeTab.documentId}
|
||||
searchSpaceId={activeTab.searchSpaceId}
|
||||
title={activeTab.title}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("flex-1", isChatPage ? "overflow-hidden" : "overflow-auto")}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
{isDocumentTab && activeTab.documentId && activeTab.searchSpaceId ? (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<DocumentTabContent
|
||||
key={activeTab.documentId}
|
||||
documentId={activeTab.documentId}
|
||||
searchSpaceId={activeTab.searchSpaceId}
|
||||
title={activeTab.title}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("flex-1", isChatPage ? "overflow-hidden" : "overflow-auto")}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ export function DocumentsSidebar({
|
|||
|
||||
const documentsContent = (
|
||||
<>
|
||||
<div className="shrink-0 flex h-12 items-center px-4">
|
||||
<div className="shrink-0 flex h-14 items-center px-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{isMobile && (
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ function getConnectorTypeDisplayName(connectorType: string): string {
|
|||
CIRCLEBACK_CONNECTOR: "Circleback",
|
||||
MCP_CONNECTOR: "MCP",
|
||||
OBSIDIAN_CONNECTOR: "Obsidian",
|
||||
ONEDRIVE_CONNECTOR: "OneDrive",
|
||||
DROPBOX_CONNECTOR: "Dropbox",
|
||||
TAVILY_API: "Tavily",
|
||||
SEARXNG_API: "SearXNG",
|
||||
LINKUP_API: "Linkup",
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export function Sidebar({
|
|||
>
|
||||
{/* Header - search space name or collapse button when collapsed */}
|
||||
{isCollapsed ? (
|
||||
<div className="flex h-12 shrink-0 items-center justify-center border-b">
|
||||
<div className="flex h-14 shrink-0 items-center justify-center border-b">
|
||||
<SidebarCollapseButton
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={onToggleCollapse ?? (() => {})}
|
||||
|
|
@ -113,7 +113,7 @@ export function Sidebar({
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-12 shrink-0 items-center gap-0 px-1 border-b">
|
||||
<div className="flex h-14 shrink-0 items-center gap-0 px-1 border-b">
|
||||
<SidebarHeader
|
||||
searchSpace={searchSpace}
|
||||
isCollapsed={isCollapsed}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ import { cn } from "@/lib/utils";
|
|||
interface TabBarProps {
|
||||
onTabSwitch?: (tab: Tab) => void;
|
||||
onNewChat?: () => void;
|
||||
rightActions?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TabBar({ onTabSwitch, onNewChat, className }: TabBarProps) {
|
||||
export function TabBar({ onTabSwitch, onNewChat, rightActions, className }: TabBarProps) {
|
||||
const tabs = useAtomValue(tabsAtom);
|
||||
const activeTabId = useAtomValue(activeTabIdAtom);
|
||||
const switchTab = useSetAtom(switchTabAtom);
|
||||
|
|
@ -45,12 +46,25 @@ export function TabBar({ onTabSwitch, onNewChat, className }: TabBarProps) {
|
|||
[closeTab, onTabSwitch]
|
||||
);
|
||||
|
||||
// Scroll active tab into view
|
||||
// Keep active tab visible with minimal scroll shift.
|
||||
useEffect(() => {
|
||||
if (!scrollRef.current || !activeTabId) return;
|
||||
const activeEl = scrollRef.current.querySelector(`[data-tab-id="${activeTabId}"]`);
|
||||
if (activeEl) {
|
||||
activeEl.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
|
||||
const scroller = scrollRef.current;
|
||||
const activeEl = scroller.querySelector<HTMLElement>(`[data-tab-id="${activeTabId}"]`);
|
||||
if (!activeEl) return;
|
||||
|
||||
const viewLeft = scroller.scrollLeft;
|
||||
const viewRight = viewLeft + scroller.clientWidth;
|
||||
const tabLeft = activeEl.offsetLeft;
|
||||
const tabRight = tabLeft + activeEl.offsetWidth;
|
||||
|
||||
if (tabLeft < viewLeft) {
|
||||
scroller.scrollTo({ left: tabLeft, behavior: "smooth" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabRight > viewRight) {
|
||||
scroller.scrollTo({ left: tabRight - scroller.clientWidth, behavior: "smooth" });
|
||||
}
|
||||
}, [activeTabId]);
|
||||
|
||||
|
|
@ -60,13 +74,13 @@ export function TabBar({ onTabSwitch, onNewChat, className }: TabBarProps) {
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-12 items-stretch shrink-0 border-b border-border/35 bg-main-panel",
|
||||
"mb-2 flex h-9 items-center shrink-0 px-1 gap-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex h-full items-stretch flex-1 overflow-x-auto overflow-y-hidden scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
|
||||
className="flex h-full items-center flex-1 gap-0.5 overflow-x-auto overflow-y-hidden scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden py-1"
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === activeTabId;
|
||||
|
|
@ -78,13 +92,13 @@ export function TabBar({ onTabSwitch, onNewChat, className }: TabBarProps) {
|
|||
data-tab-id={tab.id}
|
||||
onClick={() => handleTabClick(tab)}
|
||||
className={cn(
|
||||
"group relative flex h-full w-[170px] items-center self-stretch px-3 min-w-0 overflow-hidden text-sm font-medium border-r border-border/35 transition-colors shrink-0",
|
||||
"group relative flex h-full w-[150px] items-center px-3 min-h-0 overflow-hidden text-[13px] font-medium rounded-lg transition-all duration-150 shrink-0",
|
||||
isActive
|
||||
? "bg-muted/50 text-foreground"
|
||||
: "bg-transparent text-muted-foreground hover:bg-muted/25 hover:text-foreground"
|
||||
? "bg-muted/60 text-foreground"
|
||||
: "bg-transparent text-muted-foreground hover:bg-muted/30 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="block min-w-0 flex-1 truncate text-left transition-[padding-right] duration-150 group-hover:pr-5 group-focus-within:pr-5">
|
||||
<span className="block min-w-0 flex-1 truncate text-left group-hover:pr-5 group-focus-within:pr-5">
|
||||
{tab.title}
|
||||
</span>
|
||||
{/* biome-ignore lint/a11y/useSemanticElements: cannot nest button inside button */}
|
||||
|
|
@ -99,7 +113,7 @@ export function TabBar({ onTabSwitch, onNewChat, className }: TabBarProps) {
|
|||
}
|
||||
}}
|
||||
className={cn(
|
||||
"absolute right-2 top-1/2 -translate-y-1/2 shrink-0 rounded-sm p-0.5 transition-colors",
|
||||
"absolute right-2 top-1/2 -translate-y-1/2 shrink-0 rounded-full p-0.5 transition-all duration-150 hover:bg-muted-foreground/15",
|
||||
isActive
|
||||
? "opacity-0 group-hover:opacity-70 group-focus-within:opacity-70 hover:opacity-100"
|
||||
: "opacity-0 group-hover:opacity-60 group-focus-within:opacity-60 hover:opacity-100!"
|
||||
|
|
@ -110,18 +124,19 @@ export function TabBar({ onTabSwitch, onNewChat, className }: TabBarProps) {
|
|||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
{onNewChat && (
|
||||
<div className="flex h-full items-center px-1.5 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewChat}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-muted/60"
|
||||
title="New Chat"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewChat}
|
||||
className="flex h-6 w-6 items-center justify-center shrink-0 rounded-md text-muted-foreground transition-all duration-150 hover:text-foreground hover:bg-muted/40"
|
||||
title="New Chat"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{rightActions}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue