feat(editor): wire transcript into draft editor with local save; update PLAN; ensure API dev script present

This commit is contained in:
Ender 2025-10-23 22:43:20 +02:00
parent 498b49c474
commit 258464156b
4 changed files with 42 additions and 9 deletions

View File

@ -68,8 +68,13 @@ Voice-first authoring tool for single-user Ghost blog. Capture audio, refine wit
## Upcoming Next Actions
- [x] Backend endpoint for audio upload `/api/media/audio` (accept WebM/PCM) — implemented with MinIO via AWS SDK v3
- [x] S3-compatible adapter using MinIO (`S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`)
- [ ] Backend STT endpoint `/api/stt` (download from MinIO, call OpenAI STT, return transcript)
- [ ] Add STT trigger in UI: call `/api/stt` with `{ bucket, key }` and render transcript
- [x] Backend STT endpoint `/api/stt` (download from MinIO, call OpenAI STT, return transcript)
- [x] Add STT trigger in UI: call `/api/stt` with `{ bucket, key }` and render transcript
## Next Priorities
- [ ] Save transcript into an editor document (draft state) and display in editor.
- [ ] List uploaded media items and allow re-use/deletion.
- [ ] Add simple document persistence API (CRUD) and local storage autosave.
## MinIO Integration Checklist
- [ ] Deploy MinIO on VPS (console `:9001`, API `:9000`).

View File

@ -1,18 +1,40 @@
import { Box, Typography } from '@mui/material';
import { Box, Button, Stack, TextField, Typography } from '@mui/material';
import AdminLayout from '../layout/AdminLayout';
import Recorder from '../features/recorder/Recorder';
import { useEffect, useState } from 'react';
export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
const [draft, setDraft] = useState<string>('');
useEffect(() => {
const saved = localStorage.getItem('voxblog_draft');
if (saved) setDraft(saved);
}, []);
const saveDraft = () => {
localStorage.setItem('voxblog_draft', draft);
};
return (
<AdminLayout title="VoxBlog Admin" onLogout={onLogout}>
<Typography variant="h4" sx={{ mb: 2 }}>
Welcome to VoxBlog Editor
</Typography>
<Box sx={{ display: 'grid', gap: 3 }}>
<Recorder />
<Typography variant="body1">
Coming next: rich editor, AI tools, and Ghost publish flow.
</Typography>
<Recorder onTranscript={(t) => setDraft(t)} />
<Box>
<Typography variant="subtitle1" sx={{ mb: 1 }}>Draft</Typography>
<TextField
fullWidth
multiline
minRows={8}
value={draft}
onChange={(e) => setDraft(e.target.value)}
/>
<Stack direction="row" spacing={2} sx={{ mt: 1 }}>
<Button variant="contained" onClick={saveDraft}>Save Draft (local)</Button>
</Stack>
</Box>
</Box>
</AdminLayout>
);

View File

@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { Box, Button, Stack, Typography } from '@mui/material';
export default function Recorder() {
export default function Recorder({ onTranscript }: { onTranscript?: (t: string) => void }) {
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]);
const [recording, setRecording] = useState(false);
@ -103,7 +103,9 @@ export default function Recorder() {
throw new Error(`STT failed: ${res.status} ${txt}`);
}
const data = await res.json();
setTranscript(data.transcript || '');
const t: string = data.transcript || '';
setTranscript(t);
if (onTranscript) onTranscript(t);
} catch (e: any) {
setError(e?.message || 'Transcription failed');
}

View File

@ -32,6 +32,7 @@
"api"
],
"dependencies": {
"@aws-sdk/client-s3": "^3.916.0",
"accepts": "^2.0.0",
"body-parser": "^2.2.0",
"content-disposition": "^1.0.0",
@ -51,6 +52,7 @@
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"multer": "^2.0.2",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
@ -62,11 +64,13 @@
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"undici": "^7.16.0",
"vary": "^1.1.2"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/multer": "^2.0.0",
"@types/node": "^24.6.0",
"after": "0.8.2",
"connect-redis": "^8.0.1",