feat(editor): wire transcript into draft editor with local save; update PLAN; ensure API dev script present
This commit is contained in:
parent
498b49c474
commit
258464156b
9
PLAN.md
9
PLAN.md
@ -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`).
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user