Skip to content

Commit a117f5c

Browse files
committed
refactor: optimize files page structure and preview experience
1 parent 396c97c commit a117f5c

26 files changed

Lines changed: 710 additions & 494 deletions

frontend/features/chat/components/message/message-attachment.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import {
1212
AttachmentTrigger,
1313
} from "@/components/ui/attachment";
1414
import type { MessageAttachment } from "@/features/chat/types/messages";
15-
import type { PreviewDialogFile } from "@/shared/components/file-preview/file-preview-dialog";
15+
import type { PreviewDialogFile } from "@/shared/components/file-preview/preview-dialog";
1616
import { formatBytes, resolveFileExtension, resolveFileIcon } from "@/shared/lib/file-display";
1717
import type { FileContentResult } from "@/shared/api/file";
1818

1919
const FilePreviewDialog = dynamic(
20-
() => import("@/shared/components/file-preview/file-preview-dialog").then((module) => module.FilePreviewDialog),
20+
() => import("@/shared/components/file-preview/preview-dialog").then((module) => module.FilePreviewDialog),
2121
{ ssr: false },
2222
);
2323

frontend/features/chat/components/message/message-bot.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { Textarea } from "@/components/ui/textarea";
3333
import { cn } from "@/lib/utils";
3434
import { isUpstreamStreamingDebugBody, summarizeUpstreamError } from "@/features/chat/utils/chat-runtime";
3535
import { fetchFileContent, type FileContentResult } from "@/shared/api/file";
36-
import type { PreviewDialogFile } from "@/shared/components/file-preview/file-preview-dialog";
36+
import type { PreviewDialogFile } from "@/shared/components/file-preview/preview-dialog";
3737
import { resolveAccessToken } from "@/shared/auth/resolve-access-token";
3838
import { useLocalizedErrorMessage } from "@/i18n/use-localized-error";
3939
import { resolveLeadingImagePreview } from "@/features/chat/model/media-image-preview";

frontend/features/chat/components/message/message-user.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type { ChatModelOption } from "@/features/chat/types/chat-runtime";
1919
import { Button } from "@/components/ui/button";
2020
import { Textarea } from "@/components/ui/textarea";
2121
import type { FileContentResult } from "@/shared/api/file";
22-
import type { PreviewDialogFile } from "@/shared/components/file-preview/file-preview-dialog";
22+
import type { PreviewDialogFile } from "@/shared/components/file-preview/preview-dialog";
2323

2424
const USER_MESSAGE_COLLAPSED_LINES = 6;
2525
const USER_MESSAGE_LINE_HEIGHT_REM = 2;

