"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { SimpleThemeToggle } from "@/components/ui/simple-theme-toggle"; import { Clock, Info, Lock, MessageCircle, Plus, Upload } from "lucide-react"; const API_ENDPOINT = "https://n8n.jonasbomfim.store/webhook/cd7d10e6-bcfc-4f3a-b649-351d12b714f1"; const FALLBACK_RESPONSE = "Tive um problema para responder agora. Tente novamente em alguns instantes."; export interface ChatMessage { id: string; sender: "user" | "assistant"; content: string; createdAt: string; } export interface ChatSession { id: string; startedAt: string; updatedAt: string; topic: string; messages: ChatMessage[]; } interface AIAssistantInterfaceProps { onOpenDocuments?: () => void; onOpenChat?: () => void; history?: ChatSession[]; onAddHistory?: (session: ChatSession) => void; onClearHistory?: () => void; } export function AIAssistantInterface({ onOpenDocuments, onOpenChat, history: externalHistory, onAddHistory, onClearHistory, }: AIAssistantInterfaceProps) { const [question, setQuestion] = useState(""); const [internalHistory, setInternalHistory] = useState(externalHistory ?? []); const [activeSessionId, setActiveSessionId] = useState(null); const [manualSelection, setManualSelection] = useState(false); const [historyPanelOpen, setHistoryPanelOpen] = useState(false); const messageListRef = useRef(null); const history = internalHistory; const historyRef = useRef(history); const baseGreeting = "Olá, eu sou Zoe. Como posso ajudar hoje?"; const greetingWords = useMemo(() => baseGreeting.split(" "), [baseGreeting]); const [typedGreeting, setTypedGreeting] = useState(""); const [typedIndex, setTypedIndex] = useState(0); const [isTypingGreeting, setIsTypingGreeting] = useState(true); const [gradientGreeting, plainGreeting] = useMemo(() => { if (!typedGreeting) return ["", ""] as const; const separatorIndex = typedGreeting.indexOf("Como"); if (separatorIndex === -1) { return [typedGreeting, ""] as const; } const gradientPart = typedGreeting.slice(0, separatorIndex).trimEnd(); const plainPart = typedGreeting.slice(separatorIndex).trimStart(); return [gradientPart, plainPart] as const; }, [typedGreeting]); useEffect(() => { if (externalHistory) { setInternalHistory(externalHistory); } }, [externalHistory]); useEffect(() => { historyRef.current = history; }, [history]); const activeSession = useMemo( () => history.find((session) => session.id === activeSessionId) ?? null, [history, activeSessionId] ); const activeMessages = activeSession?.messages ?? []; const formatDateTime = useCallback( (value: string) => new Date(value).toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit", }), [] ); const formatTime = useCallback( (value: string) => new Date(value).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", }), [] ); useEffect(() => { if (history.length === 0) { setActiveSessionId(null); setManualSelection(false); return; } if (!activeSessionId && !manualSelection) { setActiveSessionId(history[history.length - 1].id); return; } const exists = history.some((session) => session.id === activeSessionId); if (!exists && !manualSelection) { setActiveSessionId(history[history.length - 1].id); } }, [history, activeSessionId, manualSelection]); useEffect(() => { if (!messageListRef.current) return; messageListRef.current.scrollTo({ top: messageListRef.current.scrollHeight, behavior: "smooth", }); }, [activeMessages.length]); useEffect(() => { setTypedGreeting(""); setTypedIndex(0); setIsTypingGreeting(true); }, []); useEffect(() => { if (!isTypingGreeting) return; if (typedIndex >= greetingWords.length) { setIsTypingGreeting(false); return; } const timeout = window.setTimeout(() => { setTypedGreeting((previous) => previous ? `${previous} ${greetingWords[typedIndex]}` : greetingWords[typedIndex] ); setTypedIndex((previous) => previous + 1); }, 260); return () => window.clearTimeout(timeout); }, [greetingWords, isTypingGreeting, typedIndex]); const handleDocuments = () => { if (onOpenDocuments) { onOpenDocuments(); return; } console.log("[ZoeIA] Abrir fluxo de documentos"); }; const handleOpenRealtimeChat = () => { if (onOpenChat) { onOpenChat(); return; } console.log("[ZoeIA] Abrir chat em tempo real"); }; const buildSessionTopic = useCallback((content: string) => { const normalized = content.trim(); if (!normalized) return "Atendimento"; return normalized.length > 60 ? `${normalized.slice(0, 57)}…` : normalized; }, []); const upsertSession = useCallback( (session: ChatSession) => { if (onAddHistory) { onAddHistory(session); } else { setInternalHistory((previous) => { const index = previous.findIndex((item) => item.id === session.id); if (index >= 0) { const updated = [...previous]; updated[index] = session; return updated; } return [...previous, session]; }); } setActiveSessionId(session.id); setManualSelection(false); }, [onAddHistory] ); const sendMessageToAssistant = useCallback( async (prompt: string, baseSession: ChatSession) => { const sessionId = baseSession.id; const appendAssistantMessage = (content: string) => { const createdAt = new Date().toISOString(); const latestSession = historyRef.current.find((session) => session.id === sessionId) ?? baseSession; const assistantMessage: ChatMessage = { id: `msg-assistant-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, sender: "assistant", content, createdAt, }; const updatedSession: ChatSession = { ...latestSession, updatedAt: assistantMessage.createdAt, messages: [...latestSession.messages, assistantMessage], }; upsertSession(updatedSession); }; try { const response = await fetch(API_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ message: prompt }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const rawPayload = await response.text(); let replyText = ""; if (rawPayload.trim().length > 0) { try { const parsed = JSON.parse(rawPayload) as { reply?: unknown }; replyText = typeof parsed.reply === "string" ? parsed.reply.trim() : ""; } catch (parseError) { console.error("[ZoeIA] Resposta JSON inválida", parseError, rawPayload); } } appendAssistantMessage(replyText || FALLBACK_RESPONSE); } catch (error) { console.error("[ZoeIA] Falha ao obter resposta da API", error); appendAssistantMessage(FALLBACK_RESPONSE); } }, [upsertSession] ); const handleSendMessage = () => { const trimmed = question.trim(); if (!trimmed) return; const now = new Date(); const userMessage: ChatMessage = { id: `msg-user-${now.getTime()}`, sender: "user", content: trimmed, createdAt: now.toISOString(), }; const existingSession = history.find((session) => session.id === activeSessionId) ?? null; const sessionToPersist: ChatSession = existingSession ? { ...existingSession, updatedAt: userMessage.createdAt, topic: existingSession.messages.length === 0 ? buildSessionTopic(trimmed) : existingSession.topic, messages: [...existingSession.messages, userMessage], } : { id: `session-${now.getTime()}`, startedAt: now.toISOString(), updatedAt: userMessage.createdAt, topic: buildSessionTopic(trimmed), messages: [userMessage], }; upsertSession(sessionToPersist); console.log("[ZoeIA] Mensagem registrada na Zoe", trimmed); setQuestion(""); setHistoryPanelOpen(false); void sendMessageToAssistant(trimmed, sessionToPersist); }; const RealtimeTriggerButton = () => ( ); const handleClearHistory = () => { if (onClearHistory) { onClearHistory(); } else { setInternalHistory([]); } setActiveSessionId(null); setManualSelection(false); setQuestion(""); setHistoryPanelOpen(false); }; const handleSelectSession = useCallback((sessionId: string) => { setManualSelection(true); setActiveSessionId(sessionId); setHistoryPanelOpen(false); }, []); const startNewConversation = useCallback(() => { setManualSelection(true); setActiveSessionId(null); setQuestion(""); setHistoryPanelOpen(false); }, []); return (
Zoe

