feat(webui): fullscreen toggle for html preview panel
This commit is contained in:
@@ -10,6 +10,8 @@ import React, {
|
||||
import {
|
||||
Download,
|
||||
Loader2,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
FileText,
|
||||
Code,
|
||||
Pencil,
|
||||
@@ -189,6 +191,21 @@ export const WorkspaceFileDialog = React.memo<{
|
||||
const [htmlView, setHtmlView] = useState<"preview" | "source">("preview");
|
||||
// Bumped after a successful save so the preview iframe reloads new content.
|
||||
const [savedTick, setSavedTick] = useState(0);
|
||||
// Browser-level fullscreen for the preview panel (toolbar stays usable).
|
||||
const previewAreaRef = useRef<HTMLDivElement>(null);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = () =>
|
||||
setIsFullscreen(document.fullscreenElement === previewAreaRef.current);
|
||||
document.addEventListener("fullscreenchange", onChange);
|
||||
return () => document.removeEventListener("fullscreenchange", onChange);
|
||||
}, []);
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (document.fullscreenElement) void document.exitFullscreen();
|
||||
else void previewAreaRef.current?.requestFullscreen();
|
||||
};
|
||||
const [discardAction, setDiscardAction] = useState<
|
||||
"close" | "cancel-edit" | null
|
||||
>(null);
|
||||
@@ -492,114 +509,315 @@ export const WorkspaceFileDialog = React.memo<{
|
||||
<DialogContent
|
||||
aria-describedby={undefined}
|
||||
className="flex h-[80vh] max-h-[80vh] min-w-[60vw] flex-col p-6"
|
||||
onEscapeKeyDown={(e) => {
|
||||
// While the preview panel is fullscreen, Esc must only exit
|
||||
// fullscreen — not close the whole dialog.
|
||||
if (document.fullscreenElement) e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogTitle className="sr-only">{path}</DialogTitle>
|
||||
<div className="mb-4 flex items-center justify-between gap-3 border-b border-border pb-4">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<FileText
|
||||
className="h-5 w-5 shrink-0 text-[var(--color-text-tertiary)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-base font-medium text-primary">
|
||||
{path}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{editing ? (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={() => {
|
||||
runOrConfirmDiscard("cancel-edit");
|
||||
}}
|
||||
disabled={saving}
|
||||
>
|
||||
<Eye
|
||||
size={16}
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 bg-[var(--brand-solid)] px-3 text-[var(--brand-foreground)] hover:opacity-90"
|
||||
onClick={save}
|
||||
disabled={saving || !dirty}
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2
|
||||
size={16}
|
||||
className="mr-1 animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Save
|
||||
size={16}
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{saving ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{showHtmlToggle && (
|
||||
<div
|
||||
role="group"
|
||||
aria-label="HTML view"
|
||||
className="flex items-center rounded-md border border-border"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-8 rounded-r-none px-2 ${
|
||||
htmlView === "preview" ? "bg-accent" : ""
|
||||
}`}
|
||||
onClick={() => setHtmlView("preview")}
|
||||
aria-pressed={htmlView === "preview"}
|
||||
>
|
||||
<Eye size={16} className="mr-1" aria-hidden="true" />
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-8 rounded-l-none px-2 ${
|
||||
htmlView === "source" ? "bg-accent" : ""
|
||||
}`}
|
||||
onClick={() => setHtmlView("source")}
|
||||
aria-pressed={htmlView === "source"}
|
||||
>
|
||||
<Code size={16} className="mr-1" aria-hidden="true" />
|
||||
Source
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{editable && (
|
||||
<div
|
||||
ref={previewAreaRef}
|
||||
className={
|
||||
isFullscreen
|
||||
? "flex h-full w-full flex-col bg-background p-4"
|
||||
: "flex min-h-0 flex-1 flex-col"
|
||||
}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between gap-3 border-b border-border pb-4">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<FileText
|
||||
className="h-5 w-5 shrink-0 text-[var(--color-text-tertiary)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-base font-medium text-primary">
|
||||
{path}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{editing ? (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={startEdit}
|
||||
disabled={loading || content === null}
|
||||
aria-label="Edit file"
|
||||
onClick={() => {
|
||||
runOrConfirmDiscard("cancel-edit");
|
||||
}}
|
||||
disabled={saving}
|
||||
>
|
||||
<Pencil
|
||||
<Eye
|
||||
size={16}
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Edit
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 bg-[var(--brand-solid)] px-3 text-[var(--brand-foreground)] hover:opacity-90"
|
||||
onClick={save}
|
||||
disabled={saving || !dirty}
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2
|
||||
size={16}
|
||||
className="mr-1 animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Save
|
||||
size={16}
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{saving ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{showHtmlToggle && (
|
||||
<div
|
||||
role="group"
|
||||
aria-label="HTML view"
|
||||
className="flex items-center rounded-md border border-border"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-8 rounded-r-none px-2 ${
|
||||
htmlView === "preview" ? "bg-accent" : ""
|
||||
}`}
|
||||
onClick={() => setHtmlView("preview")}
|
||||
aria-pressed={htmlView === "preview"}
|
||||
>
|
||||
<Eye size={16} className="mr-1" aria-hidden="true" />
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-8 rounded-l-none px-2 ${
|
||||
htmlView === "source" ? "bg-accent" : ""
|
||||
}`}
|
||||
onClick={() => setHtmlView("source")}
|
||||
aria-pressed={htmlView === "source"}
|
||||
>
|
||||
<Code size={16} className="mr-1" aria-hidden="true" />
|
||||
Source
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{showHtmlToggle && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={toggleFullscreen}
|
||||
aria-label="Toggle fullscreen"
|
||||
aria-pressed={isFullscreen}
|
||||
title="Fullscreen"
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize2 size={16} aria-hidden="true" />
|
||||
) : (
|
||||
<Maximize2 size={16} aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{editable && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={startEdit}
|
||||
disabled={loading || content === null}
|
||||
aria-label="Edit file"
|
||||
>
|
||||
<Pencil
|
||||
size={16}
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={workspaceFileUrl(threadId, path, true)}
|
||||
download={name}
|
||||
>
|
||||
<Download
|
||||
size={16}
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Download
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
disabled={deleting}
|
||||
aria-label="Delete file"
|
||||
title="Delete file"
|
||||
>
|
||||
{deleting ? (
|
||||
<Loader2
|
||||
size={16}
|
||||
className="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Trash2
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{actionError && (
|
||||
<p
|
||||
role="alert"
|
||||
className="mb-2 text-sm text-destructive"
|
||||
>
|
||||
{actionError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
{editing ? (
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
spellCheck={false}
|
||||
aria-label="File content"
|
||||
className="h-full w-full resize-none rounded-md border border-border bg-background p-4 font-mono text-sm leading-relaxed text-foreground outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
||||
placeholder="File is empty…"
|
||||
/>
|
||||
) : kind === "image" ? (
|
||||
<div className="relative h-full overflow-hidden rounded-md bg-[var(--color-surface)]">
|
||||
{/* key on path: switching files resets the zoom state. */}
|
||||
<TransformWrapper
|
||||
key={path}
|
||||
initialScale={1}
|
||||
minScale={0.2}
|
||||
maxScale={8}
|
||||
centerOnInit
|
||||
smooth={false}
|
||||
wheel={{ step: 0.05 }}
|
||||
doubleClick={{ mode: "reset" }}
|
||||
>
|
||||
{({ zoomIn, zoomOut, resetTransform }) => (
|
||||
<>
|
||||
<div className="absolute right-2 top-2 z-10 flex gap-0.5 rounded-md border border-border bg-background/90 p-1 shadow-sm">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={() => zoomOut()}
|
||||
aria-label="Zoom out"
|
||||
title="Zoom out"
|
||||
>
|
||||
<ZoomOut className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={() => zoomIn()}
|
||||
aria-label="Zoom in"
|
||||
title="Zoom in"
|
||||
>
|
||||
<ZoomIn className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={() => resetTransform()}
|
||||
aria-label="Reset zoom"
|
||||
title="Fit to window"
|
||||
>
|
||||
<RotateCcw className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<TransformComponent
|
||||
wrapperClass="!h-full !w-full"
|
||||
contentClass="!h-full !w-full flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
src={workspaceFileUrl(threadId, path)}
|
||||
alt={name}
|
||||
className="max-h-full max-w-full select-none object-contain"
|
||||
draggable={false}
|
||||
/>
|
||||
</TransformComponent>
|
||||
{onNavigate && (siblingNav.prev || siblingNav.next) && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute left-2 top-1/2 z-10 size-8 -translate-y-1/2 rounded-md border border-border bg-background/90 shadow-sm disabled:opacity-30"
|
||||
onClick={() => navigateTo(siblingNav.prev)}
|
||||
disabled={!siblingNav.prev}
|
||||
aria-label="Previous image"
|
||||
title="Previous image"
|
||||
>
|
||||
<ChevronLeft className="size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-1/2 z-10 size-8 -translate-y-1/2 rounded-md border border-border bg-background/90 shadow-sm disabled:opacity-30"
|
||||
onClick={() => navigateTo(siblingNav.next)}
|
||||
disabled={!siblingNav.next}
|
||||
aria-label="Next image"
|
||||
title="Next image"
|
||||
>
|
||||
<ChevronRight className="size-5" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TransformWrapper>
|
||||
</div>
|
||||
) : kind === "pdf" ? (
|
||||
<iframe
|
||||
src={workspaceFileUrl(threadId, path)}
|
||||
title={name}
|
||||
className="h-full w-full rounded-md border border-border"
|
||||
/>
|
||||
) : kind === "binary" || tooBigForText || tooBigForOffice ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-12 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{tooBigForText
|
||||
? "This file is too large to preview inline."
|
||||
: tooBigForOffice
|
||||
? "Office files larger than 25 MB can't be previewed inline."
|
||||
: "This file type can't be previewed."}
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
@@ -611,268 +829,98 @@ export const WorkspaceFileDialog = React.memo<{
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Download
|
||||
Download file
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loader2
|
||||
className="size-5 animate-spin text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-12 text-center">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
disabled={deleting}
|
||||
aria-label="Delete file"
|
||||
title="Delete file"
|
||||
asChild
|
||||
>
|
||||
{deleting ? (
|
||||
<Loader2
|
||||
<a
|
||||
href={workspaceFileUrl(threadId, path, true)}
|
||||
download={name}
|
||||
>
|
||||
<Download
|
||||
size={16}
|
||||
className="animate-spin"
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Trash2
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
Download file
|
||||
</a>
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
) : officePreview?.kind === "pdf" ? (
|
||||
<iframe
|
||||
src={officePreview.url}
|
||||
title={`${name} preview`}
|
||||
className="h-full w-full rounded-md border border-border"
|
||||
/>
|
||||
) : officePreview?.kind === "docx" ? (
|
||||
<DocxPreview
|
||||
document={officePreview.document}
|
||||
onError={useDocxPreviewFallback}
|
||||
/>
|
||||
) : officePreview?.kind === "spreadsheet" ? (
|
||||
<SpreadsheetPreview
|
||||
document={officePreview.document}
|
||||
onError={setError}
|
||||
/>
|
||||
) : isHtml && htmlView === "preview" && !tooBigForText ? (
|
||||
<iframe
|
||||
key={savedTick}
|
||||
src={`${workspaceFileUrl(threadId, path)}&render=1`}
|
||||
sandbox="allow-scripts"
|
||||
title={name}
|
||||
className="h-full w-full rounded-md border border-border bg-white"
|
||||
/>
|
||||
) : (
|
||||
<ScrollArea className="h-full rounded-md bg-[var(--color-surface)]">
|
||||
<div className="p-4">
|
||||
{content && content.length > 0 ? (
|
||||
isMarkdown ? (
|
||||
<div className="rounded-md p-6">
|
||||
<MarkdownContent content={content} />
|
||||
</div>
|
||||
) : (
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={oneDark}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
showLineNumbers
|
||||
wrapLines={true}
|
||||
lineProps={{ style: { whiteSpace: "pre-wrap" } }}
|
||||
>
|
||||
{content}
|
||||
</SyntaxHighlighter>
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
File is empty
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{actionError && (
|
||||
<p
|
||||
role="alert"
|
||||
className="mb-2 text-sm text-destructive"
|
||||
>
|
||||
{actionError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
{editing ? (
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
spellCheck={false}
|
||||
aria-label="File content"
|
||||
className="h-full w-full resize-none rounded-md border border-border bg-background p-4 font-mono text-sm leading-relaxed text-foreground outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
||||
placeholder="File is empty…"
|
||||
/>
|
||||
) : kind === "image" ? (
|
||||
<div className="relative h-full overflow-hidden rounded-md bg-[var(--color-surface)]">
|
||||
{/* key on path: switching files resets the zoom state. */}
|
||||
<TransformWrapper
|
||||
key={path}
|
||||
initialScale={1}
|
||||
minScale={0.2}
|
||||
maxScale={8}
|
||||
centerOnInit
|
||||
smooth={false}
|
||||
wheel={{ step: 0.05 }}
|
||||
doubleClick={{ mode: "reset" }}
|
||||
>
|
||||
{({ zoomIn, zoomOut, resetTransform }) => (
|
||||
<>
|
||||
<div className="absolute right-2 top-2 z-10 flex gap-0.5 rounded-md border border-border bg-background/90 p-1 shadow-sm">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={() => zoomOut()}
|
||||
aria-label="Zoom out"
|
||||
title="Zoom out"
|
||||
>
|
||||
<ZoomOut className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={() => zoomIn()}
|
||||
aria-label="Zoom in"
|
||||
title="Zoom in"
|
||||
>
|
||||
<ZoomIn className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={() => resetTransform()}
|
||||
aria-label="Reset zoom"
|
||||
title="Fit to window"
|
||||
>
|
||||
<RotateCcw className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<TransformComponent
|
||||
wrapperClass="!h-full !w-full"
|
||||
contentClass="!h-full !w-full flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
src={workspaceFileUrl(threadId, path)}
|
||||
alt={name}
|
||||
className="max-h-full max-w-full select-none object-contain"
|
||||
draggable={false}
|
||||
/>
|
||||
</TransformComponent>
|
||||
{onNavigate && (siblingNav.prev || siblingNav.next) && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute left-2 top-1/2 z-10 size-8 -translate-y-1/2 rounded-md border border-border bg-background/90 shadow-sm disabled:opacity-30"
|
||||
onClick={() => navigateTo(siblingNav.prev)}
|
||||
disabled={!siblingNav.prev}
|
||||
aria-label="Previous image"
|
||||
title="Previous image"
|
||||
>
|
||||
<ChevronLeft className="size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-1/2 z-10 size-8 -translate-y-1/2 rounded-md border border-border bg-background/90 shadow-sm disabled:opacity-30"
|
||||
onClick={() => navigateTo(siblingNav.next)}
|
||||
disabled={!siblingNav.next}
|
||||
aria-label="Next image"
|
||||
title="Next image"
|
||||
>
|
||||
<ChevronRight className="size-5" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TransformWrapper>
|
||||
</div>
|
||||
) : kind === "pdf" ? (
|
||||
<iframe
|
||||
src={workspaceFileUrl(threadId, path)}
|
||||
title={name}
|
||||
className="h-full w-full rounded-md border border-border"
|
||||
/>
|
||||
) : kind === "binary" || tooBigForText || tooBigForOffice ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-12 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{tooBigForText
|
||||
? "This file is too large to preview inline."
|
||||
: tooBigForOffice
|
||||
? "Office files larger than 25 MB can't be previewed inline."
|
||||
: "This file type can't be previewed."}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={workspaceFileUrl(threadId, path, true)}
|
||||
download={name}
|
||||
>
|
||||
<Download
|
||||
size={16}
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Download file
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loader2
|
||||
className="size-5 animate-spin text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-12 text-center">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={workspaceFileUrl(threadId, path, true)}
|
||||
download={name}
|
||||
>
|
||||
<Download
|
||||
size={16}
|
||||
className="mr-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Download file
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
) : officePreview?.kind === "pdf" ? (
|
||||
<iframe
|
||||
src={officePreview.url}
|
||||
title={`${name} preview`}
|
||||
className="h-full w-full rounded-md border border-border"
|
||||
/>
|
||||
) : officePreview?.kind === "docx" ? (
|
||||
<DocxPreview
|
||||
document={officePreview.document}
|
||||
onError={useDocxPreviewFallback}
|
||||
/>
|
||||
) : officePreview?.kind === "spreadsheet" ? (
|
||||
<SpreadsheetPreview
|
||||
document={officePreview.document}
|
||||
onError={setError}
|
||||
/>
|
||||
) : isHtml && htmlView === "preview" && !tooBigForText ? (
|
||||
<iframe
|
||||
key={savedTick}
|
||||
src={`${workspaceFileUrl(threadId, path)}&render=1`}
|
||||
sandbox="allow-scripts"
|
||||
title={name}
|
||||
className="h-full w-full rounded-md border border-border bg-white"
|
||||
/>
|
||||
) : (
|
||||
<ScrollArea className="h-full rounded-md bg-[var(--color-surface)]">
|
||||
<div className="p-4">
|
||||
{content && content.length > 0 ? (
|
||||
isMarkdown ? (
|
||||
<div className="rounded-md p-6">
|
||||
<MarkdownContent content={content} />
|
||||
</div>
|
||||
) : (
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={oneDark}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
showLineNumbers
|
||||
wrapLines={true}
|
||||
lineProps={{ style: { whiteSpace: "pre-wrap" } }}
|
||||
>
|
||||
{content}
|
||||
</SyntaxHighlighter>
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
File is empty
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user