frontend/features/chat/components/sections/chat-area.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { useCopyAction } from "@/shared/components/copy-action";
2525
import type { ChatModelOption } from "@/features/chat/types/chat-runtime";
2626
import type { BillingDisplayCurrency } from "@/shared/lib/billing-display";
2727
import type { FileContentResult } from "@/shared/api/file";
28-
import type { PreviewDialogFile } from "@/shared/components/file-preview/file-preview-dialog";
28+
import type { PreviewDialogFile } from "@/shared/components/file-preview/preview-dialog";
2929
import {
3030
MessageScroller,
3131
MessageScrollerButton,

frontend/features/chat/components/sections/chat-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import { isSendShortcutEvent } from "@/shared/lib/platform-shortcuts";
6868
import type { BillingDisplayCurrency } from "@/shared/lib/billing-display";
6969

7070
const FilePreviewDialog = dynamic(
71-
() => import("@/shared/components/file-preview/file-preview-dialog").then((module) => module.FilePreviewDialog),
71+
() => import("@/shared/components/file-preview/preview-dialog").then((module) => module.FilePreviewDialog),
7272
{ ssr: false },
7373
);
7474

frontend/features/files/components/app-files.tsx

Lines changed: 37 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@ import {
1919
AlertDialogTitle,
2020
} from "@/components/ui/alert-dialog";
2121
import { useDialogSnapshot } from "@/shared/hooks/use-dialog-snapshot";
22+
import { useIsMobile } from "@/shared/hooks/use-mobile";
2223
import { cn } from "@/lib/utils";
2324

24-
const FILES_SIDEBAR_WIDTH_CLASS = "md:w-64 md:basis-64 md:max-w-64 lg:w-72 lg:basis-72 lg:max-w-72";
25-
const FILES_SIDEBAR_COLLAPSED_WIDTH_CLASS = "md:w-12 md:basis-12 md:max-w-12";
26-
2725
export function AppFiles() {
2826
const tCommon = useTranslations("common.actions");
2927
const t = useTranslations("files");
28+
const isMobileViewport = useIsMobile();
3029
const {
3130
fileInputRef,
3231
mobileView,
@@ -84,6 +83,10 @@ export function AppFiles() {
8483
onToggleRagOptOut,
8584
} = useFilesPage();
8685
const stableDeleteTarget = useDialogSnapshot(deleteTarget);
86+
const selectedCount = selectedFileIDs.length;
87+
const sidebarCollapsed = !isMobileViewport && isSidebarCollapsed;
88+
const selectAllDisabled = loading || files.length === 0 || bulkDeleting;
89+
const contentDeleting = Boolean(selectedFile && deletingFileID === selectedFile.fileID);
8790

8891
return (
8992
<>
@@ -94,61 +97,37 @@ export function AppFiles() {
9497
className={cn(
9598
"h-full min-h-0 min-w-0 shrink-0 overflow-hidden border-border/45 bg-transparent transition-[width,max-width,flex-basis] duration-200",
9699
"w-full border-r-0 md:border-r",
97-
isSidebarCollapsed ? FILES_SIDEBAR_COLLAPSED_WIDTH_CLASS : FILES_SIDEBAR_WIDTH_CLASS,
100+
sidebarCollapsed
101+
? "md:w-12 md:basis-12 md:max-w-12"
102+
: "md:w-64 md:basis-64 md:max-w-64 lg:w-72 lg:basis-72 lg:max-w-72",
98103
mobileView === "detail" ? "hidden md:flex" : "flex",
99104
)}
100105
>
101-
<div className={cn("flex min-h-0 min-w-0 flex-1 flex-col px-3 md:px-2", isSidebarCollapsed && "md:px-0")}>
102-
<div className="hidden md:block">
103-
<SidebarHeader
104-
collapsed={isSidebarCollapsed}
105-
total={total}
106-
query={query}
107-
searchOpen={isSearchOpen}
108-
filterKeys={filterKeys}
109-
sortKey={sortKey}
110-
uploading={uploading}
111-
selectedCount={selectedFileIDs.length}
112-
selectAllDisabled={loading || files.length === 0 || bulkDeleting}
113-
bulkDeleteDisabled={bulkDeleting}
114-
onToggleCollapsed={onToggleSidebarCollapsed}
115-
onToggleSearch={onToggleSearch}
116-
onQueryChange={onQueryChange}
117-
onFilterToggle={onFilterToggle}
118-
onSortChange={onSortChange}
119-
onSelectLoaded={onSelectLoadedFiles}
120-
onClearSelection={onClearFileSelection}
121-
onBulkDeleteRequest={onBulkDeleteRequest}
122-
onUpload={onOpenUploadPicker}
123-
/>
124-
</div>
125-
126-
<div className="md:hidden">
127-
<SidebarHeader
128-
collapsed={false}
129-
showCollapseButton={false}
130-
total={total}
131-
query={query}
132-
searchOpen={isSearchOpen}
133-
filterKeys={filterKeys}
134-
sortKey={sortKey}
135-
uploading={uploading}
136-
selectedCount={selectedFileIDs.length}
137-
selectAllDisabled={loading || files.length === 0 || bulkDeleting}
138-
bulkDeleteDisabled={bulkDeleting}
139-
onToggleCollapsed={onToggleSidebarCollapsed}
140-
onToggleSearch={onToggleSearch}
141-
onQueryChange={onQueryChange}
142-
onFilterToggle={onFilterToggle}
143-
onSortChange={onSortChange}
144-
onSelectLoaded={onSelectLoadedFiles}
145-
onClearSelection={onClearFileSelection}
146-
onBulkDeleteRequest={onBulkDeleteRequest}
147-
onUpload={onOpenUploadPicker}
148-
/>
149-
</div>
106+
<div className={cn("flex min-h-0 min-w-0 flex-1 flex-col px-3 md:px-2", sidebarCollapsed && "md:px-0")}>
107+
<SidebarHeader
108+
total={total}
109+
query={query}
110+
searchOpen={isSearchOpen}
111+
filterKeys={filterKeys}
112+
sortKey={sortKey}
113+
uploading={uploading}
114+
selectedCount={selectedCount}
115+
selectAllDisabled={selectAllDisabled}
116+
bulkDeleteDisabled={bulkDeleting}
117+
collapsed={sidebarCollapsed}
118+
showCollapseButton={!isMobileViewport}
119+
onToggleCollapsed={onToggleSidebarCollapsed}
120+
onToggleSearch={onToggleSearch}
121+
onQueryChange={onQueryChange}
122+
onFilterToggle={onFilterToggle}
123+
onSortChange={onSortChange}
124+
onSelectLoaded={onSelectLoadedFiles}
125+
onClearSelection={onClearFileSelection}
126+
onBulkDeleteRequest={onBulkDeleteRequest}
127+
onUpload={onOpenUploadPicker}
128+
/>
150129

151-
{!isSidebarCollapsed ? (
130+
{!sidebarCollapsed ? (
152131
<SidebarList
153132
items={files}
154133
selectedFileID={selectedFileID}
@@ -169,7 +148,7 @@ export function AppFiles() {
169148
onDeleteRequest={onDeleteRequest}
170149
/>
171150
) : null}
172-
{!isSidebarCollapsed ? <StorageQuotaPanel quota={quota} /> : null}
151+
{!sidebarCollapsed ? <StorageQuotaPanel quota={quota} /> : null}
173152
</div>
174153
</aside>
175154

@@ -180,7 +159,7 @@ export function AppFiles() {
180159
<ContentHeader
181160
file={selectedFile}
182161
preview={preview}
183-
deleting={Boolean(selectedFile && deletingFileID === selectedFile.fileID)}
162+
deleting={contentDeleting}
184163
onBack={mobileView === "detail" ? onBackToList : undefined}
185164
onOpen={openPreview}
186165
onDownload={downloadPreview}
@@ -189,6 +168,7 @@ export function AppFiles() {
189168
/>
190169
<ContentPreview
191170
file={selectedFile}
171+
deferEmptyState={loading || syncing}
192172
preview={preview}
193173
extract={extract}
194174
contentTab={contentTab}
@@ -236,7 +216,7 @@ export function AppFiles() {
236216
<AlertDialogHeader>
237217
<AlertDialogTitle>{t("bulkDeleteDialog.title")}</AlertDialogTitle>
238218
<AlertDialogDescription>
239-
{t("bulkDeleteDialog.description", { count: selectedFileIDs.length })}
219+
{t("bulkDeleteDialog.description", { count: selectedCount })}
240220
</AlertDialogDescription>
241221
</AlertDialogHeader>
242222
<AlertDialogFooter>

frontend/features/files/components/sections/content/content-header.tsx

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
"use client";
22

33
import * as React from "react";
4-
import { LoaderCircle } from "lucide-react";
4+
import { ChevronLeft, Download, ExternalLink, LoaderCircle, Trash2 } from "lucide-react";
55
import { useTranslations } from "next-intl";
66

7-
import { ChevronLeft } from "lucide-react";
8-
9-
import { ExternalLink } from "@/components/animate-ui/icons/external-link";
10-
import { Download } from "@/components/animate-ui/icons/download";
11-
import { Trash2 } from "@/components/animate-ui/icons/trash-2";
127
import { formatBytes, formatDateTime, resolveFileExtension, resolveFileIcon } from "@/shared/lib/file-display";
138
import type { FilePreviewState } from "@/features/files/hooks/use-file-preview";
149
import { Button } from "@/components/ui/button";
1510
import { resolveFileProcessingBadge, resolveFileProcessingToneClass } from "@/shared/lib/file-processing";
1611
import type { FileObjectDTO } from "@/shared/api/file.types";
1712
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
1813
import { useAppLocale } from "@/i18n/app-i18n-provider";
14+
import { cn } from "@/lib/utils";
1915

2016
type ContentHeaderProps = {
2117
file: FileObjectDTO | null;
@@ -52,10 +48,10 @@ export function ContentHeader({
5248
onDeleteRequest,
5349
onToggleRagOptOut,
5450
}: ContentHeaderProps) {
51+
const tCommon = useTranslations("common.actions");
5552
const t = useTranslations("files");
5653
const tStatus = useTranslations("files.status");
5754
const { locale } = useAppLocale();
58-
const [hoveredAction, setHoveredAction] = React.useState<"open" | "download" | "delete" | null>(null);
5955

6056
if (!file) {
6157
return null;
@@ -65,6 +61,7 @@ export function ContentHeader({
6561
const fileTypeLabel = resolveRawFileTypeLabel(file);
6662
const isReady = preview.status === "ready";
6763
const thumbnail = isReady && preview.isImage ? preview.objectURL : null;
64+
const avatarFallback = file.fileName.slice(0, 2).toUpperCase() || "?";
6865
const processingBadge = resolveFileProcessingBadge({
6966
fileCategory: file.fileCategory,
7067
processingStatus: file.processingStatus,
@@ -80,15 +77,23 @@ export function ContentHeader({
8077
<div className="flex min-w-0 shrink-0 items-center justify-between gap-4 border-b border-border/40 px-3 py-4 md:px-6">
8178
<div className="flex min-w-0 flex-1 items-center gap-3">
8279
{onBack ? (
83-
<Button type="button" variant="ghost" size="icon" className="size-7 shrink-0 md:hidden" onClick={onBack}>
80+
<Button
81+
type="button"
82+
variant="ghost"
83+
size="icon"
84+
className="-ml-1 size-8 shrink-0 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground md:hidden"
85+
aria-label={tCommon("back")}
86+
onClick={onBack}
87+
>
8488
<ChevronLeft className="size-4" />
8589
</Button>
8690
) : null}
87-
<div className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md bg-background/70">
91+
92+
<div className={cn("flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md bg-background/70", onBack && "hidden md:flex")}>
8893
{thumbnail ? (
8994
<Avatar className="h-7 w-7 rounded-md">
9095
<AvatarImage src={thumbnail} alt={file.fileName} />
91-
<AvatarFallback className="text-base font-medium rounded-md bg-background text-foreground">{file.fileName.charAt(0).toUpperCase() + file.fileName.charAt(1).toUpperCase()}</AvatarFallback>
96+
<AvatarFallback className="rounded-md bg-background text-base font-medium text-foreground">{avatarFallback}</AvatarFallback>
9297
</Avatar>
9398
) : (
9499
React.createElement(fileIcon, { className: "size-5 text-muted-foreground" })
@@ -106,25 +111,31 @@ export function ContentHeader({
106111
{formatBytes(file.sizeBytes)}
107112
</p>
108113
<span
109-
className={`inline-flex rounded-md border px-1.5 py-0.5 text-[10px] font-medium ${resolveFileProcessingToneClass(processingBadge.tone)}`}
114+
className={cn(
115+
"inline-flex rounded-md border px-1.5 py-0.5 text-[10px] font-medium",
116+
resolveFileProcessingToneClass(processingBadge.tone),
117+
)}
110118
title={processingBadge.detail}
111119
>
112120
{processingBadge.label}
113121
</span>
114122
{file.embedStatus === "ready" ? (
115-
<button
123+
<Button
116124
type="button"
125+
variant="ghost"
126+
size="sm"
117127
onClick={() => onToggleRagOptOut(file.fileID, file.ragOptOut)}
118128
title={file.ragOptOut ? t("rag.disabledTitle") : t("rag.enabledTitle")}
119-
className={`inline-flex items-center gap-0.5 rounded-md border px-1.5 py-0.5 text-[10px] font-medium transition-colors ${
129+
className={cn(
130+
"h-auto gap-0.5 rounded-md border px-1.5 py-0.5 text-[10px] font-medium shadow-none",
120131
file.ragOptOut
121132
? "border-border/50 text-muted-foreground/60 hover:border-border hover:text-muted-foreground"
122-
: "border-emerald-500/30 bg-emerald-50/50 text-emerald-700 hover:bg-emerald-50 dark:border-emerald-500/20 dark:bg-emerald-950/20 dark:text-emerald-400"
123-
}`}
133+
: "border-primary/25 bg-primary/10 text-primary hover:bg-primary/15",
134+
)}
124135
>
125136
<span></span>
126137
<span>{file.ragOptOut ? t("rag.disabled") : t("rag.enabled")}</span>
127-
</button>
138+
</Button>
128139
) : null}
129140
</div>
130141
</div>
@@ -140,10 +151,8 @@ export function ContentHeader({
140151
disabled={!isReady}
141152
aria-label={t("actions.open")}
142153
title={t("actions.open")}
143-
onMouseEnter={() => setHoveredAction("open")}
144-
onMouseLeave={() => setHoveredAction((current) => (current === "open" ? null : current))}
145154
>
146-
<ExternalLink className="size-3.5" strokeWidth={1.6} animate={hoveredAction === "open" ? "default" : false} />
155+
<ExternalLink className="size-3.5" strokeWidth={1.6} />
147156
</Button>
148157
<Button
149158
type="button"
@@ -154,10 +163,8 @@ export function ContentHeader({
154163
disabled={!isReady}
155164
aria-label={t("actions.download")}
156165
title={t("actions.download")}
157-
onMouseEnter={() => setHoveredAction("download")}
158-
onMouseLeave={() => setHoveredAction((current) => (current === "download" ? null : current))}
159166
>
160-
<Download className="size-3.5" strokeWidth={1.6} animate={hoveredAction === "download" ? "default-loop" : false} />
167+
<Download className="size-3.5" strokeWidth={1.6} />
161168
</Button>
162169
<Button
163170
type="button"
@@ -168,10 +175,8 @@ export function ContentHeader({
168175
disabled={deleting}
169176
aria-label={t("actions.delete")}
170177
title={t("actions.delete")}
171-
onMouseEnter={() => setHoveredAction("delete")}
172-
onMouseLeave={() => setHoveredAction((current) => (current === "delete" ? null : current))}
173178
>
174-
{deleting ? <LoaderCircle className="size-3.5 animate-spin" strokeWidth={1.6} /> : <Trash2 className="size-3.5" strokeWidth={1.6} animate={hoveredAction === "delete" ? "default" : false} />}
179+
{deleting ? <LoaderCircle className="size-3.5 animate-spin" strokeWidth={1.6} /> : <Trash2 className="size-3.5" strokeWidth={1.6} />}
175180
</Button>
176181
</div>
177182
</div>

0 commit comments

Comments
 (0)