diff --git a/backend/__pycache__/api_routes.cpython-314.pyc b/backend/__pycache__/api_routes.cpython-314.pyc index 9fcae3b..3aa3bae 100644 Binary files a/backend/__pycache__/api_routes.cpython-314.pyc and b/backend/__pycache__/api_routes.cpython-314.pyc differ diff --git a/backend/__pycache__/db_queries.cpython-314.pyc b/backend/__pycache__/db_queries.cpython-314.pyc index 5286066..f8f6ee0 100644 Binary files a/backend/__pycache__/db_queries.cpython-314.pyc and b/backend/__pycache__/db_queries.cpython-314.pyc differ diff --git a/backend/api_routes.py b/backend/api_routes.py index 6f283ff..bfce130 100644 --- a/backend/api_routes.py +++ b/backend/api_routes.py @@ -583,3 +583,20 @@ def api_post_audit(post_id: int): return jsonify({"logs": list_audit_logs(post_id=post_id, page=page, limit=limit)}) except Exception as e: return _error(str(e), 500) + +@api.get("/posts//audio") +def api_get_audio_url(post_id: int): + """ + Get a signed URL for the original audio file. + """ + expires_in = request.args.get("expires_in", default=3600, type=int) + + try: + audio_data = get_original_audio_url(post_id, expires_in=expires_in) + return jsonify(audio_data) + except ValueError as e: + return _error(str(e), 404) + except RuntimeError as e: + return _error(str(e), 500) + except Exception as e: + return _error(f"Failed to get audio URL: {e}", 500) diff --git a/backend/uploads/15e03d49-14bd-42dd-bb7f-2e28e930c361_data.m4a b/backend/uploads/15e03d49-14bd-42dd-bb7f-2e28e930c361_data.m4a new file mode 100644 index 0000000..a4def57 Binary files /dev/null and b/backend/uploads/15e03d49-14bd-42dd-bb7f-2e28e930c361_data.m4a differ diff --git a/backend/uploads/21dc3b26-0b05-4669-a86a-053f0b441f59_data.m4a b/backend/uploads/21dc3b26-0b05-4669-a86a-053f0b441f59_data.m4a new file mode 100644 index 0000000..a4def57 Binary files /dev/null and b/backend/uploads/21dc3b26-0b05-4669-a86a-053f0b441f59_data.m4a differ diff --git a/backend/uploads/2bbc9bd6-7895-4cd1-8040-c2e886f4e901_data.m4a b/backend/uploads/2bbc9bd6-7895-4cd1-8040-c2e886f4e901_data.m4a new file mode 100644 index 0000000..a4def57 Binary files /dev/null and b/backend/uploads/2bbc9bd6-7895-4cd1-8040-c2e886f4e901_data.m4a differ diff --git a/backend/uploads/dd79bca3-9b6d-4fc5-ab24-cbf09b4930a6_data.m4a b/backend/uploads/dd79bca3-9b6d-4fc5-ab24-cbf09b4930a6_data.m4a new file mode 100644 index 0000000..a4def57 Binary files /dev/null and b/backend/uploads/dd79bca3-9b6d-4fc5-ab24-cbf09b4930a6_data.m4a differ diff --git a/frontend/src/api.js b/frontend/src/api.js index 9af531e..9e0ef6f 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -3,7 +3,7 @@ * Handles all communication with Flask API */ -const API_BASE_URL = 'http://127.0.0.1:5000/api'; +const API_BASE_URL = 'http://localhost:5000/api'; class ApiClient { constructor() { @@ -107,7 +107,8 @@ class ApiClient { if (params.page) queryParams.append('page', params.page); if (params.limit) queryParams.append('limit', params.limit); if (params.visibility) queryParams.append('visibility', params.visibility); - if (params.user_id) queryParams.append('user_id', params.user_id); + // Pass current_user_id for privacy checks (not filtering by author) + if (params.current_user_id) queryParams.append('current_user_id', params.current_user_id); return this.request(`/posts?${queryParams.toString()}`); } @@ -132,6 +133,10 @@ class ApiClient { return this.updatePost(postId, { status: 'deleted' }); } + async getPostMetadata(postId) { + return this.request(`/posts/${postId}/metadata`); + } + // ==================== Post Files ==================== async getPostFiles(postId) { diff --git a/frontend/src/components/AudioPostCard.jsx b/frontend/src/components/AudioPostCard.jsx index c89e875..3bba9ab 100644 --- a/frontend/src/components/AudioPostCard.jsx +++ b/frontend/src/components/AudioPostCard.jsx @@ -1,6 +1,17 @@ -import { Play, Volume2, MoreVertical, Clock } from 'lucide-react' +import { Play, Pause, Volume2, MoreVertical, Clock, ChevronDown, ChevronUp } from 'lucide-react' +import { useState, useRef, useEffect } from 'react' +import { api } from '../api' + +export default function AudioPostCard({ post }) { + const [isPlaying, setIsPlaying] = useState(false) + const [currentTime, setCurrentTime] = useState(0) + const [duration, setDuration] = useState(0) + const [volume, setVolume] = useState(1) + const [transcript, setTranscript] = useState(null) + const [loadingTranscript, setLoadingTranscript] = useState(false) + const [transcriptExpanded, setTranscriptExpanded] = useState(false) + const audioRef = useRef(null) -export default function AudioPostCard({ post, onPlay }) { const formatDate = (dateString) => { const date = new Date(dateString) const now = new Date() @@ -12,6 +23,92 @@ export default function AudioPostCard({ post, onPlay }) { return `${Math.floor(diffMins / 1440)}d ago` } + const formatTime = (seconds) => { + if (!seconds || isNaN(seconds)) return '0:00' + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins}:${secs.toString().padStart(2, '0')}` + } + + // Load transcript on mount if post is ready + useEffect(() => { + if (post.status === 'ready' && !transcript && !loadingTranscript) { + loadTranscript() + } + }, [post.post_id, post.status]) + + const loadTranscript = async () => { + setLoadingTranscript(true) + try { + const metadata = await api.getPostMetadata(post.post_id) + if (metadata && metadata.metadata) { + const metadataObj = JSON.parse(metadata.metadata) + const promptText = metadataObj.prompt || '' + const transcriptMatch = promptText.match(/Transcript:\n([\s\S]*?)\n\nAnswer user questions/) + if (transcriptMatch) { + setTranscript(transcriptMatch[1].trim()) + } else { + setTranscript('Transcript not available') + } + } + } catch (err) { + console.error('Failed to load transcript:', err) + setTranscript('Failed to load transcript') + } finally { + setLoadingTranscript(false) + } + } + + const togglePlay = () => { + if (!audioRef.current) return + + if (isPlaying) { + audioRef.current.pause() + } else { + audioRef.current.play() + } + setIsPlaying(!isPlaying) + } + + const handleTimeUpdate = () => { + if (audioRef.current) { + setCurrentTime(audioRef.current.currentTime) + } + } + + const handleLoadedMetadata = () => { + if (audioRef.current) { + setDuration(audioRef.current.duration) + } + } + + const handleSeek = (e) => { + const rect = e.currentTarget.getBoundingClientRect() + const x = e.clientX - rect.left + const percentage = x / rect.width + const newTime = percentage * duration + + if (audioRef.current) { + audioRef.current.currentTime = newTime + setCurrentTime(newTime) + } + } + + const handleVolumeChange = (e) => { + const newVolume = parseFloat(e.target.value) + setVolume(newVolume) + if (audioRef.current) { + audioRef.current.volume = newVolume + } + } + + const handleEnded = () => { + setIsPlaying(false) + setCurrentTime(0) + } + + const progress = duration > 0 ? (currentTime / duration) * 100 : 0 + return (
{/* Post Header */} @@ -53,28 +150,99 @@ export default function AudioPostCard({ post, onPlay }) { {/* Audio Player - Only show if ready */} {post.status === 'ready' && ( -
-
- -
-
-
+ <> +
+ {/* Hidden audio element */} +
+ + {/* Transcript Section - Always shown */} +
+ + + {transcript && ( +
+
+

+ {transcript} +

+
+
+ )} + + {!transcript && !loadingTranscript && ( +
+

No transcript available

+
+ )} + + {loadingTranscript && ( +
+

Loading transcript...

+
+ )} +
+ )} {/* Processing Status */} diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 2faec93..63c26b4 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -1,7 +1,7 @@ -import { Search, User, Volume2 } from 'lucide-react' +import { Search, LogOut } from 'lucide-react' import { useState } from 'react' -export default function Header({ user, onSearch, onLogout }) { +export default function Header({ onSearch, onLogout }) { const [searchQuery, setSearchQuery] = useState('') const handleSearch = (e) => { @@ -16,7 +16,9 @@ export default function Header({ user, onSearch, onLogout }) { {/* Left: Logo */}
- + + +

VoiceVault

@@ -35,24 +37,15 @@ export default function Header({ user, onSearch, onLogout }) {
- {/* Right: User Info / Login */} + {/* Right: Logout */}
- {user ? ( -
- {user.display_name || user.email} - -
- ) : ( - - )} +
diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 2bad3df..3ade81c 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,16 +1,49 @@ import { Plus, Home, History, Settings } from 'lucide-react' -export default function Sidebar({ activeTab, onTabChange }) { +export default function Sidebar({ user, activeTab, onTabChange }) { const navItems = [ { id: 'create', label: 'Make an Archive Post', icon: Plus }, { id: 'feed', label: 'My Feed', icon: Home }, { id: 'history', label: 'History', icon: History }, { id: 'settings', label: 'Settings', icon: Settings } ] + + // Get user initials + const getInitials = () => { + if (user?.display_name) { + return user.display_name + .split(' ') + .map(n => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) + } + if (user?.email) { + return user.email.slice(0, 2).toUpperCase() + } + return 'U' + } return (