From 7ca2c21ba368cfd578e2ac50f731056642ccdce6 Mon Sep 17 00:00:00 2001 From: Jonas Francisco Date: Thu, 13 Nov 2025 16:05:58 -0300 Subject: [PATCH] fix(ia) adicionei o endepoint de producao do agente --- .../ZoeIA/ai-assistant-interface.tsx | 690 ++++++++++++++---- .../features/pacientes/chat-widget.tsx | 27 +- 2 files changed, 545 insertions(+), 172 deletions(-) diff --git a/susconecta/components/ZoeIA/ai-assistant-interface.tsx b/susconecta/components/ZoeIA/ai-assistant-interface.tsx index 75481bc..9a064ed 100644 --- a/susconecta/components/ZoeIA/ai-assistant-interface.tsx +++ b/susconecta/components/ZoeIA/ai-assistant-interface.tsx @@ -1,29 +1,35 @@ "use client"; -import { useMemo, useState } from "react"; +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"; +import { Clock, Info, Lock, MessageCircle, Plus, Upload } from "lucide-react"; -interface HistoryEntry { +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; - text: 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?: HistoryEntry[]; - onAddHistory?: (entry: HistoryEntry) => void; + history?: ChatSession[]; + onAddHistory?: (session: ChatSession) => void; onClearHistory?: () => void; } @@ -35,11 +41,117 @@ export function AIAssistantInterface({ onClearHistory, }: AIAssistantInterfaceProps) { const [question, setQuestion] = useState(""); - const [drawerOpen, setDrawerOpen] = useState(false); - const [internalHistory, setInternalHistory] = useState([]); - const history = externalHistory ?? internalHistory; + 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 showHistoryBadge = useMemo(() => history.length > 0, [history.length]); + 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) { @@ -57,13 +169,129 @@ export function AIAssistantInterface({ 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; - handlePersistHistory(trimmed); - console.log("[ZoeIA] Mensagem enviada para Zoe", trimmed); + 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 = () => ( @@ -89,91 +317,149 @@ export function AIAssistantInterface({ ); - const handlePersistHistory = (text: string) => { - const entry: HistoryEntry = { - id: `hist-${Date.now()}`, - text, - createdAt: new Date().toISOString(), - }; - - if (onAddHistory) { - onAddHistory(entry); - } else { - setInternalHistory((prev) => [...prev, entry]); - } - setDrawerOpen(true); - }; - const handleClearHistory = () => { if (onClearHistory) { onClearHistory(); } else { setInternalHistory([]); } + setActiveSessionId(null); + setManualSelection(false); + setQuestion(""); + setHistoryPanelOpen(false); }; - const HistoryGlyph = () => ( - - - - - - - ); + 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

-

Olá, eu sou Zoe. Como posso ajudar hoje?

+ +
+
+
+ + 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 + Informativo importante

- A Zoe é a assistente virtual da Clínica Zoe. Ela reúne informações sobre seus cuidados e orienta os próximos passos. - O atendimento é informativo e não substitui a avaliação de um profissional de saúde qualificado. + 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, procure imediatamente o suporte médico presencial ou ligue para os serviços de emergência. +

+ 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.

-
+
-
+ -
- + {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)} @@ -222,9 +573,9 @@ export function AIAssistantInterface({ } }} placeholder="Pergunte qualquer coisa para a Zoe" - className="border-none bg-transparent text-sm shadow-none focus-visible:ring-0" + className="w-full flex-1 border-none bg-transparent text-sm shadow-none focus-visible:ring-0" /> -
+
- {drawerOpen && ( - + )}
); } diff --git a/susconecta/components/features/pacientes/chat-widget.tsx b/susconecta/components/features/pacientes/chat-widget.tsx index a270f5d..f038ea2 100644 --- a/susconecta/components/features/pacientes/chat-widget.tsx +++ b/susconecta/components/features/pacientes/chat-widget.tsx @@ -5,21 +5,18 @@ import { useEffect, useMemo, useState } from "react"; import { ArrowLeft, Mic, MicOff, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { AIAssistantInterface } from "@/components/ZoeIA/ai-assistant-interface"; +import { + AIAssistantInterface, + ChatSession, +} from "@/components/ZoeIA/ai-assistant-interface"; import { VoicePoweredOrb } from "@/components/ZoeIA/voice-powered-orb"; -interface HistoryEntry { - id: string; - text: string; - createdAt: string; -} - export function ChatWidget() { const [assistantOpen, setAssistantOpen] = useState(false); const [realtimeOpen, setRealtimeOpen] = useState(false); const [isRecording, setIsRecording] = useState(false); const [voiceDetected, setVoiceDetected] = useState(false); - const [history, setHistory] = useState([]); + const [history, setHistory] = useState([]); useEffect(() => { if (!assistantOpen && !realtimeOpen) return; @@ -74,8 +71,16 @@ export function ChatWidget() { openRealtime(); }; - const handleAddHistory = (entry: HistoryEntry) => { - setHistory((prev) => [...prev, entry]); + const handleUpsertHistory = (session: ChatSession) => { + setHistory((previous) => { + const index = previous.findIndex((item) => item.id === session.id); + if (index >= 0) { + const updated = [...previous]; + updated[index] = session; + return updated; + } + return [...previous, session]; + }); }; const handleClearHistory = () => { @@ -105,7 +110,7 @@ export function ChatWidget() { onOpenDocuments={handleOpenDocuments} onOpenChat={handleOpenChat} history={history} - onAddHistory={handleAddHistory} + onAddHistory={handleUpsertHistory} onClearHistory={handleClearHistory} />