From 551abc98560d9c8705c35a020ede8db9ce31cd76 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Mon, 26 Jan 2026 00:30:37 -0800 Subject: [PATCH] Enhance history management with export and import functionalities - Added endpoints for exporting generations as ZIP archives and audio files. - Implemented import functionality for generations from ZIP archives with validation. - Updated HistoryTable component to support new export and import features. - Improved error handling and user notifications for export/import processes. - Refactored related hooks and API client methods to accommodate new functionalities. --- app/src/components/History/HistoryTable.tsx | 181 +++++++++++++--- app/src/components/Sidebar.tsx | 12 +- .../components/VoiceProfiles/ProfileList.tsx | 6 +- app/src/lib/api/client.ts | 48 +++++ app/src/lib/hooks/useHistory.ts | 128 ++++++++++++ backend/export_import.py | 196 +++++++++++++++++- backend/main.py | 99 ++++++++- landing/public/apple-touch-icon.png | Bin 0 -> 50221 bytes landing/public/favicon.ico | Bin 0 -> 4286 bytes landing/public/favicon.png | Bin 0 -> 3444 bytes landing/public/voicebox-logo.png | Bin 0 -> 2971308 bytes landing/src/app/layout.tsx | 9 + scripts/update-icons.sh | 19 +- tauri/src-tauri/gen/Assets.car | Bin 3847048 -> 3847048 bytes 14 files changed, 653 insertions(+), 45 deletions(-) create mode 100644 landing/public/apple-touch-icon.png create mode 100644 landing/public/favicon.ico create mode 100644 landing/public/favicon.png create mode 100644 landing/public/voicebox-logo.png diff --git a/app/src/components/History/HistoryTable.tsx b/app/src/components/History/HistoryTable.tsx index c0e3f7f0..4e5e42d5 100644 --- a/app/src/components/History/HistoryTable.tsx +++ b/app/src/components/History/HistoryTable.tsx @@ -1,6 +1,14 @@ -import { AudioWaveform, Download, MoreHorizontal, Play, Trash2 } from 'lucide-react'; -import { useState, useRef, useEffect } from 'react'; +import { AudioWaveform, Download, FileArchive, MoreHorizontal, Play, Trash2 } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, @@ -9,7 +17,13 @@ import { } from '@/components/ui/dropdown-menu'; import { Textarea } from '@/components/ui/textarea'; import { apiClient } from '@/lib/api/client'; -import { useDeleteGeneration, useHistory } from '@/lib/hooks/useHistory'; +import { + useDeleteGeneration, + useExportGeneration, + useExportGenerationAudio, + useHistory, + useImportGeneration, +} from '@/lib/hooks/useHistory'; import { cn } from '@/lib/utils/cn'; import { formatDate, formatDuration } from '@/lib/utils/format'; import { usePlayerStore } from '@/stores/playerStore'; @@ -22,6 +36,9 @@ export function HistoryTable() { const [page, setPage] = useState(0); const [isScrolled, setIsScrolled] = useState(false); const scrollRef = useRef(null); + const fileInputRef = useRef(null); + const [importDialogOpen, setImportDialogOpen] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); const limit = 20; const { data: historyData, isLoading } = useHistory({ @@ -30,6 +47,9 @@ export function HistoryTable() { }); const deleteGeneration = useDeleteGeneration(); + const exportGeneration = useExportGeneration(); + const exportGenerationAudio = useExportGenerationAudio(); + const importGeneration = useImportGeneration(); const setAudio = usePlayerStore((state) => state.setAudio); const restartCurrentAudio = usePlayerStore((state) => state.restartCurrentAudio); const currentAudioId = usePlayerStore((state) => state.audioId); @@ -60,23 +80,65 @@ export function HistoryTable() { } }; - const handleDownload = (audioId: string, text: string) => { - const audioUrl = apiClient.getAudioUrl(audioId); - const filename = `${text.substring(0, 30).replace(/[^a-z0-9]/gi, '_')}.wav`; - const link = document.createElement('a'); - link.href = audioUrl; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const handleDownloadAudio = (generationId: string, text: string) => { + exportGenerationAudio.mutate( + { generationId, text }, + { + onError: (error) => { + alert(`Failed to download audio: ${error.message}`); + }, + }, + ); + }; + + const handleExportPackage = (generationId: string, text: string) => { + exportGeneration.mutate( + { generationId, text }, + { + onError: (error) => { + alert(`Failed to export generation: ${error.message}`); + }, + }, + ); + }; + + const _handleImportClick = () => { + file_handleImportClickk.click(); + }; + + const _handleFileChange = (_e: React.ChangeEvent) => { + cons_handleFileChangeet.files?.[0]; + if (file) { + // Validate file extension + if (!file.name.endsWith('.voicebox.zip')) { + alert('Please select a valid .voicebox.zip file'); + return; + } + setSelectedFile(file); + setImportDialogOpen(true); + } + }; + + const handleImportConfirm = () => { + if (selectedFile) { + importGeneration.mutate(selectedFile, { + onSuccess: (data) => { + setImportDialogOpen(false); + setSelectedFile(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + alert(data.message || 'Generation imported successfully'); + }, + onError: (error) => { + alert(`Failed to import generation: ${error.message}`); + }, + }); + } }; if (isLoading) { - return ( -
-
Loading history...
-
- ); + return null; } const history = historyData?.items || []; @@ -85,8 +147,25 @@ export function HistoryTable() { return (
+ {/*
+

History

+
+ + +
+
*/} + {history.length === 0 ? ( -
+
No generation history yet. Generate your first audio to see it here.
) : ( @@ -104,15 +183,20 @@ export function HistoryTable() { {history.map((gen) => { const isCurrentlyPlaying = currentAudioId === gen.id && isPlaying; return ( -