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
|
## Upcoming Next Actions
|
||||||
- [x] Backend endpoint for audio upload `/api/media/audio` (accept WebM/PCM) — implemented with MinIO via AWS SDK v3
|
- [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`)
|
- [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)
|
- [x] 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] 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
|
## MinIO Integration Checklist
|
||||||
- [ ] Deploy MinIO on VPS (console `:9001`, API `:9000`).
|
- [ ] 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 AdminLayout from '../layout/AdminLayout';
|
||||||
import Recorder from '../features/recorder/Recorder';
|
import Recorder from '../features/recorder/Recorder';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
|
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 (
|
return (
|
||||||
<AdminLayout title="VoxBlog Admin" onLogout={onLogout}>
|
<AdminLayout title="VoxBlog Admin" onLogout={onLogout}>
|
||||||
<Typography variant="h4" sx={{ mb: 2 }}>
|
<Typography variant="h4" sx={{ mb: 2 }}>
|
||||||
Welcome to VoxBlog Editor
|
Welcome to VoxBlog Editor
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'grid', gap: 3 }}>
|
<Box sx={{ display: 'grid', gap: 3 }}>
|
||||||
<Recorder />
|
<Recorder onTranscript={(t) => setDraft(t)} />
|
||||||
<Typography variant="body1">
|
<Box>
|
||||||
Coming next: rich editor, AI tools, and Ghost publish flow.
|
<Typography variant="subtitle1" sx={{ mb: 1 }}>Draft</Typography>
|
||||||
</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>
|
</Box>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Box, Button, Stack, Typography } from '@mui/material';
|
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 mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||||
const chunksRef = useRef<Blob[]>([]);
|
const chunksRef = useRef<Blob[]>([]);
|
||||||
const [recording, setRecording] = useState(false);
|
const [recording, setRecording] = useState(false);
|
||||||
@ -103,7 +103,9 @@ export default function Recorder() {
|
|||||||
throw new Error(`STT failed: ${res.status} ${txt}`);
|
throw new Error(`STT failed: ${res.status} ${txt}`);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setTranscript(data.transcript || '');
|
const t: string = data.transcript || '';
|
||||||
|
setTranscript(t);
|
||||||
|
if (onTranscript) onTranscript(t);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || 'Transcription failed');
|
setError(e?.message || 'Transcription failed');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
"api"
|
"api"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.916.0",
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"content-disposition": "^1.0.0",
|
"content-disposition": "^1.0.0",
|
||||||
@ -51,6 +52,7 @@
|
|||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
"merge-descriptors": "^2.0.0",
|
"merge-descriptors": "^2.0.0",
|
||||||
"mime-types": "^3.0.0",
|
"mime-types": "^3.0.0",
|
||||||
|
"multer": "^2.0.2",
|
||||||
"on-finished": "^2.4.1",
|
"on-finished": "^2.4.1",
|
||||||
"once": "^1.4.0",
|
"once": "^1.4.0",
|
||||||
"parseurl": "^1.3.3",
|
"parseurl": "^1.3.3",
|
||||||
@ -62,11 +64,13 @@
|
|||||||
"serve-static": "^2.2.0",
|
"serve-static": "^2.2.0",
|
||||||
"statuses": "^2.0.1",
|
"statuses": "^2.0.1",
|
||||||
"type-is": "^2.0.1",
|
"type-is": "^2.0.1",
|
||||||
|
"undici": "^7.16.0",
|
||||||
"vary": "^1.1.2"
|
"vary": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^24.6.0",
|
"@types/node": "^24.6.0",
|
||||||
"after": "0.8.2",
|
"after": "0.8.2",
|
||||||
"connect-redis": "^8.0.1",
|
"connect-redis": "^8.0.1",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user