feat: add collapsible sections to StepGenerate component with reusable CollapsibleSection
This commit is contained in:
parent
4afb21c38b
commit
b3418e3c96
44
apps/admin/src/components/steps/CollapsibleSection.tsx
Normal file
44
apps/admin/src/components/steps/CollapsibleSection.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Box, Collapse, IconButton, Stack, Typography, type SxProps, type Theme } from '@mui/material';
|
||||||
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||||
|
import { useState, type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
export default function CollapsibleSection({ title, defaultCollapsed = true, right, sx, children }: PropsWithChildren<{
|
||||||
|
title: string;
|
||||||
|
defaultCollapsed?: boolean;
|
||||||
|
right?: React.ReactNode;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}>) {
|
||||||
|
const [open, setOpen] = useState(!defaultCollapsed);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1, overflow: 'hidden', ...sx }}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
sx={{ px: 1.25, py: 0.75, cursor: 'pointer', bgcolor: 'background.paper' }}
|
||||||
|
onClick={() => setOpen(v => !v)}
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen(v => !v);
|
||||||
|
}}
|
||||||
|
sx={{ transform: open ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 150ms ease' }}
|
||||||
|
>
|
||||||
|
<ExpandMore fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="subtitle2">{title}</Typography>
|
||||||
|
</Stack>
|
||||||
|
{right}
|
||||||
|
</Stack>
|
||||||
|
<Collapse in={open} timeout="auto" unmountOnExit={false}>
|
||||||
|
<Box sx={{ p: 1.25 }}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,19 +8,16 @@ export default function SelectedImages({
|
|||||||
onRemove: (key: string) => void;
|
onRemove: (key: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 1 }}>
|
||||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Selected Images</Typography>
|
{imageKeys.map((k) => (
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 1 }}>
|
<Box key={k} sx={{ p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, textAlign: 'center', bgcolor: '#fafafa' }}>
|
||||||
{imageKeys.map((k) => (
|
<img src={`/api/media/obj?key=${encodeURIComponent(k)}`} alt={k.split('/').slice(-1)[0]} style={{ maxWidth: '100%', maxHeight: 100, objectFit: 'contain' }} />
|
||||||
<Box key={k} sx={{ p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, textAlign: 'center', bgcolor: '#fafafa' }}>
|
<Button size="small" color="error" variant="text" onClick={() => onRemove(k)} sx={{ mt: 0.5 }}>Remove</Button>
|
||||||
<img src={`/api/media/obj?key=${encodeURIComponent(k)}`} alt={k.split('/').slice(-1)[0]} style={{ maxWidth: '100%', maxHeight: 100, objectFit: 'contain' }} />
|
</Box>
|
||||||
<Button size="small" color="error" variant="text" onClick={() => onRemove(k)} sx={{ mt: 0.5 }}>Remove</Button>
|
))}
|
||||||
</Box>
|
{imageKeys.length === 0 && (
|
||||||
))}
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No images selected)</Typography>
|
||||||
{imageKeys.length === 0 && (
|
)}
|
||||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No images selected)</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Box, Stack, TextField, Typography } from '@mui/material';
|
import { Box, Stack, TextField, Typography } from '@mui/material';
|
||||||
import MediaLibrary from '../MediaLibrary';
|
import MediaLibrary from '../MediaLibrary';
|
||||||
import SelectedImages from './SelectedImages';
|
import SelectedImages from './SelectedImages';
|
||||||
|
import CollapsibleSection from './CollapsibleSection';
|
||||||
import type { Clip } from './StepAssets';
|
import type { Clip } from './StepAssets';
|
||||||
|
|
||||||
export default function StepGenerate({
|
export default function StepGenerate({
|
||||||
@ -24,8 +25,7 @@ export default function StepGenerate({
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Audio transcriptions in order */}
|
{/* Audio transcriptions in order */}
|
||||||
<Box>
|
<CollapsibleSection title="Audio Transcriptions">
|
||||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Audio Transcriptions</Typography>
|
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
{[...postClips]
|
{[...postClips]
|
||||||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||||
@ -39,17 +39,20 @@ export default function StepGenerate({
|
|||||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No audio clips)</Typography>
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No audio clips)</Typography>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Selected images */}
|
{/* Selected images */}
|
||||||
<SelectedImages imageKeys={genImageKeys} onRemove={onToggleGenImage} />
|
<CollapsibleSection title="Selected Images">
|
||||||
|
<SelectedImages imageKeys={genImageKeys} onRemove={onToggleGenImage} />
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Media library */}
|
{/* Media library */}
|
||||||
<MediaLibrary selectionMode selectedKeys={genImageKeys} onToggleSelect={onToggleGenImage} />
|
<CollapsibleSection title="Media Library">
|
||||||
|
<MediaLibrary selectionMode selectedKeys={genImageKeys} onToggleSelect={onToggleGenImage} />
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Prompt */}
|
{/* Prompt */}
|
||||||
<Box>
|
<CollapsibleSection title="AI Prompt">
|
||||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>AI Prompt</Typography>
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Instructions + context for AI generation"
|
label="Instructions + context for AI generation"
|
||||||
value={promptText}
|
value={promptText}
|
||||||
@ -58,7 +61,7 @@ export default function StepGenerate({
|
|||||||
multiline
|
multiline
|
||||||
minRows={4}
|
minRows={4}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</CollapsibleSection>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user