diff --git a/front/components/assistant/AgentMessageMarkdown.integration.test.tsx b/front/components/assistant/AgentMessageMarkdown.integration.test.tsx index 120a6a18da81..8e66e3f2f4b4 100644 --- a/front/components/assistant/AgentMessageMarkdown.integration.test.tsx +++ b/front/components/assistant/AgentMessageMarkdown.integration.test.tsx @@ -1,8 +1,9 @@ +import { ConversationSidePanelContext } from "@app/components/assistant/conversation/ConversationSidePanelContext"; import { FilePreviewProvider } from "@app/components/assistant/conversation/FilePreviewContext"; import { getFilePreviewMarkdownDirective } from "@app/lib/markdown/file_preview"; import { LightWorkspaceFactory } from "@app/tests/utils/LightWorkspaceFactory"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { AgentMessageMarkdown } from "./AgentMessageMarkdown"; @@ -112,15 +113,32 @@ describe("AgentMessageMarkdown - Integration Tests", () => { }); it("renders scoped file preview directives as previewable files", async () => { + const openPanel = vi.fn(); const { container } = render( - - - + + + + + ); const fileLink = screen.getByRole("button", { name: "booklet.pdf" }); @@ -131,10 +149,12 @@ describe("AgentMessageMarkdown - Integration Tests", () => { fireEvent.click(fileLink); - expect(await screen.findByRole("dialog")).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "Download" }) - ).toBeInTheDocument(); + await waitFor(() => { + expect(openPanel).toHaveBeenCalledWith({ + type: "file_preview", + filePath: "conversation-c1/booklet.pdf", + }); + }); }); it("hides incomplete text directives while content is streaming", async () => { diff --git a/front/components/assistant/conversation/ConversationLayout.tsx b/front/components/assistant/conversation/ConversationLayout.tsx index efb20630183e..f0723cc8186c 100644 --- a/front/components/assistant/conversation/ConversationLayout.tsx +++ b/front/components/assistant/conversation/ConversationLayout.tsx @@ -95,8 +95,8 @@ const ConversationLayoutContent = ({ return ( - - + + {children} - - + + {shouldDisplayWelcomeTourGuide && ( { ); expect(probe.panel.data).toBe("msg_1@act_1"); }); + + it("addresses path-less files by id without colliding with paths", () => { + const probe = renderProvider(); + + act(() => probe.panel.openPanel({ type: "file_preview", fileId: "fil_1" })); + expect(probe.panel.data).toBe("id:fil_1"); + + act(() => + probe.panel.openPanel({ type: "file_preview", filePath: "a.md" }) + ); + expect(probe.panel.data).toBe("a.md"); + }); }); describe("ConversationSidePanelProvider toggle", () => { diff --git a/front/components/assistant/conversation/ConversationSidePanelContext.tsx b/front/components/assistant/conversation/ConversationSidePanelContext.tsx index 996970705391..3bc0cc722033 100644 --- a/front/components/assistant/conversation/ConversationSidePanelContext.tsx +++ b/front/components/assistant/conversation/ConversationSidePanelContext.tsx @@ -33,6 +33,12 @@ type OpenPanelParams = | { type: "file_preview"; filePath: string; + fileId?: undefined; + } + | { + type: "file_preview"; + fileId: string; + filePath?: undefined; } | { type: "files"; @@ -91,6 +97,26 @@ export function useConversationSidePanelContext() { return context; } +const FILE_PREVIEW_FILE_ID_PREFIX = "id:"; + +export function encodeFilePreviewFileId(fileId: string): string { + return `${FILE_PREVIEW_FILE_ID_PREFIX}${fileId}`; +} + +// Content fragments without a sandbox path are addressed by id instead. +export function parseFilePreviewData(data?: string): { + fileId?: string; + filePath?: string; +} { + if (!data) { + return {}; + } + + return data.startsWith(FILE_PREVIEW_FILE_ID_PREFIX) + ? { fileId: data.slice(FILE_PREVIEW_FILE_ID_PREFIX.length) } + : { filePath: data }; +} + export function parseDataAsMessageIdAndActionId(data?: string): { messageId?: string; actionId?: string; @@ -118,7 +144,7 @@ function getPanelData(params: OpenPanelParams): string { : params.fileId; case FILE_PREVIEW_SIDE_PANEL_TYPE: - return params.filePath; + return params.filePath ?? encodeFilePreviewFileId(params.fileId); case FILES_SIDE_PANEL_TYPE: return "files"; diff --git a/front/components/assistant/conversation/FilePreviewContext.tsx b/front/components/assistant/conversation/FilePreviewContext.tsx index bff152096554..b5829176f9a8 100644 --- a/front/components/assistant/conversation/FilePreviewContext.tsx +++ b/front/components/assistant/conversation/FilePreviewContext.tsx @@ -1,21 +1,21 @@ -import { FilePreviewDialog } from "@app/components/file_explorer/FilePreviewDialog"; -import type { FileEntry } from "@app/components/file_explorer/types"; +import { ConversationSidePanelContext } from "@app/components/assistant/conversation/ConversationSidePanelContext"; import { isFilePreviewableContentType } from "@app/components/file_explorer/utils"; +import { useSendNotification } from "@app/hooks/useNotification"; import { fetchFileIdFromPath, getFileDownloadUrl, getFilePathDownloadUrl, - getFilePathViewUrl, - getFileViewUrl, } from "@app/lib/swr/files"; +import { normalizeError } from "@app/types/shared/utils/error_utils"; import type { LightWorkspaceType } from "@app/types/user"; import type { ReactNode } from "react"; import { createContext, useCallback, useContext, + useEffect, useMemo, - useState, + useRef, } from "react"; interface PreviewableFile { @@ -25,14 +25,21 @@ interface PreviewableFile { contentType: string; } +interface FrameFile { + fileId?: string | null; + filePath?: string; +} + type FilePreviewContextType = { + canPreview: boolean; openFilePreview: (file: PreviewableFile) => void; - resolveFileIdFromPath: (filePath: string) => Promise; + openFramePreview: (frame: FrameFile) => Promise; }; const FilePreviewContext = createContext({ + canPreview: false, openFilePreview: () => {}, - resolveFileIdFromPath: () => Promise.resolve(null), + openFramePreview: () => Promise.resolve(), }); interface FilePreviewProviderProps { @@ -44,19 +51,32 @@ export function FilePreviewProvider({ owner, children, }: FilePreviewProviderProps) { - const [previewState, setPreviewState] = useState<{ - entry: FileEntry; - fileUrl: string; - downloadUrl: string; - } | null>(null); + const sidePanel = useContext(ConversationSidePanelContext); + const sendNotification = useSendNotification(); + const canPreview = sidePanel != null; + + // The side panel context value changes on every panel navigation. Reading it + // through a ref keeps the callbacks below stable, so citations do not all + // re-render each time the panel switches. + const sidePanelRef = useRef(sidePanel); + useEffect(() => { + sidePanelRef.current = sidePanel; + }); const openFilePreview = useCallback( (file: PreviewableFile) => { - const fileUrl = file.filePath - ? getFilePathViewUrl(owner, file.filePath) - : file.fileId - ? getFileViewUrl(owner, file.fileId) - : null; + const panel = sidePanelRef.current; + + if (isFilePreviewableContentType(file.contentType) && panel) { + if (file.filePath) { + panel.openPanel({ type: "file_preview", filePath: file.filePath }); + return; + } + if (file.fileId) { + panel.openPanel({ type: "file_preview", fileId: file.fileId }); + return; + } + } const downloadUrl = file.filePath ? getFilePathDownloadUrl(owner, file.filePath) @@ -64,67 +84,47 @@ export function FilePreviewProvider({ ? getFileDownloadUrl(owner, file.fileId) : null; - if (!fileUrl || !downloadUrl) { - return; - } - - if (!isFilePreviewableContentType(file.contentType)) { + if (downloadUrl) { window.open(downloadUrl, "_blank"); - return; } - - setPreviewState({ - entry: { - kind: "file", - isDirectory: false, - fileName: file.title, - path: file.filePath ?? file.title, - contentType: file.contentType, - fileId: file.fileId ?? null, - thumbnailUrl: null, - sizeBytes: 0, - // No mtime here: stands in to cache-bust the per-URL cached PDF - // conversion, so an edited file stops rendering its stale one. - lastModifiedMs: Date.now(), - }, - fileUrl, - downloadUrl, - }); }, [owner] ); - const resolveFileIdFromPath = useCallback( - (filePath: string) => fetchFileIdFromPath({ owner, filePath }), - [owner] - ); + const openFramePreview = useCallback( + async ({ fileId, filePath }: FrameFile) => { + try { + const resolvedFileId = + fileId ?? + (filePath ? await fetchFileIdFromPath({ owner, filePath }) : null); - const handleDownload = useCallback(async () => { - if (previewState?.downloadUrl) { - window.open(previewState.downloadUrl, "_blank"); - } - }, [previewState?.downloadUrl]); + if (!resolvedFileId) { + throw new Error("No linked file was found for this Frame."); + } + + sidePanelRef.current?.openPanel({ + type: "interactive_content", + fileId: resolvedFileId, + }); + } catch (error) { + sendNotification({ + type: "error", + title: "Failed to open Frame", + description: normalizeError(error).message, + }); + } + }, + [owner, sendNotification] + ); const contextValue = useMemo( - () => ({ openFilePreview, resolveFileIdFromPath }), - [openFilePreview, resolveFileIdFromPath] + () => ({ canPreview, openFilePreview, openFramePreview }), + [canPreview, openFilePreview, openFramePreview] ); return ( {children} - { - if (!open) { - setPreviewState(null); - } - }} - onDownload={handleDownload} - owner={owner} - /> ); } diff --git a/front/components/assistant/conversation/attachment/AttachmentCitation.tsx b/front/components/assistant/conversation/attachment/AttachmentCitation.tsx index ea7d52195cf5..c0e04ccfa5c7 100644 --- a/front/components/assistant/conversation/attachment/AttachmentCitation.tsx +++ b/front/components/assistant/conversation/attachment/AttachmentCitation.tsx @@ -3,10 +3,9 @@ import { FileCitationCard } from "@app/components/assistant/conversation/attachm import { PreviewableCitation } from "@app/components/assistant/conversation/attachment/PreviewableCitation"; import type { AttachmentCitation } from "@app/components/assistant/conversation/attachment/types"; import { isAudioContentType } from "@app/components/assistant/conversation/attachment/utils"; -import { ConversationSidePanelContext } from "@app/components/assistant/conversation/ConversationSidePanelContext"; -import { isFrameContentType, opensInSidePanel } from "@app/types/files"; +import { useFilePreviewContext } from "@app/components/assistant/conversation/FilePreviewContext"; +import { isFrameContentType } from "@app/types/files"; import { Icon, useTranscribingProgress } from "@dust-tt/sparkle"; -import { useContext } from "react"; interface AttachmentCitationProps { attachmentCitation: AttachmentCitation; @@ -17,7 +16,7 @@ export function AttachmentCitation({ attachmentCitation, size = "md", }: AttachmentCitationProps) { - const sidePanel = useContext(ConversationSidePanelContext); + const { canPreview, openFramePreview } = useFilePreviewContext(); const isLoading = attachmentCitation.type === "file" && attachmentCitation.isUploading; @@ -75,43 +74,14 @@ export function AttachmentCitation({ // Interactive content (spreadsheets etc.): open side panel instead of preview dialog. // Path-backed interactive citations are handled by PreviewableCitation below. - if ( - fileId && - !isLoading && - isFrameContentType(contentType) && - sidePanel != null - ) { + if (fileId && !isLoading && isFrameContentType(contentType) && canPreview) { return ( - sidePanel.openPanel({ type: "interactive_content", fileId }) - } - onRemove={attachmentCitation.onRemove} - tooltipLabel={title} - /> - ); - } - - // Some formats (e.g. presentations) open the resizable side panel instead of - // the center preview dialog. Requires a file path (the preview conversion is - // only served on the path-based route) and the side panel provider. - if ( - filePath && - !isLoading && - opensInSidePanel(contentType) && - sidePanel != null - ) { - return ( - sidePanel.openPanel({ type: "file_preview", filePath })} + onClick={() => openFramePreview({ fileId })} onRemove={attachmentCitation.onRemove} tooltipLabel={title} /> diff --git a/front/components/assistant/conversation/attachment/PreviewableCitation.tsx b/front/components/assistant/conversation/attachment/PreviewableCitation.tsx index 15ab02c4b9e2..a0e5b70c4309 100644 --- a/front/components/assistant/conversation/attachment/PreviewableCitation.tsx +++ b/front/components/assistant/conversation/attachment/PreviewableCitation.tsx @@ -3,15 +3,12 @@ import type { FileCitationCardSize, } from "@app/components/assistant/conversation/attachment/FileCitationCard"; import { FileCitationCard } from "@app/components/assistant/conversation/attachment/FileCitationCard"; -import { ConversationSidePanelContext } from "@app/components/assistant/conversation/ConversationSidePanelContext"; import { useFilePreviewContext } from "@app/components/assistant/conversation/FilePreviewContext"; -import { useSendNotification } from "@app/hooks/useNotification"; import { getFileTypeIcon } from "@app/lib/file_icon_utils"; import { isFrameContentType, isSupportedImageContentType, } from "@app/types/files"; -import { normalizeError } from "@app/types/shared/utils/error_utils"; import { Citation, CitationImage, @@ -20,7 +17,6 @@ import { Tooltip, } from "@dust-tt/sparkle"; import type React from "react"; -import { useContext } from "react"; interface PreviewableCitationProps { containerClassName?: string; @@ -59,31 +55,11 @@ export function PreviewableCitation({ tooltipLabel, variant = "card", }: PreviewableCitationProps) { - const { openFilePreview, resolveFileIdFromPath } = useFilePreviewContext(); - const sendNotification = useSendNotification(); - const sidePanel = useContext(ConversationSidePanelContext); + const { openFilePreview, openFramePreview } = useFilePreviewContext(); const handleClick = async () => { - if (isFrameContentType(contentType) && sidePanel) { - try { - const resolvedFileId = - fileId ?? (filePath ? await resolveFileIdFromPath(filePath) : null); - - if (!resolvedFileId) { - throw new Error("No linked file was found for this Frame."); - } - - sidePanel.openPanel({ - type: "interactive_content", - fileId: resolvedFileId, - }); - } catch (error) { - sendNotification({ - type: "error", - title: "Failed to open Frame", - description: normalizeError(error).message, - }); - } + if (isFrameContentType(contentType)) { + await openFramePreview({ fileId, filePath }); return; } diff --git a/front/components/assistant/conversation/files_panel/ConversationFileExplorer.tsx b/front/components/assistant/conversation/files_panel/ConversationFileExplorer.tsx index d855e61ca283..a250eafe94af 100644 --- a/front/components/assistant/conversation/files_panel/ConversationFileExplorer.tsx +++ b/front/components/assistant/conversation/files_panel/ConversationFileExplorer.tsx @@ -9,7 +9,10 @@ import type { FileExplorerVirtualScopeRoot, } from "@app/components/file_explorer/types"; import { useFileExplorerDownload } from "@app/components/file_explorer/useFileExplorerDownload"; -import { withVirtualExplorerPath } from "@app/components/file_explorer/utils"; +import { + isFilePreviewableContentType, + withVirtualExplorerPath, +} from "@app/components/file_explorer/utils"; import { EditPodFileTabDialog } from "@app/components/pod/files/EditPodFileTabDialog"; import { AppLayoutTitle } from "@app/components/sparkle/AppLayoutTitle"; import { useConversationSandboxFiles } from "@app/hooks/conversations/useConversationSandboxFiles"; @@ -26,7 +29,6 @@ import { usePodFiles } from "@app/lib/swr/pods"; import { useSpaceInfo } from "@app/lib/swr/spaces"; import type { ConversationWithoutContentType } from "@app/types/assistant/conversation"; import { isPodConversation } from "@app/types/assistant/conversation"; -import { opensInSidePanel } from "@app/types/files"; import type { PodFileTab } from "@app/types/pod_file_tab"; import { DEFAULT_POD_FILE_TAB_ICON, @@ -208,7 +210,7 @@ export function ConversationFileExplorer({ const onOpenInPanel = useCallback( (entry: FileEntry): boolean => { - if (opensInSidePanel(entry.contentType)) { + if (isFilePreviewableContentType(entry.contentType)) { openPanel({ type: "file_preview", filePath: entry.path }); return true; } diff --git a/front/components/assistant/conversation/files_panel/ConversationFilesPanel.tsx b/front/components/assistant/conversation/files_panel/ConversationFilesPanel.tsx index fec226a8f239..3d55b6ad6b83 100644 --- a/front/components/assistant/conversation/files_panel/ConversationFilesPanel.tsx +++ b/front/components/assistant/conversation/files_panel/ConversationFilesPanel.tsx @@ -14,7 +14,7 @@ import { isFileAttachmentType } from "@app/lib/api/assistant/conversation/attach import { downloadFile } from "@app/lib/swr/files"; import type { FileSystemFileEntry } from "@app/types/api/file_system/types"; import type { ConversationWithoutContentType } from "@app/types/assistant/conversation"; -import { isFrameContentType, opensInSidePanel } from "@app/types/files"; +import { isFrameContentType } from "@app/types/files"; import type { LightWorkspaceType } from "@app/types/user"; import { Button, @@ -71,11 +71,6 @@ export function ConversationFilesPanel({ }) => { if (isFrameContentType(contentType)) { openPanel({ type: "interactive_content", fileId }); - } else if (opensInSidePanel(contentType) && filePath) { - // Some formats (e.g. presentations) open in the resizable right panel - // like frames, rather than the cramped file preview modal. Preview - // relies on the path-based conversion route, so a file path is required. - openPanel({ type: "file_preview", filePath }); } else { openFilePreview({ fileId, diff --git a/front/components/assistant/conversation/files_panel/FilePreviewPanel.tsx b/front/components/assistant/conversation/files_panel/FilePreviewPanel.tsx index e2abba5455ae..71dcae57a698 100644 --- a/front/components/assistant/conversation/files_panel/FilePreviewPanel.tsx +++ b/front/components/assistant/conversation/files_panel/FilePreviewPanel.tsx @@ -1,19 +1,31 @@ -import { useConversationSidePanelContext } from "@app/components/assistant/conversation/ConversationSidePanelContext"; +import { + parseFilePreviewData, + useConversationSidePanelContext, +} from "@app/components/assistant/conversation/ConversationSidePanelContext"; import { ConversationSidePanelHeader } from "@app/components/assistant/conversation/ConversationSidePanelHeader"; import { CenteredState } from "@app/components/assistant/conversation/interactive_content/CenteredState"; import { FilePreviewContent, + MAX_CSV_ROWS, useFilePreviewContent, } from "@app/components/file_explorer/FilePreviewContent"; +import { MarkdownFilePreviewViewModeSwitch } from "@app/components/file_explorer/MarkdownFilePreview"; import type { FileEntry } from "@app/components/file_explorer/types"; +import { useMarkdownFileEditor } from "@app/components/file_explorer/useMarkdownFileEditor"; import { useConversationSandboxFiles } from "@app/hooks/conversations/useConversationSandboxFiles"; import { getFileTypeIcon } from "@app/lib/file_icon_utils"; -import { getFilePathDownloadUrl, getFilePathViewUrl } from "@app/lib/swr/files"; +import { + getFileDownloadUrl, + getFilePathDownloadUrl, + getFilePathViewUrl, + getFileViewUrl, + useFileMetadata, +} from "@app/lib/swr/files"; import type { FileSystemFileEntry } from "@app/types/api/file_system/types"; import type { ConversationWithoutContentType } from "@app/types/assistant/conversation"; import { contentTypeFromFileName } from "@app/types/files"; import type { LightWorkspaceType } from "@app/types/user"; -import { Button, Download01, Icon } from "@dust-tt/sparkle"; +import { Button, cn, Download01, Icon } from "@dust-tt/sparkle"; interface FilePreviewPanelProps { conversation: ConversationWithoutContentType; @@ -24,7 +36,8 @@ export function FilePreviewPanel({ conversation, owner, }: FilePreviewPanelProps) { - const { data: filePath, closePanel } = useConversationSidePanelContext(); + const { data, closePanel } = useConversationSidePanelContext(); + const { fileId, filePath } = parseFilePreviewData(data); // The conversion preview is cached (Cache-Control: max-age) per URL, so we // bust it with the file's lastModifiedMs. SWR revalidates this list on mount @@ -37,8 +50,26 @@ export function FilePreviewPanel({ options: { disabled: !filePath }, }); - const fileName = filePath ? (filePath.split("/").pop() ?? filePath) : ""; - const baseUrl = filePath ? getFilePathViewUrl(owner, filePath) : null; + // Files opened by id are absent from the sandbox listing. + const { fileMetadata } = useFileMetadata({ + fileId: fileId ?? null, + owner, + disabled: !fileId, + }); + + const fileName = filePath + ? (filePath.split("/").pop() ?? filePath) + : (fileMetadata?.fileName ?? ""); + const baseUrl = filePath + ? getFilePathViewUrl(owner, filePath) + : fileId + ? getFileViewUrl(owner, fileId) + : null; + const downloadUrl = filePath + ? getFilePathDownloadUrl(owner, filePath) + : fileId + ? getFileDownloadUrl(owner, fileId) + : null; // Reuse the file-explorer entry when the sandbox listing has loaded so we get // the real content type, fileId, and version. Before it loads (or for files @@ -51,37 +82,51 @@ export function FilePreviewPanel({ ) : undefined; const contentType = - sandboxFile?.contentType ?? contentTypeFromFileName(fileName) ?? ""; + sandboxFile?.contentType ?? + fileMetadata?.contentType ?? + contentTypeFromFileName(fileName) ?? + ""; - const entry: FileEntry | null = !filePath - ? null - : sandboxFile - ? { ...sandboxFile, kind: "file" } - : { + const entry: FileEntry | null = sandboxFile + ? { ...sandboxFile, kind: "file" } + : filePath || fileMetadata + ? { kind: "file", isDirectory: false, fileName, - path: filePath, + path: filePath ?? fileName, contentType, - fileId: null, + fileId: fileId ?? null, thumbnailUrl: null, sizeBytes: 0, lastModifiedMs: 0, - }; + } + : null; const { category, truncatedContent, processedContent, + recordCounts, hasError, isContentLoading, } = useFilePreviewContent({ entry, fileUrl: baseUrl, - enabled: !!filePath, + enabled: !!entry, + }); + + const markdown = useMarkdownFileEditor({ + category, + entryPath: filePath, + fileUrl: baseUrl, + isActive: !!entry, + isContentLoading, + owner, + processedContent, }); - if (!filePath || !entry || !baseUrl) { + if (!entry || !baseUrl || !downloadUrl) { return null; } @@ -95,18 +140,52 @@ export function FilePreviewPanel({ {fileName}
+ {markdown.canEdit && ( + <> + + {markdown.isDirty && ( + <> +
-
+
{hasError ? (

@@ -114,18 +193,30 @@ export function FilePreviewPanel({

) : ( - + <> + {recordCounts && ( +
+ Showing {recordCounts.displayed} of {recordCounts.total} records + {recordCounts.total > MAX_CSV_ROWS && " (truncated)"} +
+ )} + + )}
diff --git a/front/components/markdown/FilePreviewBlock.test.tsx b/front/components/markdown/FilePreviewBlock.test.tsx index 71521dfe3b3d..e5db96d78122 100644 --- a/front/components/markdown/FilePreviewBlock.test.tsx +++ b/front/components/markdown/FilePreviewBlock.test.tsx @@ -126,18 +126,44 @@ describe("getFilePreviewDirectivePaths", () => { }); }); +function renderWithSidePanel( + ui: React.ReactNode +): { openPanel: ReturnType } & ReturnType { + const openPanel = vi.fn(); + return { + openPanel, + ...render( + + {ui} + + ), + }; +} + describe("getFilePreviewPlugin", () => { - it("renders a previewable file with the file name", async () => { + it("opens a previewable file in the side panel", async () => { const FilePreview = getFilePreviewPlugin(); - const { container } = render( - - - + const { container, openPanel } = renderWithSidePanel( + ); expect(screen.getByText("report final.pdf")).toBeInTheDocument(); @@ -146,25 +172,24 @@ describe("getFilePreviewPlugin", () => { fireEvent.click(screen.getByRole("button", { name: "report final.pdf" })); - expect( - await screen.findByRole("dialog", { name: "report final.pdf" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "Download" }) - ).toBeInTheDocument(); + await waitFor(() => { + expect(openPanel).toHaveBeenCalledWith({ + type: "file_preview", + filePath: "conversation-c1/reports/report final.pdf", + }); + }); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); it("downloads binary files without opening a preview", () => { const FilePreview = getFilePreviewPlugin(); - render( - - - + renderWithSidePanel( + ); fireEvent.click(screen.getByRole("button", { name: "archive.zip" })); diff --git a/front/components/workspace/analytics/AnalyticsConversationPanel.tsx b/front/components/workspace/analytics/AnalyticsConversationPanel.tsx index ddf2a2a0899b..92b3d91580f2 100644 --- a/front/components/workspace/analytics/AnalyticsConversationPanel.tsx +++ b/front/components/workspace/analytics/AnalyticsConversationPanel.tsx @@ -223,8 +223,8 @@ export function AnalyticsConversationPanel({
- - + + - - + +
); diff --git a/front/types/files.ts b/front/types/files.ts index d5d2a1a61bde..1902339380d7 100644 --- a/front/types/files.ts +++ b/front/types/files.ts @@ -455,14 +455,6 @@ type FileFormat = { * - Any file type that could contain executable code */ isSafeToDisplay: boolean; - /** - * When true, this format opens in the resizable conversation side panel - * (like frames) rather than the cramped file preview modal. This is the - * source of truth for the open-in-side-panel behavior per content type. - * Note: the side panel preview relies on the path-based conversion route, so - * a file path is still required at the call site. - */ - opensInSidePanel?: boolean; // When set, restricts which upload use cases expose this format in their file picker. // Possible values: conversation, avatar, tool_output, skill_attachment, upsert_document, // folders_document, upsert_table, project_context. Omit to allow in all contexts. @@ -563,13 +555,11 @@ export const FILE_FORMATS = { cat: "data", exts: [".ppt", ".pptx"], isSafeToDisplay: true, - opensInSidePanel: true, }, "application/vnd.openxmlformats-officedocument.presentationml.presentation": { cat: "data", exts: [".ppt", ".pptx"], isSafeToDisplay: true, - opensInSidePanel: true, }, "application/pdf": { cat: "data", exts: [".pdf"], isSafeToDisplay: true }, "application/vnd.google-apps.document": { @@ -1112,10 +1102,6 @@ export function isMarkdownContentType(contentType: string): boolean { return contentType === "text/markdown"; } -export function opensInSidePanel(contentType: string): boolean { - return getFileFormat(contentType)?.opensInSidePanel ?? false; -} - /** * Infers a supported content type from a file name's extension. * Returns null if the extension is not recognized.