UI upgrades
This commit is contained in:
@@ -8,6 +8,7 @@ import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext';
|
||||
import { SMART_FOLDER_VIEW_ID, SMART_FOLDER_WORKBENCH_ID } from '@app/components/smartFolders/SmartFoldersRegistration';
|
||||
import FolderSpecialRoundedIcon from '@mui/icons-material/FolderSpecialRounded';
|
||||
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
||||
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
||||
import { ButtonConfig } from '@app/types/sidebar';
|
||||
@@ -16,7 +17,6 @@ import { Tooltip } from '@app/components/shared/Tooltip';
|
||||
import AllToolsNavButton from '@app/components/shared/AllToolsNavButton';
|
||||
import ActiveToolButton from "@app/components/shared/quickAccessBar/ActiveToolButton";
|
||||
import AppConfigModal from '@app/components/shared/AppConfigModal';
|
||||
import FolderSpecialRoundedIcon from '@mui/icons-material/FolderSpecialRounded';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import { useLicenseAlert } from "@app/hooks/useLicenseAlert";
|
||||
import { requestStartTour } from '@app/constants/events';
|
||||
@@ -164,18 +164,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
return availability?.available !== false;
|
||||
}), [t, setActiveButton, handleReaderToggle, selectedToolKey, resetTool, handleToolSelect, toolAvailability]);
|
||||
|
||||
const isWatchFoldersActive = workbench === SMART_FOLDER_WORKBENCH_ID;
|
||||
|
||||
const middleButtons: ButtonConfig[] = [
|
||||
{
|
||||
id: 'files',
|
||||
name: t("quickAccess.files", "Files"),
|
||||
icon: <LocalIcon icon="folder-rounded" width="1.25rem" height="1.25rem" />,
|
||||
isRound: true,
|
||||
size: 'md',
|
||||
type: 'modal',
|
||||
onClick: handleFilesButtonClick
|
||||
},
|
||||
{
|
||||
id: 'watchFolders',
|
||||
name: t("quickAccess.watchFolders", "Watch Folders"),
|
||||
@@ -189,6 +178,15 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
navigationActions.setWorkbench(SMART_FOLDER_WORKBENCH_ID);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
name: t("quickAccess.files", "Files"),
|
||||
icon: <LocalIcon icon="folder-rounded" width="1.25rem" height="1.25rem" />,
|
||||
isRound: true,
|
||||
size: 'md',
|
||||
type: 'modal',
|
||||
onClick: handleFilesButtonClick
|
||||
},
|
||||
];
|
||||
//TODO: Activity
|
||||
//{
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useState, useCallback, useEffect } from 'react';
|
||||
import { Box, Text, Stack, Group, ActionIcon, Button, Loader, ScrollArea } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import FolderPlusIcon from '@mui/icons-material/CreateNewFolder';
|
||||
import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline';
|
||||
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
|
||||
import { useSmartFolders } from '@app/hooks/useSmartFolders';
|
||||
import { useFolderRunStatuses } from '@app/hooks/useFolderRunStatuses';
|
||||
import { SmartFolder, SmartFolderRunEntry } from '@app/types/smartFolders';
|
||||
@@ -51,6 +51,7 @@ interface FolderCardProps {
|
||||
onDelete: (folder: SmartFolder) => void;
|
||||
onOpen: (folderId: string) => void;
|
||||
onDropFiles: (folder: SmartFolder, files: File[]) => void;
|
||||
onTogglePause: (folder: SmartFolder) => void;
|
||||
}
|
||||
|
||||
function FolderCard({
|
||||
@@ -61,33 +62,40 @@ function FolderCard({
|
||||
onDelete,
|
||||
onOpen,
|
||||
onDropFiles,
|
||||
onTogglePause,
|
||||
}: FolderCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [automation, setAutomation] = useState<AutomationConfig | null>(null);
|
||||
const [fileCount, setFileCount] = useState(0);
|
||||
const [lastAdded, setLastAdded] = useState<Date | null>(null);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
automationStorage.getAutomation(folder.automationId).then(setAutomation);
|
||||
|
||||
const loadCount = () =>
|
||||
const loadData = () =>
|
||||
folderStorage.getFolderData(folder.id).then((record) => {
|
||||
setFileCount(record ? Object.keys(record.files).length : 0);
|
||||
if (!record) { setFileCount(0); setLastAdded(null); return; }
|
||||
const files = Object.values(record.files);
|
||||
setFileCount(files.length);
|
||||
const dates = files.map(f => new Date(f.addedAt)).filter(d => !isNaN(d.getTime()));
|
||||
setLastAdded(dates.length ? new Date(Math.max(...dates.map(d => d.getTime()))) : null);
|
||||
});
|
||||
loadCount();
|
||||
loadData();
|
||||
|
||||
const unsub = folderStorage.onFolderChange((changedId) => {
|
||||
if (changedId === folder.id) loadCount();
|
||||
if (changedId === folder.id) loadData();
|
||||
});
|
||||
return unsub;
|
||||
}, [folder.id, folder.automationId]);
|
||||
|
||||
const FolderIcon = iconMap[folder.icon as keyof typeof iconMap] ?? iconMap.FolderIcon;
|
||||
const isActive = isProcessing || status === 'processing';
|
||||
const isDone = status === 'done' && !isActive;
|
||||
const isPaused = folder.isPaused ?? false;
|
||||
const isActive = !isPaused && (isProcessing || status === 'processing');
|
||||
const isDone = !isPaused && status === 'done' && !isActive;
|
||||
|
||||
const statusDotColor = isActive ? '#3b82f6' : isDone ? '#22c55e' : '#6b7280';
|
||||
const statusDotColor = isPaused ? 'var(--mantine-color-dimmed)' : isActive ? '#3b82f6' : isDone ? '#22c55e' : '#6b7280';
|
||||
const statusDotPulse = isActive;
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); };
|
||||
@@ -102,15 +110,13 @@ function FolderCard({
|
||||
|
||||
const ops = automation?.operations ?? [];
|
||||
|
||||
const cardBorderColor = isDragOver
|
||||
const cardBorderColor = isDragOver || isHovered
|
||||
? folder.accentColor
|
||||
: isHovered
|
||||
? folder.accentColor
|
||||
: 'var(--border-subtle)';
|
||||
: 'var(--mantine-color-default-border)';
|
||||
|
||||
const cardBoxShadow = isDragOver || isHovered
|
||||
? `0 0.25rem 0.75rem ${folder.accentColor}15`
|
||||
: 'none';
|
||||
? `0 0.25rem 0.75rem ${folder.accentColor}25`
|
||||
: '0 0.0625rem 0.25rem rgba(0,0,0,0.08)';
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -119,7 +125,7 @@ function FolderCard({
|
||||
border: `0.0625rem solid ${cardBorderColor}`,
|
||||
backgroundColor: isDragOver
|
||||
? `${folder.accentColor}08`
|
||||
: 'var(--bg-surface, var(--mantine-color-default))',
|
||||
: 'var(--bg-toolbar)',
|
||||
transition: 'border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease',
|
||||
boxShadow: cardBoxShadow,
|
||||
cursor: 'pointer',
|
||||
@@ -196,74 +202,85 @@ function FolderCard({
|
||||
borderRadius: '1rem',
|
||||
fontSize: '0.6875rem',
|
||||
fontWeight: 500,
|
||||
backgroundColor: isActive
|
||||
backgroundColor: isPaused
|
||||
? 'var(--mantine-color-default-hover)'
|
||||
: isActive
|
||||
? 'rgba(59, 130, 246, 0.12)'
|
||||
: isDone
|
||||
? 'rgba(34, 197, 94, 0.12)'
|
||||
: 'var(--mantine-color-dimmed-alpha, rgba(120,120,120,0.1))',
|
||||
color: isActive ? '#3b82f6' : isDone ? '#22c55e' : 'var(--mantine-color-dimmed)',
|
||||
: 'rgba(34, 197, 94, 0.12)',
|
||||
color: isPaused ? 'var(--mantine-color-dimmed)' : isActive ? '#3b82f6' : '#22c55e',
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{isActive
|
||||
{isPaused
|
||||
? t('smartFolders.status.paused', 'Paused')
|
||||
: isActive
|
||||
? t('smartFolders.status.processing', 'Processing')
|
||||
: isDone
|
||||
? t('smartFolders.status.done', 'Done')
|
||||
: t('smartFolders.status.idle', 'Idle')}
|
||||
: t('smartFolders.status.active', 'Active')}
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
{/* Right: file count + hover actions */}
|
||||
<Group gap="xs" wrap="nowrap" style={{ flexShrink: 0 }} onClick={(e) => e.stopPropagation()}>
|
||||
<Box style={{ textAlign: 'right' }}>
|
||||
<Text
|
||||
fw={700}
|
||||
size="sm"
|
||||
style={{ color: folder.accentColor, lineHeight: 1.2 }}
|
||||
>
|
||||
{fileCount}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" style={{ fontSize: '0.625rem', lineHeight: 1 }}>
|
||||
{fileCount === 1
|
||||
<Group gap="sm" wrap="nowrap" align="center" style={{ flexShrink: 0 }} onClick={(e) => e.stopPropagation()}>
|
||||
<Box style={{ textAlign: 'right', marginRight: '0.25rem' }}>
|
||||
<Text fw={700} size="sm" style={{ color: '#fff', lineHeight: 1.2 }}>
|
||||
{fileCount} {fileCount === 1
|
||||
? t('smartFolders.home.file', 'file')
|
||||
: t('smartFolders.home.files', 'files')}
|
||||
</Text>
|
||||
{lastAdded && (
|
||||
<Text size="xs" c="dimmed" style={{ fontSize: '0.625rem', lineHeight: 1.3 }}>
|
||||
{(() => {
|
||||
const now = new Date();
|
||||
const isToday = lastAdded.toDateString() === now.toDateString();
|
||||
if (isToday) {
|
||||
const hrs = Math.floor((now.getTime() - lastAdded.getTime()) / 3600000);
|
||||
return hrs < 1 ? 'just now' : `${hrs}hr${hrs === 1 ? '' : 's'} ago`;
|
||||
}
|
||||
const days = Math.floor((now.getTime() - lastAdded.getTime()) / 86400000);
|
||||
return days === 1 ? '1 day ago' : `${days} days ago`;
|
||||
})()}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group
|
||||
gap={2}
|
||||
gap="xs"
|
||||
wrap="nowrap"
|
||||
align="center"
|
||||
style={{
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'opacity 0.15s ease',
|
||||
marginLeft: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
onClick={() => onTogglePause(folder)}
|
||||
aria-label={isPaused ? t('smartFolders.home.resume', 'Resume') : t('smartFolders.home.pause', 'Pause')}
|
||||
title={isPaused ? t('smartFolders.home.resume', 'Resume') : t('smartFolders.home.pause', 'Pause')}
|
||||
>
|
||||
{isPaused
|
||||
? <PlayCircleOutlineIcon style={{ fontSize: '1.125rem' }} />
|
||||
: <PauseCircleOutlineIcon style={{ fontSize: '1.125rem' }} />}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
onClick={() => onEdit(folder)}
|
||||
aria-label={t('smartFolders.home.editFolder', 'Edit folder')}
|
||||
>
|
||||
<EditIcon style={{ fontSize: '0.9375rem' }} />
|
||||
<EditIcon style={{ fontSize: '1.125rem' }} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={() => onDelete(folder)}
|
||||
aria-label={t('smartFolders.home.deleteFolder', 'Delete folder')}
|
||||
>
|
||||
<DeleteOutlineIcon style={{ fontSize: '0.9375rem' }} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
style={{ color: folder.accentColor }}
|
||||
onClick={() => onOpen(folder.id)}
|
||||
aria-label={t('smartFolders.home.openFolder', 'Open folder')}
|
||||
>
|
||||
<OpenInNewIcon style={{ fontSize: '0.9375rem' }} />
|
||||
<DeleteOutlineIcon style={{ fontSize: '1.125rem' }} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
@@ -301,28 +318,11 @@ function FolderCard({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Drop hint / description row */}
|
||||
<Group gap="xs" align="center">
|
||||
<UploadFileIcon
|
||||
style={{
|
||||
fontSize: '0.75rem',
|
||||
color: isDragOver ? folder.accentColor : 'var(--mantine-color-dimmed)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="xs"
|
||||
style={{
|
||||
color: isDragOver ? folder.accentColor : 'var(--mantine-color-dimmed)',
|
||||
fontWeight: isDragOver ? 600 : 400,
|
||||
fontSize: '0.6875rem',
|
||||
}}
|
||||
>
|
||||
{isDragOver
|
||||
? t('smartFolders.home.dropHere', 'Drop to process')
|
||||
: folder.description || t('smartFolders.home.dragHint', 'Drop PDFs to process')}
|
||||
{isDragOver && (
|
||||
<Text size="xs" fw={600} style={{ color: folder.accentColor, fontSize: '0.6875rem' }}>
|
||||
{t('smartFolders.home.dropHere', 'Drop to process')}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
</Box>
|
||||
@@ -461,7 +461,7 @@ function EmptyState({ onCreate }: { onCreate: () => void }) {
|
||||
|
||||
export function SmartFolderHomePage() {
|
||||
const { t } = useTranslation();
|
||||
const { folders, loading, deleteFolder, refreshFolders } = useSmartFolders();
|
||||
const { folders, loading, deleteFolder, updateFolder, refreshFolders } = useSmartFolders();
|
||||
const statuses = useFolderRunStatuses(folders);
|
||||
const { toolRegistry, setCustomWorkbenchViewData } = useToolWorkflow();
|
||||
const { actions } = useNavigationActions();
|
||||
@@ -480,6 +480,11 @@ export function SmartFolderHomePage() {
|
||||
[setCustomWorkbenchViewData, actions]
|
||||
);
|
||||
|
||||
const handleTogglePause = useCallback(async (folder: SmartFolder) => {
|
||||
await updateFolder({ ...folder, isPaused: !folder.isPaused });
|
||||
refreshFolders();
|
||||
}, [updateFolder, refreshFolders]);
|
||||
|
||||
const handleEdit = useCallback(async (folder: SmartFolder) => {
|
||||
setEditFolder(folder);
|
||||
const automation = await automationStorage.getAutomation(folder.automationId);
|
||||
@@ -610,6 +615,7 @@ export function SmartFolderHomePage() {
|
||||
onDelete={setDeleteTarget}
|
||||
onOpen={navigateToFolder}
|
||||
onDropFiles={processFiles}
|
||||
onTogglePause={handleTogglePause}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function SmartFolderSidebarPanel() {
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
<Box px="sm" pt="xs" pb="xs" style={{ flexShrink: 0, borderBottom: '1px solid var(--border-subtle)' }}>
|
||||
<Text size="xs" fw={600} tt="uppercase" c="dimmed">
|
||||
{t('smartFolders.sidebarFiles', 'Your Files')}
|
||||
{t('smartFolders.sidebarFiles', 'My Files')}
|
||||
</Text>
|
||||
</Box>
|
||||
<FileManagerProvider
|
||||
|
||||
@@ -7,18 +7,21 @@ import {
|
||||
Button,
|
||||
ScrollArea,
|
||||
Loader,
|
||||
Tabs,
|
||||
ActionIcon,
|
||||
Modal,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
import TaskAltIcon from '@mui/icons-material/TaskAlt';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import { useFileContext } from '@app/contexts/FileContext';
|
||||
import { useSmartFolders } from '@app/hooks/useSmartFolders';
|
||||
import { useFolderData } from '@app/hooks/useFolderData';
|
||||
import { useFolderRunState } from '@app/hooks/useFolderRunState';
|
||||
@@ -47,8 +50,6 @@ interface StepProgress {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
type FilterTab = 'processed' | 'processing' | 'pending';
|
||||
|
||||
function humaniseOp(op: string): string {
|
||||
return op
|
||||
.replace(/-pdf$|-pages$|-documents?$/i, '')
|
||||
@@ -63,6 +64,7 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
const { t } = useTranslation();
|
||||
const { toolRegistry, setCustomWorkbenchViewData } = useToolWorkflow();
|
||||
const { actions } = useNavigationActions();
|
||||
const { actions: fileActions } = useFileContext();
|
||||
const { folders } = useSmartFolders();
|
||||
const folder = folders.find(f => f.id === folderId);
|
||||
|
||||
@@ -80,10 +82,10 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [stepProgresses, setStepProgresses] = useState<StepProgress[]>([]);
|
||||
const [filterTab, setFilterTab] = useState<FilterTab>('processed');
|
||||
const [outputFiles, setOutputFiles] = useState<OutputFileRecord[]>([]);
|
||||
const [inputFiles, setInputFiles] = useState<InputFileRecord[]>([]);
|
||||
const [automation, setAutomation] = useState<AutomationConfig | null>(null);
|
||||
const [storageModal, setStorageModal] = useState<'input' | 'output' | 'failed' | null>(null);
|
||||
const processingRef = useRef<Set<string>>(new Set());
|
||||
const handledPendingRef = useRef<string | null>(null);
|
||||
|
||||
@@ -203,6 +205,12 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
[handleFiles]
|
||||
);
|
||||
|
||||
const handleView = useCallback(async (blob: Blob, name: string) => {
|
||||
const file = new File([blob], name, { type: 'application/pdf' });
|
||||
await fileActions.addFiles([file]);
|
||||
actions.setWorkbench('viewer');
|
||||
}, [fileActions, actions]);
|
||||
|
||||
const handleDownload = useCallback(async (blob: Blob, name: string) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -231,15 +239,27 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
);
|
||||
}
|
||||
|
||||
const failedFileIds = fileIds.filter(id => folderRecord?.files[id]?.status === 'error');
|
||||
const isProcessingAny = processingFileIds.length > 0;
|
||||
const ops = automation?.operations ?? [];
|
||||
|
||||
// Which files to show in the right panel
|
||||
const tabFileIds = filterTab === 'processed'
|
||||
? processedFileIds
|
||||
: filterTab === 'processing'
|
||||
? processingFileIds
|
||||
: pendingFileIds;
|
||||
const hasCompressStep = ops.some(op => op.operation === 'compress');
|
||||
const totalInputBytes = inputFiles.reduce((sum, f) => sum + f.blob.size, 0);
|
||||
const totalOutputBytes = outputFiles.reduce((sum, f) => sum + f.blob.size, 0);
|
||||
const dataSavedBytes = totalInputBytes - totalOutputBytes;
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
const abs = Math.abs(bytes);
|
||||
if (abs >= 1073741824) return `${(bytes / 1073741824).toFixed(1)} GB`;
|
||||
if (abs >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`;
|
||||
if (abs >= 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
const daysRunning = folder.createdAt
|
||||
? Math.floor((Date.now() - new Date(folder.createdAt).getTime()) / 86400000)
|
||||
: null;
|
||||
|
||||
|
||||
return (
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
@@ -284,6 +304,7 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
height: '0.75rem',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -325,24 +346,8 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
{/* Stats + Add files */}
|
||||
{/* Add files */}
|
||||
<Group gap="lg" align="center">
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Text fw={700} size="lg" style={{ color: folder.accentColor, lineHeight: 1.1 }}>
|
||||
{processedFileIds.length}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" style={{ fontSize: '0.625rem' }}>
|
||||
{t('smartFolders.workbench.processed', 'Processed')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Text fw={700} size="lg" style={{ lineHeight: 1.1 }}>
|
||||
{fileIds.length}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" style={{ fontSize: '0.625rem' }}>
|
||||
{t('smartFolders.workbench.total', 'Total')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
@@ -368,9 +373,7 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
{/* Left: Activity */}
|
||||
<Box
|
||||
style={{
|
||||
width: '22rem',
|
||||
flexShrink: 0,
|
||||
borderRight: '0.0625rem solid var(--border-subtle)',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
@@ -384,76 +387,141 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* Storage cards */}
|
||||
<Box style={{ padding: '1rem 1rem 0', flexShrink: 0 }}>
|
||||
<Box style={{ padding: '0.5rem 1rem 0', flexShrink: 0 }}>
|
||||
<Box
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(4.5rem, 1fr))',
|
||||
gap: '0.5rem',
|
||||
marginBottom: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{/* Originals */}
|
||||
{/* Inputs */}
|
||||
<Box
|
||||
onClick={() => setStorageModal('input')}
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
padding: '0.5rem 0.75rem 2rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
backgroundColor: 'var(--bg-surface, var(--mantine-color-default))',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'border-color 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.borderColor = 'var(--mantine-color-blue-filled)')}
|
||||
onMouseLeave={e => (e.currentTarget.style.borderColor = 'var(--border-subtle)')}
|
||||
>
|
||||
<Group gap="xs" mb="xs">
|
||||
<FolderOpenIcon style={{ fontSize: '0.875rem', color: 'var(--mantine-color-blue-filled)' }} />
|
||||
<Text style={{ fontSize: '0.5625rem', textTransform: 'uppercase', letterSpacing: '0.05em' }} c="dimmed">
|
||||
{t('smartFolders.workbench.originals', 'Originals')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fw={700} size="sm">{inputFiles.length}</Text>
|
||||
<Text size="xs" c="dimmed" style={{ fontSize: '0.625rem' }}>
|
||||
{t('smartFolders.workbench.storedInBrowser', 'IndexedDB')}
|
||||
<FolderOpenIcon style={{ fontSize: '1.125rem', color: 'var(--mantine-color-blue-filled)', marginBottom: '0.375rem', display: 'block' }} />
|
||||
<Text fw={800} style={{ fontSize: '1.375rem', lineHeight: 1, marginBottom: '0.25rem' }}>{inputFiles.length}</Text>
|
||||
<Text style={{ fontSize: '0.5625rem', textTransform: 'uppercase', letterSpacing: '0.06em' }} c="dimmed">
|
||||
{t('smartFolders.workbench.inputs', 'Inputs')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Processed versions */}
|
||||
{/* Outputs */}
|
||||
<Box
|
||||
onClick={() => setStorageModal('output')}
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
padding: '0.5rem 0.75rem 2rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
backgroundColor: 'var(--bg-surface, var(--mantine-color-default))',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'border-color 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.borderColor = '#22c55e')}
|
||||
onMouseLeave={e => (e.currentTarget.style.borderColor = 'var(--border-subtle)')}
|
||||
>
|
||||
<TaskAltIcon style={{ fontSize: '1.125rem', color: '#22c55e', marginBottom: '0.375rem', display: 'block' }} />
|
||||
<Text fw={800} style={{ fontSize: '1.375rem', lineHeight: 1, marginBottom: '0.25rem' }}>{outputFiles.length}</Text>
|
||||
<Text style={{ fontSize: '0.5625rem', textTransform: 'uppercase', letterSpacing: '0.06em' }} c="dimmed">
|
||||
{t('smartFolders.workbench.outputs', 'Outputs')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Processed */}
|
||||
<Box
|
||||
style={{
|
||||
padding: '0.5rem 0.75rem 2rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Group gap="xs" mb="xs">
|
||||
<TaskAltIcon style={{ fontSize: '0.875rem', color: '#22c55e' }} />
|
||||
<Text style={{ fontSize: '0.5625rem', textTransform: 'uppercase', letterSpacing: '0.05em' }} c="dimmed">
|
||||
{t('smartFolders.workbench.versions', 'Versions')}
|
||||
<CheckCircleOutlineIcon style={{ fontSize: '1.125rem', color: folder.accentColor, marginBottom: '0.375rem', display: 'block' }} />
|
||||
<Text fw={800} style={{ fontSize: '1.375rem', lineHeight: 1, marginBottom: '0.25rem' }}>{processedFileIds.length}</Text>
|
||||
<Text style={{ fontSize: '0.5625rem', textTransform: 'uppercase', letterSpacing: '0.06em' }} c="dimmed">
|
||||
{t('smartFolders.workbench.processed', 'Processed')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Failed */}
|
||||
<Box
|
||||
onClick={() => failedFileIds.length > 0 && setStorageModal('failed')}
|
||||
style={{
|
||||
padding: '0.5rem 0.75rem 2rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
textAlign: 'center',
|
||||
cursor: failedFileIds.length > 0 ? 'pointer' : 'default',
|
||||
transition: 'border-color 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={e => { if (failedFileIds.length > 0) e.currentTarget.style.borderColor = '#ef4444'; }}
|
||||
onMouseLeave={e => { if (failedFileIds.length > 0) e.currentTarget.style.borderColor = 'var(--border-subtle)'; }}
|
||||
>
|
||||
<ErrorOutlineIcon style={{ fontSize: '1.125rem', color: failedFileIds.length > 0 ? '#ef4444' : 'var(--mantine-color-dimmed)', marginBottom: '0.375rem', display: 'block' }} />
|
||||
<Text fw={800} style={{ fontSize: '1.375rem', lineHeight: 1, marginBottom: '0.25rem' }}>
|
||||
{failedFileIds.length}
|
||||
</Text>
|
||||
<Text style={{ fontSize: '0.5625rem', textTransform: 'uppercase', letterSpacing: '0.06em' }} c="dimmed">
|
||||
{t('smartFolders.workbench.failed', 'Failed')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Data saved — only when compress is in pipeline */}
|
||||
{hasCompressStep && (
|
||||
<Box
|
||||
style={{
|
||||
padding: '0.5rem 0.75rem 2rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<DownloadIcon style={{ fontSize: '1.125rem', color: dataSavedBytes > 0 ? '#22c55e' : 'var(--mantine-color-dimmed)', marginBottom: '0.375rem', display: 'block' }} />
|
||||
<Text fw={800} style={{ fontSize: '1.375rem', lineHeight: 1, marginBottom: '0.25rem' }}>
|
||||
{dataSavedBytes > 0 ? formatBytes(dataSavedBytes) : '—'}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fw={700} size="sm">{outputFiles.length}</Text>
|
||||
<Text size="xs" c="dimmed" style={{ fontSize: '0.625rem' }}>
|
||||
{t('smartFolders.workbench.processedVersions', 'IndexedDB')}
|
||||
<Text style={{ fontSize: '0.5625rem', textTransform: 'uppercase', letterSpacing: '0.06em' }} c="dimmed">
|
||||
{t('smartFolders.workbench.dataSaved', 'Saved')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Days running */}
|
||||
<Box
|
||||
style={{
|
||||
padding: '0.5rem 0.75rem 2rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<HistoryIcon style={{ fontSize: '1.125rem', color: 'var(--mantine-color-dimmed)', marginBottom: '0.375rem', display: 'block' }} />
|
||||
<Text fw={800} style={{ fontSize: '1.375rem', lineHeight: 1, marginBottom: '0.25rem' }}>
|
||||
{daysRunning !== null && daysRunning > 0 ? `${daysRunning}d` : '—'}
|
||||
</Text>
|
||||
<Text style={{ fontSize: '0.5625rem', textTransform: 'uppercase', letterSpacing: '0.06em' }} c="dimmed">
|
||||
{t('smartFolders.workbench.running', 'Running')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Version history note */}
|
||||
<Group
|
||||
gap="xs"
|
||||
style={{
|
||||
padding: '0.5rem 0.625rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
backgroundColor: 'var(--mantine-color-default-hover)',
|
||||
marginBottom: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<HistoryIcon style={{ fontSize: '0.875rem', color: 'var(--mantine-color-dimmed)', flexShrink: 0 }} />
|
||||
<Text size="xs" c="dimmed" style={{ fontSize: '0.625rem', lineHeight: 1.5 }}>
|
||||
{t(
|
||||
'smartFolders.workbench.versionNote',
|
||||
'Originals stay untouched — processed files stored as separate versions in IndexedDB'
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{/* Activity label */}
|
||||
@@ -516,55 +584,44 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
{fileIds.map((fileId) => {
|
||||
const meta = folderRecord?.files[fileId];
|
||||
const status = meta?.status ?? 'pending';
|
||||
const run = recentRuns.find(r => r.inputFileId === fileId);
|
||||
const outputFile = run ? outputFiles.find(f => f.fileId === run.displayFileId) : undefined;
|
||||
const inputFile = inputFiles.find(f => f.fileId === fileId);
|
||||
const filename = meta?.name ?? inputFile?.name ?? outputFile?.name ?? fileId;
|
||||
return (
|
||||
<Box
|
||||
key={fileId}
|
||||
style={{
|
||||
padding: '0.625rem 0.75rem',
|
||||
padding: '0.375rem 0.75rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
backgroundColor: 'var(--mantine-color-default-hover)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
border: `0.0625rem solid ${status === 'error' ? '#ef444440' : 'var(--border-subtle)'}`,
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
}}
|
||||
>
|
||||
<Group gap="xs" wrap="nowrap" justify="space-between">
|
||||
<Group gap="xs" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
{status === 'processed' && (
|
||||
<CheckCircleOutlineIcon style={{ fontSize: '0.875rem', color: '#22c55e', flexShrink: 0 }} />
|
||||
)}
|
||||
{status === 'processing' && <Loader size="0.625rem" />}
|
||||
{status === 'error' && (
|
||||
<ErrorOutlineIcon style={{ fontSize: '0.875rem', color: '#ef4444', flexShrink: 0 }} />
|
||||
)}
|
||||
{status === 'pending' && (
|
||||
<Box
|
||||
style={{
|
||||
width: '0.5rem',
|
||||
height: '0.5rem',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'var(--mantine-color-dimmed)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Text size="xs" lineClamp={1} style={{ minWidth: 0 }}>
|
||||
{status === 'processed'
|
||||
? t('smartFolders.workbench.fileProcessed', 'File processed')
|
||||
: status === 'processing'
|
||||
? t('smartFolders.workbench.fileProcessing', 'Processing…')
|
||||
: status === 'error'
|
||||
? t('smartFolders.workbench.fileError', 'Error')
|
||||
: t('smartFolders.workbench.filePending', 'Queued')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed" style={{ fontSize: '0.625rem', flexShrink: 0 }}>
|
||||
{meta?.processedAt
|
||||
? new Date(meta.processedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
: ''}
|
||||
</Text>
|
||||
</Group>
|
||||
{meta?.errorMessage && (
|
||||
<Text size="xs" c="red" mt={4} lineClamp={2}>{meta.errorMessage}</Text>
|
||||
{status === 'processed' && <CheckCircleOutlineIcon style={{ fontSize: '0.875rem', color: '#22c55e', flexShrink: 0 }} />}
|
||||
{status === 'processing' && <Loader size="0.625rem" />}
|
||||
{status === 'error' && <ErrorOutlineIcon style={{ fontSize: '0.875rem', color: '#ef4444', flexShrink: 0 }} />}
|
||||
{status === 'pending' && <Box style={{ width: '0.5rem', height: '0.5rem', borderRadius: '50%', backgroundColor: 'var(--mantine-color-dimmed)', flexShrink: 0 }} />}
|
||||
<Text size="xs" lineClamp={1} style={{ flex: 1, minWidth: 0 }}>{filename}</Text>
|
||||
{meta?.errorMessage && <Text size="xs" c="red" lineClamp={1} style={{ flexShrink: 0, maxWidth: '40%' }}>{meta.errorMessage}</Text>}
|
||||
{outputFile && (
|
||||
<ActionIcon size="sm" variant="subtle" onClick={() => handleView(outputFile.blob, outputFile.name)} title="View">
|
||||
<VisibilityIcon style={{ fontSize: '1rem' }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{outputFile && (
|
||||
<ActionIcon size="sm" variant="subtle" onClick={() => handleDownload(outputFile.blob, outputFile.name)} title="Download output">
|
||||
<DownloadIcon style={{ fontSize: '1rem' }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<Text size="xs" c="dimmed" style={{ fontSize: '0.625rem', flexShrink: 0 }}>
|
||||
{(meta?.processedAt || meta?.addedAt)
|
||||
? new Date((meta.processedAt ?? meta.addedAt)!).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
: ''}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
@@ -574,239 +631,147 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
{/* Right: Output files */}
|
||||
<Box style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<Tabs
|
||||
value={filterTab}
|
||||
onChange={(v) => setFilterTab((v as FilterTab) || 'processed')}
|
||||
style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
|
||||
>
|
||||
<Box
|
||||
px="xl"
|
||||
style={{
|
||||
borderBottom: '0.0625rem solid var(--border-subtle)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Tabs.List style={{ borderBottom: 'none' }}>
|
||||
<Tabs.Tab value="processed" fz="xs">
|
||||
{t('smartFolders.workbench.processed', 'Processed')}
|
||||
{processedFileIds.length > 0 && (
|
||||
<Box
|
||||
component="span"
|
||||
ml="xs"
|
||||
style={{
|
||||
padding: '0.0625rem 0.375rem',
|
||||
borderRadius: '1rem',
|
||||
backgroundColor: `${folder.accentColor}20`,
|
||||
color: folder.accentColor,
|
||||
fontSize: '0.625rem',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{processedFileIds.length}
|
||||
</Box>
|
||||
)}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="processing" fz="xs">
|
||||
{t('smartFolders.workbench.processing', 'Processing')}
|
||||
{processingFileIds.length > 0 && (
|
||||
<Box
|
||||
component="span"
|
||||
ml="xs"
|
||||
style={{
|
||||
padding: '0.0625rem 0.375rem',
|
||||
borderRadius: '1rem',
|
||||
backgroundColor: 'rgba(59,130,246,0.15)',
|
||||
color: '#3b82f6',
|
||||
fontSize: '0.625rem',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{processingFileIds.length}
|
||||
</Box>
|
||||
)}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="pending" fz="xs">
|
||||
{t('smartFolders.workbench.pending', 'Pending')}
|
||||
{pendingFileIds.length > 0 && (
|
||||
<Box
|
||||
component="span"
|
||||
ml="xs"
|
||||
style={{
|
||||
padding: '0.0625rem 0.375rem',
|
||||
borderRadius: '1rem',
|
||||
backgroundColor: 'var(--mantine-color-default-hover)',
|
||||
color: 'var(--mantine-color-dimmed)',
|
||||
fontSize: '0.625rem',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{pendingFileIds.length}
|
||||
</Box>
|
||||
)}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Box>
|
||||
|
||||
<ScrollArea style={{ flex: 1 }}>
|
||||
<Box p="xl">
|
||||
{filterTab === 'processed' ? (
|
||||
recentRuns.length === 0 ? (
|
||||
<Box style={{ textAlign: 'center', padding: '3rem 0' }}>
|
||||
<Box
|
||||
style={{
|
||||
width: '3rem',
|
||||
height: '3rem',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'var(--mantine-color-default-hover)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto 0.75rem',
|
||||
}}
|
||||
>
|
||||
<DownloadIcon style={{ fontSize: '1.25rem', color: 'var(--mantine-color-dimmed)' }} />
|
||||
</Box>
|
||||
<Text size="sm" c="dimmed" mb={4}>
|
||||
{t('smartFolders.workbench.noOutput', 'No processed files yet')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('smartFolders.workbench.dropToStart', 'Drop files on the left to start')}
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack gap="sm">
|
||||
{recentRuns.filter(r => r.status === 'processed').map((run, i) => {
|
||||
const inputFile = inputFiles.find(f => f.fileId === run.inputFileId);
|
||||
const outputFile = outputFiles.find(f => f.fileId === run.displayFileId);
|
||||
const inputMeta = folderRecord?.files[run.inputFileId];
|
||||
return (
|
||||
<Box
|
||||
key={`${run.inputFileId}-${i}`}
|
||||
style={{
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
backgroundColor: 'var(--bg-surface, var(--mantine-color-default))',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Original row */}
|
||||
<Box
|
||||
style={{
|
||||
padding: '0.625rem 1rem',
|
||||
borderBottom: '0.0625rem solid var(--border-subtle)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.625rem',
|
||||
backgroundColor: 'var(--mantine-color-default-hover)',
|
||||
}}
|
||||
>
|
||||
<FolderOpenIcon style={{ fontSize: '0.875rem', color: 'var(--mantine-color-blue-filled)', flexShrink: 0 }} />
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text style={{ fontSize: '0.625rem', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--mantine-color-dimmed)' }}>
|
||||
{t('smartFolders.workbench.input', 'Input')}
|
||||
</Text>
|
||||
<Text size="xs" lineClamp={1} style={{ minWidth: 0 }}>
|
||||
{inputFile?.name ?? inputMeta?.name ?? run.inputFileId}
|
||||
</Text>
|
||||
</Box>
|
||||
{inputFile && (
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
onClick={() => handleDownload(inputFile.blob, inputFile.name)}
|
||||
aria-label={t('smartFolders.workbench.downloadOriginal', 'Download original')}
|
||||
>
|
||||
<DownloadIcon style={{ fontSize: '0.875rem' }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Box>
|
||||
{/* Processed row */}
|
||||
<Box
|
||||
style={{
|
||||
padding: '0.625rem 1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.625rem',
|
||||
}}
|
||||
>
|
||||
<CheckCircleOutlineIcon style={{ fontSize: '0.875rem', color: '#22c55e', flexShrink: 0 }} />
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text style={{ fontSize: '0.625rem', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--mantine-color-dimmed)' }}>
|
||||
{t('smartFolders.workbench.output', 'Output')}
|
||||
</Text>
|
||||
<Text size="xs" lineClamp={1} style={{ minWidth: 0 }}>
|
||||
{outputFile?.name ?? run.displayFileId}
|
||||
</Text>
|
||||
</Box>
|
||||
{outputFile && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
leftSection={<DownloadIcon style={{ fontSize: '0.75rem' }} />}
|
||||
onClick={() => handleDownload(outputFile.blob, outputFile.name)}
|
||||
>
|
||||
{t('smartFolders.workbench.download', 'Download')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)
|
||||
) : (
|
||||
tabFileIds.length === 0 ? (
|
||||
<Text size="xs" c="dimmed" ta="center" py="xl">
|
||||
{t('smartFolders.workbench.noFiles', 'No files')}
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
{tabFileIds.map((fileId) => {
|
||||
const meta = folderRecord?.files[fileId];
|
||||
const status = meta?.status ?? 'pending';
|
||||
return (
|
||||
<Box
|
||||
key={fileId}
|
||||
style={{
|
||||
padding: '0.75rem 1rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
backgroundColor: 'var(--bg-surface, var(--mantine-color-default))',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
{status === 'processing' && <Loader size="0.75rem" />}
|
||||
{status === 'pending' && (
|
||||
<Box
|
||||
style={{
|
||||
width: '0.5rem',
|
||||
height: '0.5rem',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'var(--mantine-color-dimmed)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Text size="sm" c="dimmed" style={{ flex: 1 }}>
|
||||
{status === 'processing'
|
||||
? t('smartFolders.workbench.fileProcessing', 'Processing…')
|
||||
: t('smartFolders.workbench.filePending', 'Queued')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Storage file modal */}
|
||||
<Modal
|
||||
opened={storageModal !== null}
|
||||
onClose={() => setStorageModal(null)}
|
||||
title={
|
||||
<Text fw={600} size="sm">
|
||||
{storageModal === 'input'
|
||||
? t('smartFolders.workbench.inputFiles', 'Input files')
|
||||
: storageModal === 'output'
|
||||
? t('smartFolders.workbench.outputFiles', 'Output files')
|
||||
: t('smartFolders.workbench.failedFiles', 'Failed files')}
|
||||
</Text>
|
||||
}
|
||||
size="md"
|
||||
>
|
||||
{storageModal === 'input' && (
|
||||
inputFiles.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" py="xl">
|
||||
{t('smartFolders.workbench.noInputFiles', 'No input files stored yet')}
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
{inputFiles.map((file) => (
|
||||
<Box
|
||||
key={file.fileId}
|
||||
style={{
|
||||
padding: '0.625rem 0.75rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<FolderOpenIcon style={{ fontSize: '1rem', color: 'var(--mantine-color-blue-filled)', flexShrink: 0 }} />
|
||||
<Text size="sm" style={{ flex: 1, minWidth: 0 }} lineClamp={1}>{file.name}</Text>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
onClick={() => handleDownload(file.blob, file.name)}
|
||||
aria-label={t('smartFolders.workbench.download', 'Download')}
|
||||
>
|
||||
<DownloadIcon style={{ fontSize: '0.875rem' }} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
)}
|
||||
{storageModal === 'output' && (
|
||||
outputFiles.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" py="xl">
|
||||
{t('smartFolders.workbench.noOutputFiles', 'No output files stored yet')}
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
{outputFiles.map((file) => (
|
||||
<Box
|
||||
key={file.fileId}
|
||||
style={{
|
||||
padding: '0.625rem 0.75rem',
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid var(--border-subtle)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<TaskAltIcon style={{ fontSize: '1rem', color: '#22c55e', flexShrink: 0 }} />
|
||||
<Text size="sm" style={{ flex: 1, minWidth: 0 }} lineClamp={1}>{file.name}</Text>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
onClick={() => handleDownload(file.blob, file.name)}
|
||||
aria-label={t('smartFolders.workbench.download', 'Download')}
|
||||
>
|
||||
<DownloadIcon style={{ fontSize: '0.875rem' }} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
)}
|
||||
{storageModal === 'failed' && (
|
||||
failedFileIds.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" py="xl">
|
||||
{t('smartFolders.workbench.noFailedFiles', 'No failed files')}
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
{failedFileIds.map((fileId) => {
|
||||
const meta = folderRecord?.files[fileId];
|
||||
const inputFile = inputFiles.find(f => f.fileId === fileId);
|
||||
const filename = meta?.name ?? inputFile?.name ?? fileId;
|
||||
return (
|
||||
<Box
|
||||
key={fileId}
|
||||
style={{
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
border: '0.0625rem solid #ef444440',
|
||||
backgroundColor: 'rgba(239,68,68,0.04)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Group gap="xs" style={{ padding: '0.625rem 0.75rem', borderBottom: '0.0625rem solid #ef444420' }}>
|
||||
<ErrorOutlineIcon style={{ fontSize: '1rem', color: '#ef4444', flexShrink: 0 }} />
|
||||
<Text size="sm" style={{ flex: 1, minWidth: 0 }} lineClamp={1}>{filename}</Text>
|
||||
{inputFile && (
|
||||
<ActionIcon size="sm" variant="subtle" onClick={() => handleDownload(inputFile.blob, inputFile.name)}>
|
||||
<DownloadIcon style={{ fontSize: '0.875rem' }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{inputFile && (
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={async () => {
|
||||
await updateFileMetadata(fileId, { status: 'pending', errorMessage: undefined });
|
||||
const file = new File([inputFile.blob], inputFile.name, { type: 'application/pdf' });
|
||||
runAutomation(file, fileId);
|
||||
setStorageModal(null);
|
||||
}}
|
||||
title="Retry"
|
||||
>
|
||||
<ReplayIcon style={{ fontSize: '0.875rem' }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
{meta?.errorMessage && (
|
||||
<Text size="xs" c="red" style={{ padding: '0.5rem 0.75rem' }}>{meta.errorMessage}</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)
|
||||
)}
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface SmartFolder {
|
||||
updatedAt: string;
|
||||
order?: number;
|
||||
isDefault?: boolean;
|
||||
isPaused?: boolean;
|
||||
}
|
||||
|
||||
export interface FolderFileMetadata {
|
||||
|
||||
Reference in New Issue
Block a user