feat(transcription): add meeting assistant micro-service v0.1.0
- Added FastAPI backend with FFmpeg and Gemini 2.0 integration - Added React frontend with upload and meeting list - Integrated into main docker-compose stack and dashboard
This commit is contained in:
121
transcription-tool/frontend/src/App.tsx
Normal file
121
transcription-tool/frontend/src/App.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Upload, Mic, FileText, Clock, CheckCircle2, Loader2, AlertCircle, ChevronRight } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const API_BASE = '/tr/api'
|
||||
|
||||
interface Meeting {
|
||||
id: number
|
||||
title: string
|
||||
status: string
|
||||
date_recorded: string
|
||||
duration_seconds?: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [meetings, setMeetings] = useState<Meeting[]>([])
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchMeetings = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_BASE}/meetings`)
|
||||
setMeetings(res.data)
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch meetings", e)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchMeetings()
|
||||
const interval = setInterval(fetchMeetings, 5000) // Poll every 5s
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setUploading(true)
|
||||
setError(null)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
await axios.post(`${API_BASE}/upload`, formData)
|
||||
fetchMeetings()
|
||||
} catch (e) {
|
||||
setError("Upload failed. Make sure the file is not too large.")
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-200">
|
||||
<div className="max-w-5xl mx-auto px-4 py-12">
|
||||
<header className="flex items-center justify-between mb-12">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Meeting Assistant</h1>
|
||||
<p className="text-slate-500 mt-2">Transcribe and analyze your meetings with Gemini 2.0</p>
|
||||
</div>
|
||||
<label className={clsx(
|
||||
"flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-full font-semibold transition-all cursor-pointer shadow-lg shadow-blue-500/20",
|
||||
uploading && "opacity-50 cursor-not-allowed"
|
||||
)}>
|
||||
{uploading ? <Loader2 className="h-5 w-5 animate-spin" /> : <Upload className="h-5 w-5" />}
|
||||
{uploading ? "Uploading..." : "New Meeting"}
|
||||
<input type="file" className="hidden" accept="audio/*" onChange={handleUpload} disabled={uploading} />
|
||||
</label>
|
||||
</header>
|
||||
|
||||
{error && (
|
||||
<div className="mb-8 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl text-red-600 dark:text-red-400 flex items-center gap-3">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4">
|
||||
{meetings.length === 0 ? (
|
||||
<div className="text-center py-20 bg-white dark:bg-slate-900 rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800">
|
||||
<Mic className="h-12 w-12 mx-auto mb-4 text-slate-300" />
|
||||
<p className="text-slate-500 font-medium">No meetings yet. Upload your first audio file.</p>
|
||||
</div>
|
||||
) : (
|
||||
meetings.map(m => (
|
||||
<div key={m.id} className="group bg-white dark:bg-slate-900 p-6 rounded-2xl border border-slate-200 dark:border-slate-800 hover:shadow-xl transition-all flex items-center justify-between cursor-pointer">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={clsx(
|
||||
"p-3 rounded-xl",
|
||||
m.status === 'COMPLETED' ? "bg-green-100 dark:bg-green-900/30 text-green-600" :
|
||||
m.status === 'ERROR' ? "bg-red-100 dark:bg-red-900/30 text-red-600" :
|
||||
"bg-blue-100 dark:bg-blue-900/30 text-blue-600 animate-pulse"
|
||||
)}>
|
||||
{m.status === 'COMPLETED' ? <CheckCircle2 className="h-6 w-6" /> : <FileText className="h-6 w-6" />}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg leading-tight">{m.title}</h3>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-slate-500">
|
||||
<span className="flex items-center gap-1"><Clock className="h-3.5 w-3.5" /> {new Date(m.created_at).toLocaleDateString()}</span>
|
||||
{m.duration_seconds && (
|
||||
<span>{Math.round(m.duration_seconds / 60)} min</span>
|
||||
)}
|
||||
<span className={clsx(
|
||||
"font-semibold uppercase tracking-wider text-[10px] px-2 py-0.5 rounded",
|
||||
m.status === 'COMPLETED' ? "bg-green-100 text-green-700" : "bg-slate-100 text-slate-600"
|
||||
)}>{m.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="h-6 w-6 text-slate-300 group-hover:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user