develop #83
@ -46,6 +46,8 @@ export function AIAssistantInterface({
|
||||
const [manualSelection, setManualSelection] = useState(false);
|
||||
const [historyPanelOpen, setHistoryPanelOpen] = useState(false);
|
||||
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 historyRef = useRef<ChatSession[]>(history);
|
||||
const baseGreeting = "Olá, eu sou Zoe. Como posso ajudar hoje?";
|
||||
@ -138,16 +140,29 @@ export function AIAssistantInterface({
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ message: prompt }),
|
||||
});
|
||||
let replyText = "";
|
||||
let response: Response;
|
||||
|
||||
if (pdfFile) {
|
||||
// Monta FormData conforme especificação: campos 'pdf' e 'message'
|
||||
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();
|
||||
let replyText = "";
|
||||
|
||||
if (rawPayload.trim()) {
|
||||
try {
|
||||
@ -168,7 +183,7 @@ export function AIAssistantInterface({
|
||||
appendAssistantMessage(FALLBACK_RESPONSE);
|
||||
}
|
||||
},
|
||||
[upsertSession]
|
||||
[upsertSession, pdfFile]
|
||||
);
|
||||
|
||||
const handleSendMessage = () => {
|
||||
@ -204,5 +219,103 @@ export function AIAssistantInterface({
|
||||
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));
|
||||
};
|
||||
|
||||
|
||||
const generateAIResponse = useCallback(
|
||||
async (userMessage: string, files: any[]) => {
|
||||
try {
|
||||
const hasAudio = files.some((file) =>
|
||||
file.name.toLowerCase().endsWith(".mp3")
|
||||
);
|
||||
const hasPdf = files.some((file) =>
|
||||
file.name.toLowerCase().endsWith(".pdf")
|
||||
);
|
||||
const pdfFile = files.find((file) => file.name.toLowerCase().endsWith(".pdf"));
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// Adiciona mensagem
|
||||
formData.append("message", userMessage);
|
||||
|
||||
// Adiciona arquivos corretamente
|
||||
files.forEach((file) => {
|
||||
const ext = file.name.toLowerCase().split(".").pop();
|
||||
if (ext === "mp3") {
|
||||
formData.append("audio", file.file); // nome do campo deve ser 'audio'
|
||||
} else if (ext === "pdf") {
|
||||
formData.append("pdf", file.file); // nome do campo deve ser 'pdf'
|
||||
}
|
||||
});
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
let response: Response;
|
||||
if (pdfFile) {
|
||||
const formData = new FormData();
|
||||
formData.append("pdf", pdfFile.file); // campo 'pdf'
|
||||
formData.append("message", userMessage); // campo 'message'
|
||||
response = await fetch(API_ENDPOINT, {
|
||||
method: "POST",
|
||||
body: formData, // multipart/form-data automático
|
||||
});
|
||||
} else {
|
||||
response = await fetch(API_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: userMessage }),
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
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" />
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
/>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user