mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
Merge pull request #1532 from CREDO23/imporve-artifacts-accessibility
[Feat] Artifacts sidebar for chat deliverables
This commit is contained in:
commit
efa9efc80b
40 changed files with 1306 additions and 43 deletions
|
|
@ -58,6 +58,7 @@ import {
|
|||
DrawerTitle,
|
||||
} from "@/components/ui/drawer";
|
||||
import { DropdownMenuLabel } from "@/components/ui/dropdown-menu";
|
||||
import { withArtifactAnchor } from "@/features/chat-artifacts";
|
||||
import { useComments } from "@/hooks/use-comments";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
|
|
@ -433,12 +434,12 @@ const MessageInfoDropdown: FC<{ chatTurnId: string | null | undefined }> = ({ ch
|
|||
* body and is picked up by the timeline instead.
|
||||
*/
|
||||
const BODY_TOOLS = {
|
||||
generate_report: GenerateReportToolUI,
|
||||
generate_resume: GenerateResumeToolUI,
|
||||
generate_podcast: GeneratePodcastToolUI,
|
||||
generate_video_presentation: GenerateVideoPresentationToolUI,
|
||||
display_image: GenerateImageToolUI,
|
||||
generate_image: GenerateImageToolUI,
|
||||
generate_report: withArtifactAnchor(GenerateReportToolUI),
|
||||
generate_resume: withArtifactAnchor(GenerateResumeToolUI),
|
||||
generate_podcast: withArtifactAnchor(GeneratePodcastToolUI),
|
||||
generate_video_presentation: withArtifactAnchor(GenerateVideoPresentationToolUI),
|
||||
display_image: withArtifactAnchor(GenerateImageToolUI),
|
||||
generate_image: withArtifactAnchor(GenerateImageToolUI),
|
||||
} as const;
|
||||
|
||||
const NullBodyTool: ToolCallMessagePartComponent = () => null;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { AlarmClock, AlertTriangle, Inbox, LibraryBig } from "lucide-react";
|
||||
import { AlarmClock, AlertTriangle, Boxes, Inbox, LibraryBig } from "lucide-react";
|
||||
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTheme } from "next-themes";
|
||||
|
|
@ -328,6 +328,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
// in the sidebar (also surfaced in the icon rail's collapsed mode via this
|
||||
// list). Announcements has been moved to the avatar dropdown.
|
||||
const isAutomationsActive = pathname?.includes("/automations") === true;
|
||||
const isArtifactsActive = pathname?.endsWith("/artifacts") === true;
|
||||
const navItems: NavItem[] = useMemo(
|
||||
() =>
|
||||
(
|
||||
|
|
@ -345,6 +346,12 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
icon: AlarmClock,
|
||||
isActive: isAutomationsActive,
|
||||
},
|
||||
{
|
||||
title: "Artifacts",
|
||||
url: `/dashboard/${searchSpaceId}/artifacts`,
|
||||
icon: Boxes,
|
||||
isActive: isArtifactsActive,
|
||||
},
|
||||
isMobile
|
||||
? {
|
||||
title: "Documents",
|
||||
|
|
@ -362,6 +369,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
totalUnreadCount,
|
||||
searchSpaceId,
|
||||
isAutomationsActive,
|
||||
isArtifactsActive,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-quer
|
|||
import { activeTabAtom } from "@/atoms/tabs/tabs.atom";
|
||||
import { ActionLogButton } from "@/components/agent-action-log/action-log-button";
|
||||
import { ChatShareButton } from "@/components/new-chat/chat-share-button";
|
||||
import { ArtifactsToggleButton } from "@/features/chat-artifacts";
|
||||
import type { ThreadRecord } from "@/lib/chat/thread-persistence";
|
||||
|
||||
interface HeaderProps {
|
||||
|
|
@ -71,6 +72,7 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
|||
{/* Right side - Actions */}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{hasThread && <ActionLogButton threadId={currentThreadState.id} />}
|
||||
{hasThread && <ArtifactsToggleButton />}
|
||||
{threadForButton && <ChatShareButton thread={threadForButton} />}
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,14 @@ import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel
|
|||
import { citationPanelAtom, closeCitationPanelAtom } from "@/atoms/citation/citation-panel.atom";
|
||||
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
|
||||
import {
|
||||
type RightPanelTab,
|
||||
rightPanelCollapsedAtom,
|
||||
rightPanelTabAtom,
|
||||
} from "@/atoms/layout/right-panel.atom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { artifactsPanelOpenAtom, closeArtifactsPanelAtom } from "@/features/chat-artifacts";
|
||||
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/features/chat-messages/hitl";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DocumentsSidebar } from "../sidebar";
|
||||
|
|
@ -47,6 +52,14 @@ const ReportPanelContent = dynamic(
|
|||
{ ssr: false, loading: () => null }
|
||||
);
|
||||
|
||||
const ArtifactsPanelContent = dynamic(
|
||||
() =>
|
||||
import("@/features/chat-artifacts").then((m) => ({
|
||||
default: m.ArtifactsPanelContent,
|
||||
})),
|
||||
{ ssr: false, loading: () => null }
|
||||
);
|
||||
|
||||
interface RightPanelProps {
|
||||
documentsPanel?: {
|
||||
open: boolean;
|
||||
|
|
@ -100,6 +113,7 @@ export function RightPanelToggleButton({
|
|||
const editorState = useAtomValue(editorPanelAtom);
|
||||
const hitlEditState = useAtomValue(hitlEditPanelAtom);
|
||||
const citationState = useAtomValue(citationPanelAtom);
|
||||
const artifactsOpen = useAtomValue(artifactsPanelOpenAtom);
|
||||
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||
const editorOpen =
|
||||
editorState.isOpen &&
|
||||
|
|
@ -110,7 +124,8 @@ export function RightPanelToggleButton({
|
|||
: !!editorState.localFilePath);
|
||||
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
||||
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
||||
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
|
||||
const hasContent =
|
||||
documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen;
|
||||
const label = collapsed ? "Expand panel" : "Collapse panel";
|
||||
|
||||
if (!hasContent) return null;
|
||||
|
|
@ -152,6 +167,7 @@ export function RightPanelExpandButton() {
|
|||
const editorState = useAtomValue(editorPanelAtom);
|
||||
const hitlEditState = useAtomValue(hitlEditPanelAtom);
|
||||
const citationState = useAtomValue(citationPanelAtom);
|
||||
const artifactsOpen = useAtomValue(artifactsPanelOpenAtom);
|
||||
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||
const editorOpen =
|
||||
editorState.isOpen &&
|
||||
|
|
@ -162,7 +178,8 @@ export function RightPanelExpandButton() {
|
|||
: !!editorState.localFilePath);
|
||||
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
||||
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
||||
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
|
||||
const hasContent =
|
||||
documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen;
|
||||
|
||||
if (!collapsed || !hasContent) return null;
|
||||
|
||||
|
|
@ -179,8 +196,31 @@ const PANEL_WIDTHS = {
|
|||
editor: 640,
|
||||
"hitl-edit": 640,
|
||||
citation: 560,
|
||||
artifacts: 420,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Priority order used to fall back to another open surface when the active
|
||||
* tab's content closes. Artifacts sit just above the always-available sources
|
||||
* tab.
|
||||
*/
|
||||
const TAB_FALLBACK_ORDER: RightPanelTab[] = [
|
||||
"hitl-edit",
|
||||
"citation",
|
||||
"editor",
|
||||
"report",
|
||||
"artifacts",
|
||||
"sources",
|
||||
];
|
||||
|
||||
function resolveEffectiveTab(
|
||||
activeTab: RightPanelTab,
|
||||
openByTab: Record<RightPanelTab, boolean>
|
||||
): RightPanelTab {
|
||||
if (openByTab[activeTab]) return activeTab;
|
||||
return TAB_FALLBACK_ORDER.find((tab) => openByTab[tab]) ?? "sources";
|
||||
}
|
||||
|
||||
export function RightPanel({
|
||||
documentsPanel,
|
||||
showCollapseButton = true,
|
||||
|
|
@ -195,6 +235,8 @@ export function RightPanel({
|
|||
const closeHitlEdit = useSetAtom(closeHitlEditPanelAtom);
|
||||
const citationState = useAtomValue(citationPanelAtom);
|
||||
const closeCitation = useSetAtom(closeCitationPanelAtom);
|
||||
const artifactsOpen = useAtomValue(artifactsPanelOpenAtom);
|
||||
const closeArtifacts = useSetAtom(closeArtifactsPanelAtom);
|
||||
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
|
||||
|
||||
const documentsOpen = documentsPanel?.open ?? false;
|
||||
|
|
@ -210,13 +252,14 @@ export function RightPanel({
|
|||
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!reportOpen && !editorOpen && !hitlEditOpen && !citationOpen) return;
|
||||
if (!reportOpen && !editorOpen && !hitlEditOpen && !citationOpen && !artifactsOpen) return;
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
if (hitlEditOpen) closeHitlEdit();
|
||||
else if (citationOpen) closeCitation();
|
||||
else if (editorOpen) closeEditor();
|
||||
else if (reportOpen) closeReport();
|
||||
else if (artifactsOpen) closeArtifacts();
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
|
@ -226,41 +269,26 @@ export function RightPanel({
|
|||
editorOpen,
|
||||
hitlEditOpen,
|
||||
citationOpen,
|
||||
artifactsOpen,
|
||||
closeReport,
|
||||
closeEditor,
|
||||
closeHitlEdit,
|
||||
closeCitation,
|
||||
closeArtifacts,
|
||||
]);
|
||||
|
||||
const isVisible =
|
||||
(documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen) && !collapsed;
|
||||
(documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen) &&
|
||||
!collapsed;
|
||||
|
||||
let effectiveTab = activeTab;
|
||||
if (effectiveTab === "hitl-edit" && !hitlEditOpen) {
|
||||
effectiveTab = citationOpen
|
||||
? "citation"
|
||||
: editorOpen
|
||||
? "editor"
|
||||
: reportOpen
|
||||
? "report"
|
||||
: "sources";
|
||||
} else if (effectiveTab === "citation" && !citationOpen) {
|
||||
effectiveTab = editorOpen ? "editor" : reportOpen ? "report" : "sources";
|
||||
} else if (effectiveTab === "editor" && !editorOpen) {
|
||||
effectiveTab = citationOpen ? "citation" : reportOpen ? "report" : "sources";
|
||||
} else if (effectiveTab === "report" && !reportOpen) {
|
||||
effectiveTab = citationOpen ? "citation" : editorOpen ? "editor" : "sources";
|
||||
} else if (effectiveTab === "sources" && !documentsOpen) {
|
||||
effectiveTab = hitlEditOpen
|
||||
? "hitl-edit"
|
||||
: citationOpen
|
||||
? "citation"
|
||||
: editorOpen
|
||||
? "editor"
|
||||
: reportOpen
|
||||
? "report"
|
||||
: "sources";
|
||||
}
|
||||
const effectiveTab = resolveEffectiveTab(activeTab, {
|
||||
sources: documentsOpen,
|
||||
report: reportOpen,
|
||||
editor: editorOpen,
|
||||
"hitl-edit": hitlEditOpen,
|
||||
citation: citationOpen,
|
||||
artifacts: artifactsOpen,
|
||||
});
|
||||
|
||||
const targetWidth = PANEL_WIDTHS[effectiveTab];
|
||||
const collapseButton = showCollapseButton ? (
|
||||
|
|
@ -329,6 +357,11 @@ export function RightPanel({
|
|||
<CitationPanelContent chunkId={citationState.chunkId} onClose={closeCitation} />
|
||||
</div>
|
||||
)}
|
||||
{effectiveTab === "artifacts" && artifactsOpen && (
|
||||
<div className="h-full flex flex-col">
|
||||
<ArtifactsPanelContent onClose={closeArtifacts} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -145,6 +145,10 @@ export function Sidebar({
|
|||
() => navItems.find((item) => item.url.endsWith("/automations")),
|
||||
[navItems]
|
||||
);
|
||||
const artifactsItem = useMemo(
|
||||
() => navItems.find((item) => item.url.endsWith("/artifacts")),
|
||||
[navItems]
|
||||
);
|
||||
const documentsItem = useMemo(
|
||||
() => navItems.find((item) => item.url === "#documents"),
|
||||
[navItems]
|
||||
|
|
@ -153,7 +157,10 @@ export function Sidebar({
|
|||
() =>
|
||||
navItems.filter(
|
||||
(item) =>
|
||||
item.url !== "#inbox" && item.url !== "#documents" && !item.url.endsWith("/automations")
|
||||
item.url !== "#inbox" &&
|
||||
item.url !== "#documents" &&
|
||||
!item.url.endsWith("/automations") &&
|
||||
!item.url.endsWith("/artifacts")
|
||||
),
|
||||
[navItems]
|
||||
);
|
||||
|
|
@ -242,6 +249,16 @@ export function Sidebar({
|
|||
tooltipContent={isCollapsed ? automationsItem.title : undefined}
|
||||
/>
|
||||
)}
|
||||
{artifactsItem && (
|
||||
<SidebarButton
|
||||
icon={artifactsItem.icon}
|
||||
label={artifactsItem.title}
|
||||
onClick={() => onNavItemClick?.(artifactsItem)}
|
||||
isCollapsed={isCollapsed}
|
||||
isActive={artifactsItem.isActive}
|
||||
tooltipContent={isCollapsed ? artifactsItem.title : undefined}
|
||||
/>
|
||||
)}
|
||||
{documentsItem && (
|
||||
<SidebarButton
|
||||
icon={documentsItem.icon}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,6 @@ export function CombinedPlayer({ slides }: CombinedPlayerProps) {
|
|||
compositionHeight={1080}
|
||||
style={{ width: "100%", aspectRatio: "16/9" }}
|
||||
controls
|
||||
autoPlay
|
||||
loop
|
||||
acknowledgeRemotionLicense
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -485,7 +485,7 @@ function VideoPresentationPlayer({
|
|||
);
|
||||
}
|
||||
|
||||
function StatusPoller({
|
||||
export function StatusPoller({
|
||||
presentationId,
|
||||
title,
|
||||
shareToken,
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
export { GenerateVideoPresentationToolUI } from "./generate-video-presentation";
|
||||
export {
|
||||
GenerateVideoPresentationToolUI,
|
||||
StatusPoller as VideoPresentationViewer,
|
||||
} from "./generate-video-presentation";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue