feat: add image selection and transcription review for AI content generation
This commit is contained in:
parent
a05fdda2d2
commit
bb166f9377
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user