atualizacoes codex
This commit is contained in:
@@ -40,6 +40,7 @@ export function AreasView() {
|
|||||||
const [loadingList, setLoadingList] = useState(true);
|
const [loadingList, setLoadingList] = useState(true);
|
||||||
|
|
||||||
const totalPages = Math.max(1, totalPaginas);
|
const totalPages = Math.max(1, totalPaginas);
|
||||||
|
const isFiltering = filterNome.trim().length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -262,7 +263,7 @@ export function AreasView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto p-3 md:p-6">
|
<div className="flex-1 overflow-auto p-3 md:p-6">
|
||||||
{!loadingList && areasList.length === 0 && totalRegistros === 0 ? (
|
{!loadingList && totalRegistros === 0 && !isFiltering ? (
|
||||||
<Card className="max-w-2xl mx-auto mt-12">
|
<Card className="max-w-2xl mx-auto mt-12">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
@@ -297,6 +298,12 @@ export function AreasView() {
|
|||||||
Carregando...
|
Carregando...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
) : totalRegistros === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2} className="text-center text-muted-foreground py-8">
|
||||||
|
Nenhum resultado para o filtro {filterNome.trim() ? `"${filterNome.trim()}"` : "atual"}.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
areasList.map((item) => (
|
areasList.map((item) => (
|
||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { audioGenerationService } from "@/services/audioGeneration";
|
||||||
|
|
||||||
const SUPPORTED_FORMATS = ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm'];
|
const SUPPORTED_FORMATS = ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm'];
|
||||||
const MAX_FILE_SIZE = 25 * 1024 * 1024; // 25 MB
|
const MAX_FILE_SIZE = 25 * 1024 * 1024; // 25 MB
|
||||||
@@ -186,19 +187,23 @@ export const AudioView = () => {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadAudio = (audio: GeneratedAudio) => {
|
const handleDownloadAudio = async (audio: GeneratedAudio) => {
|
||||||
// In real implementation, download the actual audio file
|
const filename = `audio_${audio.voiceLabel}_${new Date(audio.timestamp).getTime()}.mp3`;
|
||||||
const a = document.createElement('a');
|
try {
|
||||||
a.href = audio.audioUrl;
|
await audioGenerationService.downloadAudioFile(audio.audioUrl, filename);
|
||||||
a.download = `audio_${audio.voiceLabel}_${new Date(audio.timestamp).getTime()}.mp3`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Download iniciado",
|
title: "Download iniciado",
|
||||||
description: "O arquivo de áudio está sendo baixado.",
|
description: "O arquivo de áudio está sendo baixado.",
|
||||||
});
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message =
|
||||||
|
error && typeof error === "object" && "message" in error ? String((error as { message: string }).message) : "Não foi possível baixar o áudio.";
|
||||||
|
toast({
|
||||||
|
title: "Erro ao baixar",
|
||||||
|
description: message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteAudio = (audioId: string) => {
|
const handleDeleteAudio = (audioId: string) => {
|
||||||
@@ -482,10 +487,11 @@ export const AudioView = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="gap-1"
|
className="gap-1"
|
||||||
onClick={() => handleDownloadAudio(generatedAudio)}
|
onClick={() => void handleDownloadAudio(generatedAudio)}
|
||||||
>
|
>
|
||||||
<Download className="w-3 h-3" />
|
<Download className="w-3 h-3" />
|
||||||
Baixar MP3
|
Baixar MP3
|
||||||
@@ -658,9 +664,10 @@ export const AudioView = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 shrink-0">
|
<div className="flex gap-2 shrink-0">
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleDownloadAudio(item)}
|
onClick={() => void handleDownloadAudio(item)}
|
||||||
>
|
>
|
||||||
<Download className="w-3 h-3" />
|
<Download className="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -134,18 +134,23 @@ export const GenerationView = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadAudio = (audio: AudioRecord) => {
|
const handleDownloadAudio = async (audio: AudioRecord) => {
|
||||||
const a = document.createElement('a');
|
const filename = `audio_${audio.voice}_${new Date(audio.created_at).getTime()}.mp3`;
|
||||||
a.href = audio.audio_url;
|
try {
|
||||||
a.download = `audio_${audio.voice}_${new Date(audio.created_at).getTime()}.mp3`;
|
await audioGenerationService.downloadAudioFile(audio.audio_url, filename);
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Download iniciado",
|
title: "Download iniciado",
|
||||||
description: "O arquivo de áudio está sendo baixado.",
|
description: "O arquivo de áudio está sendo baixado.",
|
||||||
});
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message =
|
||||||
|
error && typeof error === "object" && "message" in error ? String((error as { message: string }).message) : "Não foi possível baixar o áudio.";
|
||||||
|
toast({
|
||||||
|
title: "Erro ao baixar",
|
||||||
|
description: message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteAudio = async (audioId: string) => {
|
const handleDeleteAudio = async (audioId: string) => {
|
||||||
@@ -290,10 +295,11 @@ export const GenerationView = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="gap-1"
|
className="gap-1"
|
||||||
onClick={() => handleDownloadAudio(lastGeneratedAudio)}
|
onClick={() => void handleDownloadAudio(lastGeneratedAudio)}
|
||||||
>
|
>
|
||||||
<Download className="w-3 h-3" />
|
<Download className="w-3 h-3" />
|
||||||
Baixar
|
Baixar
|
||||||
@@ -385,9 +391,10 @@ export const GenerationView = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => handleDownloadAudio(audio)}
|
onClick={() => void handleDownloadAudio(audio)}
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Bot, User, Copy, File } from "lucide-react";
|
import { Bot, User, Copy, File } from "lucide-react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -53,7 +54,7 @@ export const ChatMessage = ({ role, content, model, attachments }: ChatMessagePr
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex-1 max-w-3xl space-y-1 md:space-y-2 ${isAssistant ? "" : "flex flex-col items-end"}`}>
|
<div className={`flex-1 max-w-4xl space-y-1 md:space-y-2 ${isAssistant ? "" : "flex flex-col items-end"}`}>
|
||||||
{/* Attachments */}
|
{/* Attachments */}
|
||||||
{attachments && attachments.length > 0 && (
|
{attachments && attachments.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 mb-2">
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
@@ -73,15 +74,19 @@ export const ChatMessage = ({ role, content, model, attachments }: ChatMessagePr
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`inline-block px-3 md:px-4 py-2 md:py-3 rounded-xl ${
|
className={`inline-block max-w-[90%] px-3 md:px-4 py-2 md:py-3 rounded-xl break-words ${
|
||||||
isAssistant
|
isAssistant
|
||||||
? "bg-card border border-border"
|
? "bg-card border border-border text-foreground"
|
||||||
: "bg-gradient-to-br from-primary to-secondary text-primary-foreground"
|
: "bg-gradient-to-br from-primary to-secondary text-primary-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className={`text-sm md:text-base leading-relaxed ${isAssistant ? "text-foreground" : "text-white"}`}>
|
{isAssistant ? (
|
||||||
{content}
|
<div className="prose prose-sm md:prose-base prose-neutral dark:prose-invert max-w-none prose-headings:font-semibold prose-p:leading-relaxed">
|
||||||
</p>
|
<ReactMarkdown>{content}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm md:text-base leading-relaxed text-white">{content}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showActions && (
|
{showActions && (
|
||||||
|
|||||||
@@ -133,8 +133,9 @@ export function ParecerJuridicoDetailView() {
|
|||||||
new Date(a.created_at.replace(" ", "T")).getTime() -
|
new Date(a.created_at.replace(" ", "T")).getTime() -
|
||||||
new Date(b.created_at.replace(" ", "T")).getTime()
|
new Date(b.created_at.replace(" ", "T")).getTime()
|
||||||
);
|
);
|
||||||
|
const nonEmptyMessages = sorted.filter((m) => (m.content ?? "").trim().length > 0);
|
||||||
setMessages(
|
setMessages(
|
||||||
sorted.map((m) => ({
|
nonEmptyMessages.map((m) => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
role: m.role,
|
role: m.role,
|
||||||
content: m.content,
|
content: m.content,
|
||||||
|
|||||||
@@ -155,18 +155,32 @@ export function ParecerJuridicoFormView() {
|
|||||||
<div className="flex-1 overflow-auto p-3 md:p-6">
|
<div className="flex-1 overflow-auto p-3 md:p-6">
|
||||||
<div className="max-w-4xl mx-auto flex flex-col gap-6">
|
<div className="max-w-4xl mx-auto flex flex-col gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="titulo-parecer">Título do parecer</Label>
|
<Label htmlFor="titulo-parecer" className="flex flex-wrap items-baseline gap-1">
|
||||||
|
<span>Título do parecer</span>
|
||||||
|
<span className="text-destructive font-semibold" aria-hidden>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">obrigatório</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="titulo-parecer"
|
id="titulo-parecer"
|
||||||
value={tituloParecer}
|
value={tituloParecer}
|
||||||
onChange={(e) => setTituloParecer(e.target.value)}
|
onChange={(e) => setTituloParecer(e.target.value)}
|
||||||
placeholder="Ex.: Parecer sobre contrato de prestação de serviços"
|
placeholder="Ex.: Parecer sobre contrato de prestação de serviços"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="area">Área</Label>
|
<Label htmlFor="area" className="flex flex-wrap items-baseline gap-1">
|
||||||
|
<span>Área</span>
|
||||||
|
<span className="text-destructive font-semibold" aria-hidden>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">obrigatório</span>
|
||||||
|
</Label>
|
||||||
<Select value={areaId || "none"} onValueChange={(v) => setAreaId(v === "none" ? "" : v)} disabled={areasLoading}>
|
<Select value={areaId || "none"} onValueChange={(v) => setAreaId(v === "none" ? "" : v)} disabled={areasLoading}>
|
||||||
<SelectTrigger id="area" className="w-full">
|
<SelectTrigger id="area" className="w-full">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
@@ -190,7 +204,7 @@ export function ParecerJuridicoFormView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="prompt">Prompt</Label>
|
<Label htmlFor="prompt">Prompt da base</Label>
|
||||||
<Select
|
<Select
|
||||||
value={promptId || "none"}
|
value={promptId || "none"}
|
||||||
onValueChange={(v) => setPromptId(v === "none" ? "" : v)}
|
onValueChange={(v) => setPromptId(v === "none" ? "" : v)}
|
||||||
@@ -221,29 +235,45 @@ export function ParecerJuridicoFormView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="conteudo-prompt">Conteúdo do prompt (editável)</Label>
|
<Label htmlFor="conteudo-prompt" className="flex flex-wrap items-baseline gap-1">
|
||||||
|
<span>Conteúdo do prompt</span>
|
||||||
|
<span className="text-destructive font-semibold" aria-hidden>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">obrigatório</span>
|
||||||
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="conteudo-prompt"
|
id="conteudo-prompt"
|
||||||
value={conteudoPrompt}
|
value={conteudoPrompt}
|
||||||
onChange={(e) => setConteudoPrompt(e.target.value)}
|
onChange={(e) => setConteudoPrompt(e.target.value)}
|
||||||
placeholder="Selecione um prompt acima ou edite o conteúdo aqui. Este valor será enviado na geração do parecer."
|
placeholder="Selecione um prompt acima ou edite o conteúdo aqui."
|
||||||
className="min-h-[200px] resize-y w-full"
|
className="min-h-[200px] resize-y w-full"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="instrucao">Instrução</Label>
|
<Label htmlFor="instrucao" className="flex flex-wrap items-baseline gap-1">
|
||||||
|
<span>Instrução</span>
|
||||||
|
<span className="text-destructive font-semibold" aria-hidden>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">obrigatório</span>
|
||||||
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="instrucao"
|
id="instrucao"
|
||||||
value={instrucao}
|
value={instrucao}
|
||||||
onChange={(e) => setInstrucao(e.target.value)}
|
onChange={(e) => setInstrucao(e.target.value)}
|
||||||
placeholder="Instruções adicionais para a geração do parecer..."
|
placeholder="Instruções adicionais para a geração do parecer..."
|
||||||
className="min-h-[100px] resize-y w-full"
|
className="min-h-[100px] resize-y w-full"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="anexo">Anexo (máx. 1 arquivo)</Label>
|
<Label htmlFor="anexo">Anexo</Label>
|
||||||
<p className="text-xs text-muted-foreground">Formatos permitidos: PDF, TXT.</p>
|
<p className="text-xs text-muted-foreground">Formatos permitidos: PDF, TXT.</p>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<div className="flex items-center gap-2 w-full min-h-10 rounded-md border border-input bg-background px-3 py-2">
|
<div className="flex items-center gap-2 w-full min-h-10 rounded-md border border-input bg-background px-3 py-2">
|
||||||
|
|||||||
@@ -183,10 +183,12 @@ export const PromptsView = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 sm:items-center">
|
||||||
<Button onClick={() => navigate("/codex/prompts/novo")}>
|
<Button onClick={() => navigate("/codex/prompts/novo")}>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Novo prompt
|
Novo prompt
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
@@ -207,10 +209,10 @@ export const PromptsView = () => {
|
|||||||
Carregando...
|
Carregando...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : promptsList.length === 0 ? (
|
) : totalRegistros === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="text-center text-muted-foreground py-10 text-base">
|
<TableCell colSpan={3} className="text-center text-muted-foreground py-10 text-base">
|
||||||
Não há resultados.
|
{hasActiveFilters ? "Nenhum prompt encontrado para os filtros selecionados." : "Não há resultados."}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+18
-5
@@ -135,21 +135,34 @@ class AreasService {
|
|||||||
throw new Error("ID da área é obrigatório");
|
throw new Error("ID da área é obrigatório");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const idEnc = encodeURIComponent(id.trim());
|
||||||
const body: EditarAreaBody = {
|
const body: EditarAreaBody = {
|
||||||
nome: nome.trim(),
|
nome: nome.trim(),
|
||||||
descricao: (descricao ?? "").trim(),
|
descricao: (descricao ?? "").trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await apiService.put<EditarAreaSuccessResponse | EditarAreaErrorResponse>(
|
/** Mesmo prefixo de webhook que o fluxo n8n (UUID) usado nos endpoints de "parecer/areas". */
|
||||||
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/area/${id}`,
|
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/parecer/area/${idEnc}`;
|
||||||
|
|
||||||
|
const response = await apiService.put<
|
||||||
|
EditarAreaSuccessResponse | EditarAreaErrorResponse | (EditarAreaSuccessResponse | EditarAreaErrorResponse)[]
|
||||||
|
>(
|
||||||
|
url,
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data.success === false) {
|
const raw = response.data;
|
||||||
throw new Error((response.data as EditarAreaErrorResponse).message ?? "Erro ao editar área");
|
const data = Array.isArray(raw) ? raw[0] : raw;
|
||||||
|
|
||||||
|
if (!data || typeof data !== "object" || !("success" in data)) {
|
||||||
|
throw new Error("Resposta inválida ao editar área");
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data as EditarAreaSuccessResponse;
|
if (data.success === false) {
|
||||||
|
throw new Error((data as EditarAreaErrorResponse).message ?? "Erro ao editar área");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as EditarAreaSuccessResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletar(id: string): Promise<DeletarAreaSuccessResponse> {
|
async deletar(id: string): Promise<DeletarAreaSuccessResponse> {
|
||||||
|
|||||||
@@ -180,6 +180,89 @@ class AudioGenerationService {
|
|||||||
this.toApiError(error, 'Erro ao deletar áudio');
|
this.toApiError(error, 'Erro ao deletar áudio');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baixa o arquivo no navegador (sem navegar para a URL).
|
||||||
|
* Usa blob local + <a download> — necessário porque links cross-origin ignoram `download` e abrem a URL.
|
||||||
|
*/
|
||||||
|
async downloadAudioFile(audioUrl: string, filename?: string): Promise<void> {
|
||||||
|
const urlTrim = audioUrl?.trim();
|
||||||
|
if (!urlTrim) {
|
||||||
|
throw new Error('URL do áudio inválida');
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeName = (filename || `audio_${Date.now()}.mp3`).replace(/[/\\?%*:|"<>]/g, '_');
|
||||||
|
|
||||||
|
let sameOrigin = false;
|
||||||
|
try {
|
||||||
|
const u = new URL(urlTrim, typeof window !== 'undefined' ? window.location.href : undefined);
|
||||||
|
sameOrigin = typeof window !== 'undefined' && u.origin === window.location.origin;
|
||||||
|
} catch {
|
||||||
|
sameOrigin = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchBlob = async (): Promise<Blob> => {
|
||||||
|
const response = await fetch(urlTrim, {
|
||||||
|
mode: 'cors',
|
||||||
|
credentials: sameOrigin ? 'include' : 'omit',
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Falha ao baixar (HTTP ${response.status})`);
|
||||||
|
}
|
||||||
|
return response.blob();
|
||||||
|
};
|
||||||
|
|
||||||
|
const xhrBlob = (): Promise<Blob> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', urlTrim, true);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
resolve(xhr.response);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Falha ao baixar (HTTP ${xhr.status})`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = () => reject(new Error('Falha de rede ao baixar o áudio'));
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
let blob: Blob;
|
||||||
|
try {
|
||||||
|
blob = await fetchBlob();
|
||||||
|
} catch (e1) {
|
||||||
|
try {
|
||||||
|
blob = await xhrBlob();
|
||||||
|
} catch (e2) {
|
||||||
|
console.error('downloadAudioFile:', e1, e2);
|
||||||
|
throw new Error(
|
||||||
|
'Não foi possível baixar o áudio. Se o arquivo estiver em outro domínio, é preciso CORS liberando GET para esta origem.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsMimeFix =
|
||||||
|
!blob.type ||
|
||||||
|
blob.type === 'application/octet-stream' ||
|
||||||
|
blob.type === 'text/html';
|
||||||
|
const typedBlob = needsMimeFix ? new Blob([blob], { type: 'audio/mpeg' }) : blob;
|
||||||
|
|
||||||
|
const objectUrl = URL.createObjectURL(typedBlob);
|
||||||
|
try {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = objectUrl;
|
||||||
|
a.download = safeName.endsWith('.mp3') ? safeName : `${safeName}.mp3`;
|
||||||
|
a.style.display = 'none';
|
||||||
|
a.rel = 'noopener';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const audioGenerationService = new AudioGenerationService();
|
export const audioGenerationService = new AudioGenerationService();
|
||||||
|
|||||||
@@ -151,25 +151,35 @@ class PromptsService {
|
|||||||
throw new Error("ID do prompt é obrigatório");
|
throw new Error("ID do prompt é obrigatório");
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiService.put<EditarPromptSuccessResponse | EditarPromptErrorResponse>(
|
const idEnc = encodeURIComponent(id.trim());
|
||||||
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/prompt/editar/${id}`,
|
/** Mesmo prefixo de webhook n8n usado em edição de área (`areas.editar`). */
|
||||||
{
|
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/parecer/prompt/${idEnc}`;
|
||||||
|
|
||||||
|
const response = await apiService.put<
|
||||||
|
EditarPromptSuccessResponse | EditarPromptErrorResponse | (EditarPromptSuccessResponse | EditarPromptErrorResponse)[]
|
||||||
|
>(url, {
|
||||||
titulo: (body.titulo ?? "").trim(),
|
titulo: (body.titulo ?? "").trim(),
|
||||||
descricao: (body.descricao ?? "").trim(),
|
descricao: (body.descricao ?? "").trim(),
|
||||||
area_id: body.area_id.trim(),
|
area_id: body.area_id.trim(),
|
||||||
conteudo: (body.conteudo ?? "").trim(),
|
conteudo: (body.conteudo ?? "").trim(),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.success === false) {
|
const raw = response.data;
|
||||||
const err = response.data as EditarPromptErrorResponse;
|
const data = Array.isArray(raw) ? raw[0] : raw;
|
||||||
|
|
||||||
|
if (!data || typeof data !== "object" || !("success" in data)) {
|
||||||
|
throw new Error("Resposta inválida ao editar prompt");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success === false) {
|
||||||
|
const err = data as EditarPromptErrorResponse;
|
||||||
const msg = err.missing_fields?.length
|
const msg = err.missing_fields?.length
|
||||||
? `Preencha: ${err.missing_fields.join(", ")}`
|
? `Preencha: ${err.missing_fields.join(", ")}`
|
||||||
: "Erro ao editar prompt.";
|
: "Erro ao editar prompt.";
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data as EditarPromptSuccessResponse;
|
return data as EditarPromptSuccessResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user