diff --git a/src/components/audio/GenerationView.tsx b/src/components/audio/GenerationView.tsx index a1a9c90..8444f7f 100644 --- a/src/components/audio/GenerationView.tsx +++ b/src/components/audio/GenerationView.tsx @@ -3,7 +3,7 @@ import { ChatHeader } from "@/components/ChatHeader"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Input } from "@/components/ui/input"; -import { Mic, Play, Download, Trash2, Search } from "lucide-react"; +import { Mic, Download, Trash2, Search, ChevronLeft, ChevronRight } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { @@ -13,33 +13,56 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationItem, +} from "@/components/ui/pagination"; import { useToast } from "@/hooks/use-toast"; -import { audioGenerationService, VOICE_OPTIONS, VoiceType } from "@/services/audioGeneration"; - -interface GeneratedAudio { - id: string; - text: string; - voice: string; - voiceLabel: string; - audioUrl: string; - timestamp: Date; -} +import { audioGenerationService, VOICE_OPTIONS, VoiceType, AudioRecord } from "@/services/audioGeneration"; export const GenerationView = () => { const [textToSpeech, setTextToSpeech] = useState(""); const [isProcessing, setIsProcessing] = useState(false); const [selectedVoice, setSelectedVoice] = useState("alloy"); - const [generatedAudio, setGeneratedAudio] = useState(null); - const [audioHistory, setAudioHistory] = useState([]); + const [lastGeneratedAudio, setLastGeneratedAudio] = useState(null); + const [audioHistory, setAudioHistory] = useState([]); + const [isLoadingAudios, setIsLoadingAudios] = useState(false); const [audioSearchQuery, setAudioSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [perPage, setPerPage] = useState(10); const { toast } = useToast(); - useEffect(() => { - const savedAudios = localStorage.getItem('audioHistory'); - if (savedAudios) { - setAudioHistory(JSON.parse(savedAudios)); + // Carrega áudios do banco de dados + const loadAudios = async (page: number = currentPage, limit: number = perPage) => { + setIsLoadingAudios(true); + try { + const fetchedAudios = await audioGenerationService.getAudios(undefined, page, limit); + + // Garante que sempre seja um array + if (Array.isArray(fetchedAudios)) { + setAudioHistory(fetchedAudios); + } else { + console.warn('Resposta da API não é um array:', fetchedAudios); + setAudioHistory([]); + } + } catch (error: any) { + console.error('Erro ao carregar áudios:', error); + toast({ + title: "Erro ao carregar histórico", + description: error.message || "Não foi possível carregar o histórico de áudios.", + variant: "destructive", + }); + setAudioHistory([]); + } finally { + setIsLoadingAudios(false); } - }, []); + }; + + // Carrega áudios ao montar e quando a paginação mudar + useEffect(() => { + loadAudios(); + }, [currentPage, perPage]); const handleGenerateAudio = async () => { // Valida o texto antes de enviar @@ -66,20 +89,23 @@ export const GenerationView = () => { if (response.success) { console.log('URL do áudio gerado:', response.audio_url); - const audio: GeneratedAudio = { + // Cria objeto do áudio recém-gerado para exibição imediata + const newGeneratedAudio: AudioRecord = { id: response.audio_generation_id, - text: response.message, + user_email: '', + estabelecimento_id: 0, + input_text: response.message, + model: 'tts-1', voice: selectedVoice, - voiceLabel: VOICE_OPTIONS[selectedVoice].label, - audioUrl: response.audio_url, - timestamp: new Date(), + audio_url: response.audio_url, + duration_seconds: null, + file_size: 0, + cost_usd: '0', + created_at: new Date().toISOString(), }; - setGeneratedAudio(audio); - - const newHistory = [audio, ...audioHistory].slice(0, 10); - setAudioHistory(newHistory); - localStorage.setItem('audioHistory', JSON.stringify(newHistory)); + // Salva o último áudio gerado para exibição + setLastGeneratedAudio(newGeneratedAudio); toast({ title: "Áudio gerado com sucesso", @@ -88,6 +114,10 @@ export const GenerationView = () => { // Limpa o campo de texto após sucesso setTextToSpeech(""); + + // Recarrega a lista de áudios (sem aguardar para não bloquear a UI) + loadAudios(1, perPage); + setCurrentPage(1); } else { throw new Error('Erro ao gerar áudio'); } @@ -104,38 +134,65 @@ export const GenerationView = () => { } }; - const handleDownloadAudio = (audio: GeneratedAudio) => { + const handleDownloadAudio = (audio: AudioRecord) => { const a = document.createElement('a'); - a.href = audio.audioUrl; - a.download = `audio_${audio.voiceLabel}_${new Date(audio.timestamp).getTime()}.mp3`; + a.href = audio.audio_url; + a.download = `audio_${audio.voice}_${new Date(audio.created_at).getTime()}.mp3`; document.body.appendChild(a); a.click(); document.body.removeChild(a); - + toast({ title: "Download iniciado", description: "O arquivo de áudio está sendo baixado.", }); }; - const handleDeleteAudio = (audioId: string) => { - if (generatedAudio?.id === audioId) { - setGeneratedAudio(null); + const handleDeleteAudio = async (audioId: string) => { + try { + const result = await audioGenerationService.deleteAudio(audioId); + + if (result.success) { + toast({ + title: "Áudio removido", + description: "O áudio foi removido com sucesso.", + }); + + // Se o áudio deletado for o último gerado, limpa o preview + if (lastGeneratedAudio && lastGeneratedAudio.id === audioId) { + setLastGeneratedAudio(null); + } + + // Recarrega a lista de áudios + await loadAudios(); + } else { + throw new Error(result.message || 'Erro ao deletar áudio'); + } + } catch (error: any) { + console.error('Erro ao deletar áudio:', error); + toast({ + title: "Erro ao remover", + description: error.message || "Não foi possível remover o áudio.", + variant: "destructive", + }); } - const newHistory = audioHistory.filter(a => a.id !== audioId); - setAudioHistory(newHistory); - localStorage.setItem('audioHistory', JSON.stringify(newHistory)); - - toast({ - title: "Áudio removido", - description: "O áudio foi removido do histórico.", - }); }; - const filteredAudios = audioHistory.filter(item => - item.voiceLabel.toLowerCase().includes(audioSearchQuery.toLowerCase()) || - item.text.toLowerCase().includes(audioSearchQuery.toLowerCase()) - ); + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const handlePerPageChange = (value: string) => { + setPerPage(parseInt(value)); + setCurrentPage(1); + }; + + const filteredAudios = Array.isArray(audioHistory) + ? audioHistory.filter(item => + (VOICE_OPTIONS[item.voice]?.label || item.voice).toLowerCase().includes(audioSearchQuery.toLowerCase()) || + item.input_text.toLowerCase().includes(audioSearchQuery.toLowerCase()) + ) + : []; return (
@@ -222,30 +279,30 @@ export const GenerationView = () => { )}
- {generatedAudio && ( + {lastGeneratedAudio && (

Áudio Gerado

- Voz: {generatedAudio.voiceLabel} + Voz: {VOICE_OPTIONS[lastGeneratedAudio.voice]?.label || lastGeneratedAudio.voice}

- -
-

{generatedAudio.text}

+

{lastGeneratedAudio.input_text}

@@ -269,16 +326,10 @@ export const GenerationView = () => { )} - -
-
-

Histórico de Áudios

-

- {audioHistory.length} {audioHistory.length === 1 ? 'item' : 'itens'} -

-
- -
+ + {/* Search and Filters */} +
+
{ className="pl-9" />
- - {filteredAudios.length === 0 ? ( -
- -

{audioSearchQuery ? 'Nenhum áudio encontrado' : 'Nenhum áudio no histórico'}

-
- ) : ( -
- {filteredAudios.map((audio) => ( -
-
-
-
-

Voz: {audio.voiceLabel}

-
-

- {new Date(audio.timestamp).toLocaleDateString('pt-BR')} às{' '} - {new Date(audio.timestamp).toLocaleTimeString('pt-BR')} -

-
-
- - -
-
-

- {audio.text} -

- -
- ))} -
- )} +
+ + {/* Loading State */} + {isLoadingAudios ? ( +
+
+

Carregando áudios...

+
+ ) : ( + <> + {filteredAudios.length === 0 ? ( +
+ +

+ {audioSearchQuery ? 'Nenhum áudio encontrado' : 'Nenhum áudio gerado ainda'} +

+
+ ) : ( + <> +
+ {filteredAudios.map((audio) => ( +
+
+
+
+

+ Voz: {VOICE_OPTIONS[audio.voice]?.label || audio.voice} +

+
+

+ {new Date(audio.created_at).toLocaleDateString('pt-BR')} às{' '} + {new Date(audio.created_at).toLocaleTimeString('pt-BR')} +

+
+
+ + +
+
+

+ {audio.input_text} +

+ +
+ ))} +
+ + {/* Pagination */} + {!audioSearchQuery && audioHistory.length >= perPage && ( +
+ + + + + + + + + Página {currentPage} + + + + + + + + +
+ )} + + )} + + )}
); -}; \ No newline at end of file +}; diff --git a/src/components/audio/TranscriptionView.tsx b/src/components/audio/TranscriptionView.tsx index a5125c7..a0be9e3 100644 --- a/src/components/audio/TranscriptionView.tsx +++ b/src/components/audio/TranscriptionView.tsx @@ -2,37 +2,67 @@ import { useState, useEffect } from "react"; import { ChatHeader } from "@/components/ChatHeader"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Upload, Download, Trash2, FileAudio, Copy, Search } from "lucide-react"; +import { Upload, Download, Trash2, FileAudio, Copy, Search, ChevronLeft, ChevronRight } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationItem, +} from "@/components/ui/pagination"; import { useToast } from "@/hooks/use-toast"; -import { transcriptionService } from "@/services/transcription"; +import { transcriptionService, TranscriptionRecord } from "@/services/transcription"; const SUPPORTED_FORMATS = ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm']; -const MAX_FILE_SIZE = 25 * 1024 * 1024; // 25 MB - -interface TranscriptionResult { - id: string; - fileName: string; - text: string; - timestamp: Date; - audioUrl?: string; -} export const TranscriptionView = () => { const [isTranscribing, setIsTranscribing] = useState(false); - const [transcriptionResult, setTranscriptionResult] = useState(null); + const [lastTranscriptionResult, setLastTranscriptionResult] = useState(null); const [selectedFile, setSelectedFile] = useState(null); - const [transcriptionHistory, setTranscriptionHistory] = useState([]); + const [transcriptionHistory, setTranscriptionHistory] = useState([]); + const [isLoadingTranscriptions, setIsLoadingTranscriptions] = useState(false); const [transcriptionSearchQuery, setTranscriptionSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [perPage, setPerPage] = useState(10); const { toast } = useToast(); - useEffect(() => { - const savedTranscriptions = localStorage.getItem('transcriptionHistory'); - if (savedTranscriptions) { - setTranscriptionHistory(JSON.parse(savedTranscriptions)); + // Carrega transcrições do banco de dados + const loadTranscriptions = async (page: number = currentPage, limit: number = perPage) => { + setIsLoadingTranscriptions(true); + try { + const fetchedTranscriptions = await transcriptionService.getTranscriptions(undefined, page, limit); + + // Garante que sempre seja um array + if (Array.isArray(fetchedTranscriptions)) { + setTranscriptionHistory(fetchedTranscriptions); + } else { + console.warn('Resposta da API não é um array:', fetchedTranscriptions); + setTranscriptionHistory([]); + } + } catch (error: any) { + console.error('Erro ao carregar transcrições:', error); + toast({ + title: "Erro ao carregar histórico", + description: error.message || "Não foi possível carregar o histórico de transcrições.", + variant: "destructive", + }); + setTranscriptionHistory([]); + } finally { + setIsLoadingTranscriptions(false); } - }, []); + }; + + // Carrega transcrições ao montar e quando a paginação mudar + useEffect(() => { + loadTranscriptions(); + }, [currentPage, perPage]); const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -64,24 +94,31 @@ export const TranscriptionView = () => { // Verifica se a transcrição foi bem-sucedida if (response.success) { - const result: TranscriptionResult = { + // Cria objeto da transcrição recém-gerada para exibição imediata + const newTranscription: TranscriptionRecord = { id: response.transcription_id, - fileName: file.name, - text: response.message, - timestamp: new Date(), - audioUrl: response.audio_url, + user_email: '', + estabelecimento_id: 0, + audio_file_name: file.name, + audio_duration_seconds: 0, + transcription_text: response.message, + model: 'whisper-1', + audio_url: response.audio_url, + cost_usd: '0', + created_at: new Date().toISOString(), }; - setTranscriptionResult(result); - - const newHistory = [result, ...transcriptionHistory].slice(0, 10); - setTranscriptionHistory(newHistory); - localStorage.setItem('transcriptionHistory', JSON.stringify(newHistory)); + // Salva a última transcrição para exibição + setLastTranscriptionResult(newTranscription); toast({ title: "Transcrição concluída", description: "Seu áudio foi transcrito com sucesso!", }); + + // Recarrega a lista de transcrições (sem aguardar para não bloquear a UI) + loadTranscriptions(1, perPage); + setCurrentPage(1); } else { throw new Error(response.message || 'Erro ao transcrever áudio'); } @@ -99,52 +136,80 @@ export const TranscriptionView = () => { }; const handleDeleteTranscription = () => { - setTranscriptionResult(null); + setLastTranscriptionResult(null); setSelectedFile(null); }; - const handleDeleteTranscriptionFromHistory = (transcriptionId: string) => { - if (transcriptionResult?.id === transcriptionId) { - setTranscriptionResult(null); - } - const newHistory = transcriptionHistory.filter(t => t.id !== transcriptionId); - setTranscriptionHistory(newHistory); - localStorage.setItem('transcriptionHistory', JSON.stringify(newHistory)); - - toast({ - title: "Transcrição removida", - description: "A transcrição foi removida do histórico.", - }); - }; + const handleDeleteTranscriptionFromHistory = async (transcriptionId: string) => { + try { + const result = await transcriptionService.deleteTranscription(transcriptionId); - const handleCopyTranscription = () => { - if (transcriptionResult) { - navigator.clipboard.writeText(transcriptionResult.text); + if (result.success) { + toast({ + title: "Transcrição removida", + description: "A transcrição foi removida com sucesso.", + }); + + // Se a transcrição deletada for a última gerada, limpa o preview + if (lastTranscriptionResult && lastTranscriptionResult.id === transcriptionId) { + setLastTranscriptionResult(null); + } + + // Recarrega a lista de transcrições + await loadTranscriptions(); + } else { + throw new Error(result.message || 'Erro ao deletar transcrição'); + } + } catch (error: any) { + console.error('Erro ao deletar transcrição:', error); toast({ - title: "Texto copiado", - description: "A transcrição foi copiada para a área de transferência", + title: "Erro ao remover", + description: error.message || "Não foi possível remover a transcrição.", + variant: "destructive", }); } }; - const handleDownloadTranscription = () => { - if (transcriptionResult) { - const blob = new Blob([transcriptionResult.text], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `transcricao_${transcriptionResult.fileName}.txt`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } + const handleCopyTranscription = (text: string) => { + navigator.clipboard.writeText(text); + toast({ + title: "Texto copiado", + description: "A transcrição foi copiada para a área de transferência", + }); }; - const filteredTranscriptions = transcriptionHistory.filter(item => - item.fileName.toLowerCase().includes(transcriptionSearchQuery.toLowerCase()) || - item.text.toLowerCase().includes(transcriptionSearchQuery.toLowerCase()) - ); + const handleDownloadTranscription = (transcription: TranscriptionRecord) => { + const blob = new Blob([transcription.transcription_text], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `transcricao_${transcription.audio_file_name}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + toast({ + title: "Download iniciado", + description: "O arquivo de transcrição está sendo baixado.", + }); + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const handlePerPageChange = (value: string) => { + setPerPage(parseInt(value)); + setCurrentPage(1); + }; + + const filteredTranscriptions = Array.isArray(transcriptionHistory) + ? transcriptionHistory.filter(item => + item.audio_file_name.toLowerCase().includes(transcriptionSearchQuery.toLowerCase()) || + item.transcription_text.toLowerCase().includes(transcriptionSearchQuery.toLowerCase()) + ) + : []; return (
@@ -198,7 +263,7 @@ export const TranscriptionView = () => { {selectedFile ? selectedFile.name : "Clique para selecionar ou arraste o arquivo"}

- {selectedFile + {selectedFile ? `${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` : "Máximo 25 MB" } @@ -222,37 +287,37 @@ export const TranscriptionView = () => { )}

- {transcriptionResult && ( + {lastTranscriptionResult && (
-

{transcriptionResult.fileName}

+

{lastTranscriptionResult.audio_file_name}

- Transcrito {new Date(transcriptionResult.timestamp).toLocaleTimeString('pt-BR')} + Transcrito {new Date(lastTranscriptionResult.created_at).toLocaleTimeString('pt-BR')}

- - -

- {transcriptionResult.text} + {lastTranscriptionResult.transcription_text}

)} - -
-
-

Histórico de Transcrições

-

- {transcriptionHistory.length} {transcriptionHistory.length === 1 ? 'item' : 'itens'} -

-
- -
+ + {/* Search and Filters */} +
+
{ className="pl-9" />
+ +
- {filteredTranscriptions.length === 0 ? ( -
- -

{transcriptionSearchQuery ? 'Nenhuma transcrição encontrada' : 'Nenhuma transcrição no histórico'}

-
- ) : ( -
- {filteredTranscriptions.map((item) => ( -
-
-
-

{item.fileName}

-

- {new Date(item.timestamp).toLocaleDateString('pt-BR')} às{' '} - {new Date(item.timestamp).toLocaleTimeString('pt-BR')} + {/* Loading State */} + {isLoadingTranscriptions ? ( +

+
+

Carregando transcrições...

+
+ ) : ( + <> + {filteredTranscriptions.length === 0 ? ( +
+ +

+ {transcriptionSearchQuery ? 'Nenhuma transcrição encontrada' : 'Nenhuma transcrição no histórico'} +

+
+ ) : ( + <> +
+ {filteredTranscriptions.map((item) => ( +
+
+
+

{item.audio_file_name}

+

+ {new Date(item.created_at).toLocaleDateString('pt-BR')} às{' '} + {new Date(item.created_at).toLocaleTimeString('pt-BR')} +

+
+
+ + + +
+
+

+ {item.transcription_text}

- -
-

- {item.text} -

+ ))}
- ))} -
- )} -
+ + {/* Pagination */} + {!transcriptionSearchQuery && transcriptionHistory.length >= perPage && ( +
+ + + + + + + + + Página {currentPage} + + + + + + + + +
+ )} + + )} + + )}
); -}; \ No newline at end of file +}; diff --git a/src/components/chat/ChatSidebar.tsx b/src/components/chat/ChatSidebar.tsx index fbcb3c4..198746f 100644 --- a/src/components/chat/ChatSidebar.tsx +++ b/src/components/chat/ChatSidebar.tsx @@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useToast } from "@/hooks/use-toast"; -import { chatService, StoredChat, StoredFolder } from "@/services/chat"; +import { chatService, ChatRecord, FolderRecord } from "@/services/chat"; import { formatDistanceToNow } from "date-fns"; import { ptBR } from "date-fns/locale"; import { @@ -38,7 +38,7 @@ interface ChatSidebarProps { isCollapsed: boolean; onToggleCollapse: () => void; onNewChat: () => void; - onSelectChat?: (chat: StoredChat) => void; + onSelectChat?: (chat: ChatRecord) => void; currentChatId?: string; } @@ -46,20 +46,19 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect const { toast } = useToast(); const [searchQuery, setSearchQuery] = useState(""); const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false); - const [isEditFolderOpen, setIsEditFolderOpen] = useState(false); const [isDeleteFolderOpen, setIsDeleteFolderOpen] = useState(false); const [isDeleteChatOpen, setIsDeleteChatOpen] = useState(false); const [newFolderName, setNewFolderName] = useState(""); - const [editingFolder, setEditingFolder] = useState(null); - const [deletingFolder, setDeletingFolder] = useState(null); - const [deletingChat, setDeletingChat] = useState(null); + const [deletingFolder, setDeletingFolder] = useState(null); + const [deletingChat, setDeletingChat] = useState(null); + const [isLoading, setIsLoading] = useState(false); - // Estado carregado do localStorage via chatService - const [chats, setChats] = useState([]); - const [folders, setFolders] = useState([]); + // Estado carregado do banco de dados via chatService + const [chats, setChats] = useState([]); + const [folders, setFolders] = useState([]); const [expandedFolders, setExpandedFolders] = useState>(new Set()); - // Carrega chats e pastas do localStorage quando o componente monta + // Carrega chats e pastas do banco de dados quando o componente monta useEffect(() => { loadChatsAndFolders(); @@ -76,28 +75,39 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect }; }, []); - const loadChatsAndFolders = () => { - const loadedChats = chatService.getAllChats(); - const loadedFolders = chatService.getAllFolders(); + const loadChatsAndFolders = async () => { + setIsLoading(true); + try { + const data = await chatService.getChatsAndFolders(); - setChats(loadedChats); - setFolders(loadedFolders); + console.log('Dados carregados:', data); - // Expande todas as pastas por padrão - setExpandedFolders(new Set(loadedFolders.map(f => f.id))); + setChats(data.chats); + setFolders(data.folders); + + // Expande todas as pastas por padrão + setExpandedFolders(new Set(data.folders.map(f => f.id))); + } catch (error: any) { + console.error('Erro ao carregar chats e pastas:', error); + toast({ + title: "Erro ao carregar dados", + description: error.message || "Não foi possível carregar chats e pastas.", + variant: "destructive", + }); + + // Define arrays vazios em caso de erro + setChats([]); + setFolders([]); + } finally { + setIsLoading(false); + } }; - const handleCreateFolder = () => { + const handleCreateFolder = async () => { if (newFolderName.trim()) { try { - const newFolder: StoredFolder = { - id: chatService.generateChatId(), // Usa mesmo gerador de ID - name: newFolderName, - createdAt: new Date(), - chatIds: [], - }; - chatService.saveFolder(newFolder); - loadChatsAndFolders(); + await chatService.createFolder(newFolderName); + await loadChatsAndFolders(); setNewFolderName(""); setIsCreateFolderOpen(false); @@ -105,82 +115,41 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect title: "Pasta criada", description: `Pasta "${newFolderName}" criada com sucesso.`, }); - } catch (error) { + } catch (error: any) { + console.error('Erro ao criar pasta:', error); toast({ title: "Erro ao criar pasta", - description: "Não foi possível criar a pasta. Tente novamente.", + description: error.message || "Não foi possível criar a pasta. Tente novamente.", variant: "destructive", }); } } }; - const handleEditFolder = () => { - if (editingFolder && newFolderName.trim()) { - try { - const updatedFolder: StoredFolder = { - ...editingFolder, - name: newFolderName, - }; - chatService.saveFolder(updatedFolder); - loadChatsAndFolders(); - setNewFolderName(""); - setEditingFolder(null); - setIsEditFolderOpen(false); - - toast({ - title: "Pasta renomeada", - description: `Pasta renomeada para "${newFolderName}".`, - }); - } catch (error) { - toast({ - title: "Erro ao renomear pasta", - description: "Não foi possível renomear a pasta. Tente novamente.", - variant: "destructive", - }); - } - } - }; - - const handleDeleteFolder = () => { + const handleDeleteFolder = async () => { if (deletingFolder) { try { - // Remove chats da pasta (volta para "Sem Pasta") - const updatedChats = chats.map(chat => { - if (chat.folderId === deletingFolder.id) { - const updated = { ...chat, folderId: undefined }; - chatService.saveChat(updated); - return updated; - } - return chat; - }); - - chatService.deleteFolder(deletingFolder.id); - loadChatsAndFolders(); + await chatService.deleteFolder(deletingFolder.id); + await loadChatsAndFolders(); setDeletingFolder(null); setIsDeleteFolderOpen(false); toast({ title: "Pasta excluída", - description: "As conversas foram movidas para 'Sem Pasta'.", + description: "A pasta foi excluída com sucesso.", }); - } catch (error) { + } catch (error: any) { + console.error('Erro ao excluir pasta:', error); toast({ title: "Erro ao excluir pasta", - description: "Não foi possível excluir a pasta. Tente novamente.", + description: error.message || "Não foi possível excluir a pasta. Tente novamente.", variant: "destructive", }); } } }; - const openEditFolder = (folder: StoredFolder) => { - setEditingFolder(folder); - setNewFolderName(folder.name); - setIsEditFolderOpen(true); - }; - - const openDeleteFolder = (folder: StoredFolder) => { + const openDeleteFolder = (folder: FolderRecord) => { setDeletingFolder(folder); setIsDeleteFolderOpen(true); }; @@ -195,62 +164,31 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect setExpandedFolders(newExpanded); }; - const moveToFolder = (chatId: string, folderId: string) => { + const moveToFolder = async (chatId: string, folderId: string) => { try { - const chat = chats.find(c => c.id === chatId); - if (chat) { - const updatedChat: StoredChat = { - ...chat, - folderId: folderId, - }; - chatService.saveChat(updatedChat); - loadChatsAndFolders(); + await chatService.moveChatToFolder(chatId, folderId); + await loadChatsAndFolders(); - const folder = folders.find(f => f.id === folderId); - toast({ - title: "Chat movido", - description: `Movido para a pasta "${folder?.name}".`, - }); - } - } catch (error) { + const folder = folders.find(f => f.id === folderId); + toast({ + title: "Chat movido", + description: `Movido para a pasta "${folder?.name}".`, + }); + } catch (error: any) { + console.error('Erro ao mover chat:', error); toast({ title: "Erro ao mover chat", - description: "Não foi possível mover o chat. Tente novamente.", + description: error.message || "Não foi possível mover o chat. Tente novamente.", variant: "destructive", }); } }; - const removeFromFolder = (chatId: string) => { - try { - const chat = chats.find(c => c.id === chatId); - if (chat) { - const updatedChat: StoredChat = { - ...chat, - folderId: undefined, - }; - chatService.saveChat(updatedChat); - loadChatsAndFolders(); - - toast({ - title: "Chat removido da pasta", - description: "Chat movido para 'Sem Pasta'.", - }); - } - } catch (error) { - toast({ - title: "Erro ao remover chat", - description: "Não foi possível remover o chat da pasta.", - variant: "destructive", - }); - } - }; - - const handleDeleteChat = () => { + const handleDeleteChat = async () => { if (deletingChat) { try { - chatService.deleteChat(deletingChat.id); - loadChatsAndFolders(); + await chatService.deleteChat(deletingChat.id); + await loadChatsAndFolders(); setDeletingChat(null); setIsDeleteChatOpen(false); @@ -258,22 +196,23 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect title: "Chat excluído", description: "A conversa foi excluída com sucesso.", }); - } catch (error) { + } catch (error: any) { + console.error('Erro ao excluir chat:', error); toast({ title: "Erro ao excluir chat", - description: "Não foi possível excluir o chat. Tente novamente.", + description: error.message || "Não foi possível excluir o chat. Tente novamente.", variant: "destructive", }); } } }; - const openDeleteChat = (chat: StoredChat) => { + const openDeleteChat = (chat: ChatRecord) => { setDeletingChat(chat); setIsDeleteChatOpen(true); }; - const handleSelectChat = (chat: StoredChat) => { + const handleSelectChat = (chat: ChatRecord) => { if (onSelectChat) { onSelectChat(chat); } @@ -283,20 +222,17 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect const filteredChats = chats.filter((chat) => { const searchLower = searchQuery.toLowerCase(); const titleMatch = chat.title.toLowerCase().includes(searchLower); - const contentMatch = chat.messages.some(msg => - msg.content.toLowerCase().includes(searchLower) - ); - return titleMatch || contentMatch; + return titleMatch; }); - // Separar chats sem pasta - const chatsWithoutFolder = filteredChats.filter((c) => !c.folderId); + // Separar chats sem pasta (folder_id é null) + const chatsWithoutFolder = filteredChats.filter((c) => c.folder_id === null); - // Agrupar chats por pasta + // Agrupar chats por pasta (quando folder_id === folder.id) const chatsByFolder = folders.reduce((acc, folder) => { - acc[folder.id] = filteredChats.filter((c) => c.folderId === folder.id); + acc[folder.id] = filteredChats.filter((c) => c.folder_id === folder.id); return acc; - }, {} as Record); + }, {} as Record); if (isCollapsed) { return ( @@ -382,45 +318,13 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect - {/* Edit Folder Dialog */} - - - - Editar Pasta - - Renomeie sua pasta de conversas. - - -
-
- - setNewFolderName(e.target.value)} - placeholder="Digite o novo nome..." - onKeyDown={(e) => e.key === "Enter" && handleEditFolder()} - /> -
-
- - - - -
-
- {/* Delete Folder Alert */} Excluir Pasta? - Esta ação não pode ser desfeita. As conversas dentro da pasta serão movidas para "Sem Pasta". + Esta ação não pode ser desfeita. A pasta "{deletingFolder?.name}" será excluída. @@ -436,57 +340,78 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect {/* Conversations List */}
- {/* Folders */} - {folders.map((folder) => ( -
-
- + + + + + + + openDeleteFolder(folder)} + > + + Excluir Pasta + + +
- - {folder.name} - - - {chatsByFolder[folder.id]?.length || 0} - - - - - - - - openEditFolder(folder)} - > - - Editar Nome - - openDeleteFolder(folder)} - > - - Excluir Pasta - - - -
+ {expandedFolders.has(folder.id) && chatsByFolder[folder.id]?.length > 0 && ( +
+ {chatsByFolder[folder.id].map((chat) => ( + handleSelectChat(chat)} + folders={folders} + onMoveToFolder={moveToFolder} + onDelete={() => openDeleteChat(chat)} + /> + ))} +
+ )} +
+ ))} - {expandedFolders.has(folder.id) && chatsByFolder[folder.id]?.length > 0 && ( -
- {chatsByFolder[folder.id].map((chat) => ( + {/* Chats without folder */} + {chatsWithoutFolder.length > 0 && ( +
+
+ Sem Pasta +
+ {chatsWithoutFolder.map((chat) => ( handleSelectChat(chat)} folders={folders} onMoveToFolder={moveToFolder} - onRemoveFromFolder={removeFromFolder} onDelete={() => openDeleteChat(chat)} /> ))}
)} -
- ))} - {/* Chats without folder */} - {chatsWithoutFolder.length > 0 && ( -
-
- Sem Pasta -
- {chatsWithoutFolder.map((chat) => ( - handleSelectChat(chat)} - folders={folders} - onMoveToFolder={moveToFolder} - onRemoveFromFolder={removeFromFolder} - onDelete={() => openDeleteChat(chat)} - /> - ))} -
+ {/* Empty state */} + {!isLoading && chats.length === 0 && ( +
+ +

+ Nenhuma conversa ainda +

+

+ Clique em "Novo Chat" para começar +

+
+ )} + )}
@@ -548,12 +464,11 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect }; interface ChatItemProps { - chat: StoredChat; + chat: ChatRecord; isSelected: boolean; onSelect: () => void; - folders: StoredFolder[]; + folders: FolderRecord[]; onMoveToFolder: (chatId: string, folderId: string) => void; - onRemoveFromFolder: (chatId: string) => void; onDelete: () => void; } @@ -563,50 +478,29 @@ const ChatItem = ({ onSelect, folders, onMoveToFolder, - onRemoveFromFolder, onDelete, }: ChatItemProps) => { - // Pega a última mensagem do usuário - const lastUserMessage = chat.messages - .filter(m => m.role === 'user') - .slice(-1)[0]; - // Formata timestamp relativo - const timeAgo = formatDistanceToNow(new Date(chat.updatedAt), { + const timeAgo = formatDistanceToNow(new Date(chat.updated_at), { addSuffix: true, locale: ptBR, }); - // Limita o título a 40 caracteres - const truncatedTitle = chat.title.length > 40 - ? chat.title.substring(0, 40) + '...' - : chat.title; - - // Limita a mensagem a 50 caracteres - const truncatedMessage = lastUserMessage?.content.length > 50 - ? lastUserMessage.content.substring(0, 50) + '...' - : lastUserMessage?.content; - return (
- -
+ +

- {truncatedTitle} + {chat.title}

- {lastUserMessage && ( -

- {truncatedMessage} -

- )} -

+

{timeAgo}

@@ -622,17 +516,8 @@ const ChatItem = ({ - {chat.folderId && ( - onRemoveFromFolder(chat.id)} - > - - Remover da Pasta - - )} - - {folders.map((folder) => ( + {/* Opções para mover para pastas */} + {folders.filter(f => f.id !== chat.folder_id).map((folder) => ( { const [isLoading, setIsLoading] = useState(false); - // Salva o chat no localStorage sempre que as mensagens mudam - useEffect(() => { - if (messages.length > 1) { // Salva apenas se houver mensagens além da inicial - saveCurrentChat(); - // Dispara evento customizado para a sidebar recarregar - window.dispatchEvent(new Event('chatUpdated')); - } - }, [messages]); + // NOTA: Salvamento automático desabilitado - mensagens já são salvas na API + // quando enviadas via handleSendMessage + // useEffect(() => { + // if (messages.length > 1) { + // saveCurrentChat(); + // window.dispatchEvent(new Event('chatUpdated')); + // } + // }, [messages]); - // Função para salvar o chat atual - const saveCurrentChat = () => { - try { - const chatTitle = chatService.generateChatTitle( - messages.find(m => m.role === 'user')?.content || 'Nova Conversa' - ); - - const storedChat: StoredChat = { - id: currentChatId, - title: chatTitle, - createdAt: new Date(), - updatedAt: new Date(), - model: selectedModel, - systemPrompt: systemPrompt, - messages: messages.map(msg => ({ - ...msg, - timestamp: new Date(), - })), - }; - - chatService.saveChat(storedChat); - } catch (error) { - console.error('Erro ao salvar chat:', error); - } - }; + // NOTA: Função de salvamento localStorage desabilitada - migrado para banco de dados + // const saveCurrentChat = () => { + // try { + // const chatTitle = chatService.generateChatTitle( + // messages.find(m => m.role === 'user')?.content || 'Nova Conversa' + // ); + // const storedChat: StoredChat = { + // id: currentChatId, + // title: chatTitle, + // createdAt: new Date(), + // updatedAt: new Date(), + // model: selectedModel, + // systemPrompt: systemPrompt, + // messages: messages.map(msg => ({ + // ...msg, + // timestamp: new Date(), + // })), + // }; + // chatService.saveChat(storedChat); + // } catch (error) { + // console.error('Erro ao salvar chat:', error); + // } + // }; const handleNewChat = () => { // Reseta para "0" - novo chat sempre começa com chat_id "0" @@ -89,22 +87,56 @@ export const ChatView = () => { window.dispatchEvent(new Event('chatUpdated')); }; - const handleLoadChat = (chat: StoredChat) => { - // Carrega um chat existente do histórico + const handleLoadChat = async (chat: ChatRecord) => { + // Carrega um chat existente do banco de dados setCurrentChatId(chat.id); - setSelectedModel(chat.model); - setSystemPrompt(chat.systemPrompt); - // Converte mensagens do StoredChat para Message - const loadedMessages: Message[] = chat.messages.map(msg => ({ - id: msg.id, - role: msg.role, - content: msg.content, - model: msg.model, - attachments: msg.attachments, - })); + // Limpa mensagens enquanto carrega + setMessages([]); + setIsLoading(true); - setMessages(loadedMessages); + try { + // Busca mensagens do chat na API + const messagesFromAPI = await chatService.getChatMessages(chat.id); + + console.log('Mensagens carregadas da API:', messagesFromAPI); + + // Converte MessageRecord para Message + const loadedMessages: Message[] = messagesFromAPI.map((msg: MessageRecord) => ({ + id: msg.id, + role: msg.role, + content: msg.content, + model: undefined, // model_id vem como número, não temos mapeamento reverso + attachments: msg.has_attachments ? [] : undefined, // Não temos detalhes dos anexos no GET + })); + + setMessages(loadedMessages); + + toast({ + title: "Chat carregado", + description: `${loadedMessages.length} mensagens carregadas.`, + }); + } catch (error: any) { + console.error('Erro ao carregar mensagens:', error); + + toast({ + title: "Erro ao carregar chat", + description: error.message || "Não foi possível carregar as mensagens.", + variant: "destructive", + }); + + // Inicia com mensagem padrão em caso de erro + setMessages([ + { + id: "1", + role: "assistant", + content: "Olá! Sou o assistente HGTX Codex. Como posso ajudá-lo hoje?", + model: selectedModel, + }, + ]); + } finally { + setIsLoading(false); + } }; const handleSendMessage = async (content: string, files?: File[]) => { @@ -142,7 +174,18 @@ export const ChatView = () => { // Isso mantém o contexto da conversa para as próximas mensagens if (response.chat_id && response.chat_id !== currentChatId) { console.log(`Chat ID atualizado: ${currentChatId} → ${response.chat_id}`); + + // Se estava com chat_id "0", significa que é a primeira mensagem + // e o chat acabou de ser criado no backend + const isFirstMessage = currentChatId === "0"; + setCurrentChatId(response.chat_id); + + // Dispara evento para ChatSidebar recarregar e mostrar o novo chat + if (isFirstMessage) { + console.log('Primeira mensagem - novo chat criado, atualizando sidebar'); + window.dispatchEvent(new Event('chatUpdated')); + } } const aiResponse: Message = { diff --git a/src/components/images/ImageView.tsx b/src/components/images/ImageView.tsx index 2fa7a5b..f0a64e6 100644 --- a/src/components/images/ImageView.tsx +++ b/src/components/images/ImageView.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { ChatHeader } from "@/components/ChatHeader"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; -import { Download, Trash2, Sparkles, Clock, Search } from "lucide-react"; +import { Download, Trash2, Sparkles, Clock, Search, ChevronLeft, ChevronRight } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Input } from "@/components/ui/input"; import { @@ -14,41 +14,61 @@ import { } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useToast } from "@/hooks/use-toast"; -import { imageGenerationService, IMAGE_SIZE_OPTIONS, ImageSize } from "@/services/imageGeneration"; - -interface GeneratedImage { - id: string; - url: string; - prompt: string; - size: ImageSize; - timestamp: Date; -} +import { + imageGenerationService, + IMAGE_SIZE_OPTIONS, + ImageSize, + ImageRecord +} from "@/services/imageGeneration"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, +} from "@/components/ui/pagination"; export const ImageView = () => { const [prompt, setPrompt] = useState(""); const [isGenerating, setIsGenerating] = useState(false); const [selectedSize, setSelectedSize] = useState("1024x1024"); const [searchQuery, setSearchQuery] = useState(""); - const [images, setImages] = useState([]); + const [images, setImages] = useState([]); + const [lastGeneratedImage, setLastGeneratedImage] = useState(null); + const [isLoadingImages, setIsLoadingImages] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [perPage, setPerPage] = useState(10); const { toast } = useToast(); - // Carrega histórico do localStorage ao montar o componente - useEffect(() => { - const savedImages = localStorage.getItem('imageHistory'); - if (savedImages) { - try { - const parsedImages = JSON.parse(savedImages); - // Converte strings de data de volta para Date objects - const imagesWithDates = parsedImages.map((img: any) => ({ - ...img, - timestamp: new Date(img.timestamp), - })); - setImages(imagesWithDates); - } catch (error) { - console.error('Erro ao carregar histórico de imagens:', error); + // Carrega imagens do banco de dados ao montar o componente + const loadImages = async (page: number = currentPage, limit: number = perPage) => { + setIsLoadingImages(true); + try { + const fetchedImages = await imageGenerationService.getImages(undefined, page, limit); + + // Garante que sempre seja um array + if (Array.isArray(fetchedImages)) { + setImages(fetchedImages); + } else { + console.warn('Resposta da API não é um array:', fetchedImages); + setImages([]); } + } catch (error: any) { + console.error('Erro ao carregar imagens:', error); + toast({ + title: "Erro ao carregar histórico", + description: error.message || "Não foi possível carregar o histórico de imagens.", + variant: "destructive", + }); + setImages([]); + } finally { + setIsLoadingImages(false); } - }, []); + }; + + // Carrega imagens ao montar e quando a paginação mudar + useEffect(() => { + loadImages(); + }, [currentPage, perPage]); const handleGenerate = async () => { // Valida a descrição antes de enviar @@ -73,28 +93,36 @@ export const ImageView = () => { // Verifica se a geração foi bem-sucedida if (response.success) { - // Log da URL da imagem para debug - console.log('URL da imagem gerada:', response.image_url); - - const newImage: GeneratedImage = { - id: response.image_generation_id, - url: response.image_url, - prompt: response.message, - size: selectedSize, - timestamp: new Date(), - }; - - const newHistory = [newImage, ...images].slice(0, 20); // Mantém apenas as últimas 20 imagens - setImages(newHistory); - localStorage.setItem('imageHistory', JSON.stringify(newHistory)); - toast({ title: "Imagem gerada com sucesso", description: `Tamanho: ${IMAGE_SIZE_OPTIONS[selectedSize].label}`, }); + // Cria objeto da imagem recém-gerada para exibição imediata + const newGeneratedImage: ImageRecord = { + id: response.image_generation_id, + user_email: '', // Será preenchido pelo backend + estabelecimento_id: 0, // Será preenchido pelo backend + description: response.message, // Descrição original + model: 'dall-e-3', // Modelo padrão + image_url: response.image_url, + size: selectedSize, + cost_usd: '0', + total_tokens: 0, + input_tokens: 0, + output_tokens: 0, + created_at: new Date().toISOString(), + }; + + // Salva a última imagem gerada para exibição + setLastGeneratedImage(newGeneratedImage); + // Limpa o campo de descrição após sucesso setPrompt(""); + + // Recarrega a lista de imagens (sem aguardar para não bloquear a UI) + loadImages(1, perPage); + setCurrentPage(1); } else { throw new Error('Erro ao gerar imagem'); } @@ -111,22 +139,41 @@ export const ImageView = () => { } }; - const handleDelete = (id: string) => { - const newHistory = images.filter((img) => img.id !== id); - setImages(newHistory); - localStorage.setItem('imageHistory', JSON.stringify(newHistory)); + const handleDelete = async (id: string) => { + try { + const result = await imageGenerationService.deleteImage(id); - toast({ - title: "Imagem removida", - description: "A imagem foi removida do histórico.", - }); + if (result.success) { + toast({ + title: "Imagem removida", + description: "A imagem foi removida com sucesso.", + }); + + // Se a imagem deletada for a última gerada, limpa o preview + if (lastGeneratedImage && lastGeneratedImage.id === id) { + setLastGeneratedImage(null); + } + + // Recarrega a lista de imagens + await loadImages(); + } else { + throw new Error(result.message || 'Erro ao deletar imagem'); + } + } catch (error: any) { + console.error('Erro ao deletar imagem:', error); + toast({ + title: "Erro ao remover", + description: error.message || "Não foi possível remover a imagem.", + variant: "destructive", + }); + } }; - const handleDownload = async (image: GeneratedImage) => { + const handleDownload = async (image: ImageRecord) => { try { await imageGenerationService.downloadImage( - image.url, - `${image.prompt.substring(0, 30)}_${image.size}.png` + image.image_url, + `${image.description.substring(0, 30)}_${image.size}.png` ); toast({ @@ -142,9 +189,20 @@ export const ImageView = () => { } }; - const filteredImages = images.filter((img) => - img.prompt.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const handlePerPageChange = (value: string) => { + setPerPage(parseInt(value)); + setCurrentPage(1); // Reset para primeira página ao mudar itens por página + }; + + const filteredImages = Array.isArray(images) + ? images.filter((img) => + img.description.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : []; return (
@@ -227,11 +285,11 @@ export const ImageView = () => { )} {/* Recent Images Preview */} - {!isGenerating && images.length > 0 && ( + {!isGenerating && lastGeneratedImage && (

Última Geração

@@ -241,36 +299,102 @@ export const ImageView = () => { {/* History Tab */} - {/* Search */} -
- - setSearchQuery(e.target.value)} - placeholder="Buscar por descrição..." - className="pl-9" - /> + {/* Search and Filters */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="Buscar por descrição..." + className="pl-9" + /> +
+
- {/* Images Grid */} - {filteredImages.length > 0 ? ( -
- {filteredImages.map((image) => ( - - ))} + {/* Loading State */} + {isLoadingImages ? ( +
+
+

Carregando imagens...

) : ( -
- -

- {searchQuery ? "Nenhuma imagem encontrada" : "Nenhuma imagem gerada ainda"} -

-
+ <> + {/* Images Grid */} + {filteredImages.length > 0 ? ( + <> +
+ {filteredImages.map((image) => ( + + ))} +
+ + {/* Pagination */} + {!searchQuery && images.length >= perPage && ( +
+ + + + + + + + + Página {currentPage} + + + + + + + + +
+ )} + + ) : ( +
+ +

+ {searchQuery ? "Nenhuma imagem encontrada" : "Nenhuma imagem gerada ainda"} +

+
+ )} + )} @@ -282,9 +406,9 @@ export const ImageView = () => { }; interface ImageCardProps { - image: GeneratedImage; + image: ImageRecord; onDelete: (id: string) => void; - onDownload: (image: GeneratedImage) => void; + onDownload: (image: ImageRecord) => void; } const ImageCard = ({ image, onDelete, onDownload }: ImageCardProps) => { @@ -292,9 +416,10 @@ const ImageCard = ({ image, onDelete, onDownload }: ImageCardProps) => { const [imageLoading, setImageLoading] = useState(true); // Calcula tempo relativo - const getRelativeTime = (timestamp: Date) => { + const getRelativeTime = (timestamp: string) => { const now = new Date(); - const diff = now.getTime() - new Date(timestamp).getTime(); + const date = new Date(timestamp); + const diff = now.getTime() - date.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); @@ -306,13 +431,13 @@ const ImageCard = ({ image, onDelete, onDownload }: ImageCardProps) => { }; const handleImageError = () => { - console.warn('Erro CORS ao carregar imagem:', image.url); + console.warn('Erro CORS ao carregar imagem:', image.image_url); setImageError(true); setImageLoading(false); }; const handleImageLoad = () => { - console.log('Imagem carregada com sucesso:', image.url); + console.log('Imagem carregada com sucesso:', image.image_url); setImageLoading(false); setImageError(false); }; @@ -332,7 +457,7 @@ const ImageCard = ({ image, onDelete, onDownload }: ImageCardProps) => {

A imagem foi gerada, mas não pode ser exibida aqui

{ Abrir imagem em nova aba
) : ( {image.prompt} { )}
-

{image.prompt}

+

{image.description}

- {IMAGE_SIZE_OPTIONS[image.size].label} - {getRelativeTime(image.timestamp)} + {IMAGE_SIZE_OPTIONS[image.size as ImageSize]?.label || image.size} + {getRelativeTime(image.created_at)}