From bb166f93773f1ae064291ab85c22db2bcdd921ab Mon Sep 17 00:00:00 2001 From: Ender Date: Fri, 24 Oct 2025 19:42:40 +0200 Subject: [PATCH] feat: add image selection and transcription review for AI content generation --- apps/admin/src/components/EditorShell.tsx | 74 ++++++++++++++++++++-- apps/admin/src/components/MediaLibrary.tsx | 46 +++++++++++++- 2 files changed, 112 insertions(+), 8 deletions(-) diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx index 23c993c..19092a8 100644 --- a/apps/admin/src/components/EditorShell.tsx +++ b/apps/admin/src/components/EditorShell.tsx @@ -20,6 +20,7 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog const [previewHtml, setPreviewHtml] = useState(''); const [previewLoading, setPreviewLoading] = useState(false); const [previewError, setPreviewError] = useState(null); + const [genImageKeys, setGenImageKeys] = useState([]); 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 editorRef.current?.insertHtmlAtCursor(``)} + onInsert={(url) => { + if (editorRef.current) { + editorRef.current.insertHtmlAtCursor(``); + } else { + setDraft((prev) => `${prev || ''}

`); + } + }} onSetFeature={(url) => setMeta(m => ({ ...m, featureImage: url }))} showSetFeature /> @@ -240,11 +251,64 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog )} {activeStep === 2 && ( - - Generate - - AI content generation will use your prompt and assets. This step is coming soon. + + Generate + + Select images as generation assets, review audio transcriptions, and set the prompt to guide AI. + + {/* Audio transcriptions in order */} + + Audio Transcriptions + + {[...postClips].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()).map((clip, idx) => ( + + #{idx + 1} · {new Date(clip.createdAt).toLocaleString()} + {clip.transcript || '(no transcript yet)'} + + ))} + {postClips.length === 0 && ( + (No audio clips) + )} + + + + {/* Images selected for generation */} + + Selected Images + + {genImageKeys.map((k) => ( + + {k.split('/').slice(-1)[0]} + + + ))} + {genImageKeys.length === 0 && ( + (No images selected) + )} + + + + {/* Media library for selecting images */} + + + {/* AI prompt used for generation */} + + AI Prompt + setPromptText(e.target.value)} + fullWidth + multiline + minRows={4} + /> + + diff --git a/apps/admin/src/components/MediaLibrary.tsx b/apps/admin/src/components/MediaLibrary.tsx index 0a60df9..418d913 100644 --- a/apps/admin/src/components/MediaLibrary.tsx +++ b/apps/admin/src/components/MediaLibrary.tsx @@ -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([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); @@ -142,9 +156,21 @@ export default function MediaLibrary({ onInsert, onSetFeature, showSetFeature }: {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 ( - + {name} @@ -154,7 +180,21 @@ export default function MediaLibrary({ onInsert, onSetFeature, showSetFeature }: {fmtSize(it.size)} · {fmtDate(it.lastModified)} - + {selectionMode && onToggleSelect ? ( + + ) : ( + onInsert && ( + + ) + )} {showSetFeature && onSetFeature && ( )}