fix(ia) ajuste na integração da IA para suportar pdf
This commit is contained in:
parent
a8d9b1f896
commit
964e25bd7e
@ -46,6 +46,8 @@ export function AIAssistantInterface({
|
|||||||
const [manualSelection, setManualSelection] = useState(false);
|
const [manualSelection, setManualSelection] = useState(false);
|
||||||
const [historyPanelOpen, setHistoryPanelOpen] = useState(false);
|
const [historyPanelOpen, setHistoryPanelOpen] = useState(false);
|
||||||
const messageListRef = useRef<HTMLDivElement | null>(null);
|
const messageListRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const pdfInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [pdfFile, setPdfFile] = useState<File | null>(null); // arquivo PDF selecionado
|
||||||
const history = internalHistory;
|
const history = internalHistory;
|
||||||
const historyRef = useRef<ChatSession[]>(history);
|
const historyRef = useRef<ChatSession[]>(history);
|
||||||
const baseGreeting = "Olá, eu sou Zoe. Como posso ajudar hoje?";
|
const baseGreeting = "Olá, eu sou Zoe. Como posso ajudar hoje?";
|
||||||
@ -138,16 +140,29 @@ export function AIAssistantInterface({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(API_ENDPOINT, {
|
let replyText = "";
|
||||||
method: "POST",
|
let response: Response;
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
if (pdfFile) {
|
||||||
},
|
// Monta FormData conforme especificação: campos 'pdf' e 'message'
|
||||||
body: JSON.stringify({ message: prompt }),
|
const formData = new FormData();
|
||||||
});
|
formData.append("pdf", pdfFile);
|
||||||
|
formData.append("message", prompt);
|
||||||
|
response = await fetch(API_ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData, // multipart/form-data gerenciado pelo browser
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response = await fetch(API_ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message: prompt }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const rawPayload = await response.text();
|
const rawPayload = await response.text();
|
||||||
let replyText = "";
|
|
||||||
|
|
||||||
if (rawPayload.trim()) {
|
if (rawPayload.trim()) {
|
||||||
try {
|
try {
|
||||||
@ -168,7 +183,7 @@ export function AIAssistantInterface({
|
|||||||
appendAssistantMessage(FALLBACK_RESPONSE);
|
appendAssistantMessage(FALLBACK_RESPONSE);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[upsertSession]
|
[upsertSession, pdfFile]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSendMessage = () => {
|
const handleSendMessage = () => {
|
||||||
@ -204,5 +219,103 @@ export function AIAssistantInterface({
|
|||||||
void sendMessageToAssistant(trimmed, sessionToUse);
|
void sendMessageToAssistant(trimmed, sessionToUse);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div>/* restante da interface (UI) omitida para focar na lógica */</div>;
|
const handleSelectPdf = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file && file.type === "application/pdf") {
|
||||||
|
setPdfFile(file);
|
||||||
|
}
|
||||||
|
// Permite re-selecionar o mesmo arquivo
|
||||||
|
e.target.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const removePdf = () => setPdfFile(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-3xl mx-auto p-4 space-y-4">
|
||||||
|
{/* Área superior exibindo PDF selecionado */}
|
||||||
|
{pdfFile && (
|
||||||
|
<div className="flex items-center justify-between border rounded-lg p-3 bg-muted/50">
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<Upload className="w-5 h-5 text-primary" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate" title={pdfFile.name}>{pdfFile.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
PDF anexado • {(pdfFile.size / 1024).toFixed(1)} KB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="secondary" size="sm" onClick={removePdf}>
|
||||||
|
Remover
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lista de mensagens */}
|
||||||
|
<div
|
||||||
|
ref={messageListRef}
|
||||||
|
className="border rounded-lg p-4 h-96 overflow-y-auto space-y-3 bg-background"
|
||||||
|
>
|
||||||
|
{activeMessages.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground">Nenhuma mensagem ainda. Envie uma pergunta.</p>
|
||||||
|
)}
|
||||||
|
{activeMessages.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
className={`flex ${m.sender === "user" ? "justify-end" : "justify-start"}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`px-3 py-2 rounded-lg max-w-xs text-sm whitespace-pre-wrap ${
|
||||||
|
m.sender === "user" ? "bg-primary text-primary-foreground" : "bg-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{m.content}
|
||||||
|
<div className="mt-1 text-[10px] opacity-70">
|
||||||
|
{formatTime(m.createdAt)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input & ações */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => pdfInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
PDF
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={pdfInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="application/pdf"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleSelectPdf}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="Digite sua pergunta"
|
||||||
|
value={question}
|
||||||
|
onChange={(e) => setQuestion(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleSendMessage} disabled={!question.trim()}>
|
||||||
|
Enviar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{pdfFile
|
||||||
|
? "A próxima mensagem será enviada junto ao PDF como multipart/form-data."
|
||||||
|
: "Selecione um PDF para anexar ao próximo envio."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,44 +124,28 @@ const FileUploadChat = ({ onOpenVoice }: { onOpenVoice?: () => void }) => {
|
|||||||
setUploadedFiles((prev) => prev.filter((file) => file.id !== fileId));
|
setUploadedFiles((prev) => prev.filter((file) => file.id !== fileId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const generateAIResponse = useCallback(
|
const generateAIResponse = useCallback(
|
||||||
async (userMessage: string, files: any[]) => {
|
async (userMessage: string, files: any[]) => {
|
||||||
try {
|
try {
|
||||||
const hasAudio = files.some((file) =>
|
const pdfFile = files.find((file) => file.name.toLowerCase().endsWith(".pdf"));
|
||||||
file.name.toLowerCase().endsWith(".mp3")
|
|
||||||
);
|
|
||||||
const hasPdf = files.some((file) =>
|
|
||||||
file.name.toLowerCase().endsWith(".pdf")
|
|
||||||
);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
let response: Response;
|
||||||
|
if (pdfFile) {
|
||||||
// Adiciona mensagem
|
const formData = new FormData();
|
||||||
formData.append("message", userMessage);
|
formData.append("pdf", pdfFile.file); // campo 'pdf'
|
||||||
|
formData.append("message", userMessage); // campo 'message'
|
||||||
// Adiciona arquivos corretamente
|
response = await fetch(API_ENDPOINT, {
|
||||||
files.forEach((file) => {
|
method: "POST",
|
||||||
const ext = file.name.toLowerCase().split(".").pop();
|
body: formData, // multipart/form-data automático
|
||||||
if (ext === "mp3") {
|
});
|
||||||
formData.append("audio", file.file); // nome do campo deve ser 'audio'
|
} else {
|
||||||
} else if (ext === "pdf") {
|
response = await fetch(API_ENDPOINT, {
|
||||||
formData.append("pdf", file.file); // nome do campo deve ser 'pdf'
|
method: "POST",
|
||||||
}
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
body: JSON.stringify({ message: userMessage }),
|
||||||
|
});
|
||||||
const response = await fetch(API_ENDPOINT, {
|
}
|
||||||
method: "POST",
|
|
||||||
body:
|
|
||||||
hasAudio || hasPdf
|
|
||||||
? formData
|
|
||||||
: JSON.stringify({ message: userMessage }),
|
|
||||||
headers:
|
|
||||||
hasAudio || hasPdf
|
|
||||||
? undefined // Quando usar FormData, NÃO definir Content-Type, o navegador define com boundary correto
|
|
||||||
: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
@ -546,6 +530,13 @@ const FileUploadChat = ({ onOpenVoice }: { onOpenVoice?: () => void }) => {
|
|||||||
>
|
>
|
||||||
<Paperclip className="w-4 h-4 sm:w-5 sm:h-5" />
|
<Paperclip className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => handleFileSelect(e.target.files)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user