Assistente Clínica Zoe

{gradientGreeting && ( {gradientGreeting} {plainGreeting ? " " : ""} )} {plainGreeting && {plainGreeting}}
{history.length > 0 && ( )} {history.length > 0 && ( )}
Organizamos exames, orientações e tarefas assistenciais em um painel único para acelerar decisões clínicas. Utilize a Zoe para revisar resultados, registrar percepções e alinhar próximos passos com a equipe de saúde.
Suas informações permanecem criptografadas e seguras com a equipe Zoe.
Informativo importante

A Zoe acompanha toda a jornada clínica, consolida exames e registra orientações para que você tenha clareza em cada etapa do cuidado. As respostas são informativas e complementam a avaliação de um profissional de saúde qualificado.

Em situações de urgência, entre em contato com a equipe médica presencial ou acione os serviços de emergência da sua região.

Estamos reunindo o histórico da sua jornada. Enquanto isso, você pode anexar exames, enviar dúvidas ou solicitar contato com a equipe Zoe.

{activeSession ? "Atendimento em andamento" : "Inicie uma conversa"}

{activeSession?.topic ?? "O primeiro contato orienta nossas recomendações clínicas"}

{activeSession && ( Atualizado às {formatTime(activeSession.updatedAt)} )}
{activeMessages.length > 0 ? ( activeMessages.map((message) => (

{message.content}

{formatTime(message.createdAt)}
)) ) : (

Envie sua primeira mensagem

Compartilhe uma dúvida, exame ou orientação que deseja revisar. A Zoe registra o pedido e te retorna com um resumo organizado para a equipe de saúde.

)}
setQuestion(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); handleSendMessage(); } }} placeholder="Pergunte qualquer coisa para a Zoe" className="w-full flex-1 border-none bg-transparent text-sm shadow-none focus-visible:ring-0" />
{historyPanelOpen && ( )}
); }