feat: add image selection and transcription review for AI content generation

This commit is contained in:
Ender 2025-10-24 19:42:40 +02:00
parent a05fdda2d2
commit bb166f9377
2 changed files with 112 additions and 8 deletions

View File

@ -20,6 +20,7 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
const [previewHtml, setPreviewHtml] = useState<string>('');
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
const [previewError, setPreviewError] = useState<string | null>(null);
const [genImageKeys, setGenImageKeys] = useState<string[]>([]);
useEffect(() => {
const savedId = initialPostId || localStorage.getItem('voxblog_draft_id');
@ -136,6 +137,10 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
}
}, [activeStep]);
const toggleGenImage = (key: string) => {
setGenImageKeys((prev) => prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]);
};
// No inline post switching here; selection happens on Posts page
return (
@ -215,7 +220,13 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
</Box>
<Box sx={{ flex: 1 }}>
<MediaLibrary
onInsert={(url) => editorRef.current?.insertHtmlAtCursor(`<img src="${url}" alt="" />`)}
onInsert={(url) => {
if (editorRef.current) {
editorRef.current.insertHtmlAtCursor(`<img src="${url}" alt="" />`);
} else {
setDraft((prev) => `${prev || ''}<p><img src="${url}" alt="" /></p>`);
}
}}
onSetFeature={(url) => setMeta(m => ({ ...m, featureImage: url }))}
showSetFeature
/>
@ -240,11 +251,64 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
)}
{activeStep === 2 && (
<Box>
<Typography variant="subtitle1" sx={{ mb: 1 }}>Generate</Typography>
<Typography variant="body2" sx={{ mb: 1, color: 'text.secondary' }}>
AI content generation will use your prompt and assets. This step is coming soon.
<Box sx={{ display: 'grid', gap: 2 }}>
<Typography variant="subtitle1">Generate</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Select images as generation assets, review audio transcriptions, and set the prompt to guide AI.
</Typography>
{/* Audio transcriptions in order */}
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Audio Transcriptions</Typography>
<Stack spacing={1}>
{[...postClips].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()).map((clip, idx) => (
<Box key={clip.id} sx={{ p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, bgcolor: 'background.paper' }}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>#{idx + 1} · {new Date(clip.createdAt).toLocaleString()}</Typography>
<Typography variant="body2" sx={{ mt: 0.5 }}>{clip.transcript || '(no transcript yet)'}</Typography>
</Box>
))}
{postClips.length === 0 && (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No audio clips)</Typography>
)}
</Stack>
</Box>
{/* Images selected for generation */}
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Selected Images</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 1 }}>
{genImageKeys.map((k) => (
<Box key={k} sx={{ p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, textAlign: 'center', bgcolor: '#fafafa' }}>
<img src={`/api/media/obj?key=${encodeURIComponent(k)}`} alt={k.split('/').slice(-1)[0]} style={{ maxWidth: '100%', maxHeight: 100, objectFit: 'contain' }} />
<Button size="small" color="error" variant="text" onClick={() => toggleGenImage(k)} sx={{ mt: 0.5 }}>Remove</Button>
</Box>
))}
{genImageKeys.length === 0 && (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No images selected)</Typography>
)}
</Box>
</Box>
{/* Media library for selecting images */}
<MediaLibrary
selectionMode
selectedKeys={genImageKeys}
onToggleSelect={toggleGenImage}
/>
{/* AI prompt used for generation */}
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>AI Prompt</Typography>
<TextField
label="Instructions + context for AI generation"
value={promptText}
onChange={(e) => setPromptText(e.target.value)}
fullWidth
multiline
minRows={4}
/>
</Box>
<Stack direction="row" spacing={1}>
<Button variant="contained" disabled>Generate Draft (Coming Soon)</Button>
<Button variant="outlined" onClick={() => setActiveStep(3)}>Skip to Edit</Button>

View File

@ -7,7 +7,21 @@ type MediaItem = {
lastModified: string | null;
};
export default function MediaLibrary({ onInsert, onSetFeature, showSetFeature }: { onInsert: (url: string) => void; onSetFeature?: (url: string) => void; showSetFeature?: boolean }) {
export default function MediaLibrary({
onInsert,
onSetFeature,
showSetFeature,
selectionMode,
selectedKeys,
onToggleSelect,
}: {
onInsert?: (url: string) => void;
onSetFeature?: (url: string) => void;
showSetFeature?: boolean;
selectionMode?: boolean;
selectedKeys?: string[];
onToggleSelect?: (key: string) => void;
}) {
const [items, setItems] = useState<MediaItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
@ -142,9 +156,21 @@ export default function MediaLibrary({ onInsert, onSetFeature, showSetFeature }:
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 1.5 }}>
{filtered.map((it) => {
const url = `/api/media/obj?key=${encodeURIComponent(it.key)}`;
const selected = !!selectedKeys?.includes(it.key);
const name = it.key.split('/').slice(-1)[0];
return (
<Paper key={it.key} sx={{ p: 1.25, display: 'flex', flexDirection: 'column', minHeight: 320, overflow: 'hidden' }}>
<Paper
key={it.key}
sx={{
p: 1.25,
display: 'flex',
flexDirection: 'column',
minHeight: 320,
overflow: 'hidden',
border: '1px solid',
borderColor: selected ? 'primary.main' : 'divider',
}}
>
<Box sx={{ width: '100%', flex: '0 0 150px', display: 'flex', alignItems: 'center', justifyContent: 'center', mb: 1, overflow: 'hidden', background: '#fafafa', borderRadius: 1 }}>
<a href={url} target="_blank" rel="noreferrer">
<img src={url} alt={name} style={{ maxWidth: '100%', maxHeight: '150px', objectFit: 'contain', display: 'block' }} />
@ -154,7 +180,21 @@ export default function MediaLibrary({ onInsert, onSetFeature, showSetFeature }:
<Typography variant="caption" sx={{ display: 'block', color: 'text.secondary' }}>{fmtSize(it.size)} · {fmtDate(it.lastModified)}</Typography>
<Box sx={{ flexGrow: 1 }} />
<Stack direction="row" spacing={1} sx={{ mt: 0.5, flexWrap: 'wrap' }}>
{selectionMode && onToggleSelect ? (
<Button
size="small"
variant={selected ? 'contained' : 'outlined'}
color={selected ? 'primary' : 'inherit'}
sx={{ flex: '1 1 48%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
onClick={() => onToggleSelect(it.key)}
>
{selected ? 'Selected' : 'Select'}
</Button>
) : (
onInsert && (
<Button size="small" variant="outlined" sx={{ flex: '1 1 48%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} onClick={() => onInsert(url)}>Insert</Button>
)
)}
{showSetFeature && onSetFeature && (
<Button size="small" variant="outlined" sx={{ flex: '1 1 48%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} onClick={() => onSetFeature(url)}>Set Feature</Button>
)}