analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
1 lines • 17.7 kB
Source Map (JSON)
{"version":3,"sources":["../src/hooks/useChatbot.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { CHATBOT_MESSAGE_ROLES } from '../types/chatbot';\nimport type {\n ChatbotApiClient,\n ChatbotConversation,\n ChatbotMessage,\n ChatbotCurrentContext,\n SendChatbotMessageResult,\n} from '../types/chatbot';\n\nconst DEFAULT_CONVERSATIONS_PAGE_SIZE = 20;\nconst DEFAULT_MESSAGES_PAGE_SIZE = 50;\n\n/**\n * Monotonic counter used as a fallback when `crypto.randomUUID` is not\n * available (older browsers / some jsdom builds).\n */\nlet placeholderCounter = 0;\n\n/**\n * Generate a collision-safe id for optimistic user bubbles. Prefers\n * `crypto.randomUUID()` when available (browsers + modern Node), and\n * falls back to a timestamp + monotonic counter otherwise.\n */\nfunction generatePlaceholderId(): string {\n const globalCrypto = (globalThis as { crypto?: Crypto }).crypto;\n if (globalCrypto && typeof globalCrypto.randomUUID === 'function') {\n return `pending-${globalCrypto.randomUUID()}`;\n }\n placeholderCounter += 1;\n return `pending-${Date.now()}-${placeholderCounter}`;\n}\n\n/**\n * Replace the optimistic placeholder with the server-confirmed pair.\n */\nfunction replacePlaceholder(\n prev: ChatbotMessage[],\n placeholderId: string,\n result: SendChatbotMessageResult\n): ChatbotMessage[] {\n return [\n ...prev.filter((m) => m.id !== placeholderId),\n result.userMessage,\n result.assistantMessage,\n ];\n}\n\n/**\n * Remove an optimistic placeholder (used on error rollback).\n */\nfunction removePlaceholder(\n prev: ChatbotMessage[],\n placeholderId: string\n): ChatbotMessage[] {\n return prev.filter((m) => m.id !== placeholderId);\n}\n\n/**\n * Filter out an item by id. Generic helper used for conversations.\n */\nfunction removeById<T extends { id: string }>(items: T[], id: string): T[] {\n return items.filter((item) => item.id !== id);\n}\n\n/**\n * Map any thrown value to a Portuguese user-facing message. The raw\n * `err.message` is intentionally ignored — transport-level strings\n * (axios \"Network Error\", \"Request failed with status code 500\") are\n * not localized and should not reach the UI. Observability remains\n * intact: the original error still bubbles through `console.error` /\n * Sentry at higher layers. The signature leaves room to evolve into a\n * type-aware mapper later without changing call sites.\n */\nfunction toUserErrorMessage(_err: unknown, fallback: string): string {\n return fallback;\n}\n\n/**\n * Data and actions exposed by `useChatbot`\n */\nexport interface UseChatbotReturn {\n isOpen: boolean;\n isSending: boolean;\n isLoadingHistory: boolean;\n isLoadingMessages: boolean;\n errorMessage: string | null;\n conversations: ChatbotConversation[];\n activeConversationId: string | null;\n messages: ChatbotMessage[];\n openPanel: () => void;\n closePanel: () => void;\n togglePanel: () => void;\n selectConversation: (id: string | null) => void;\n startNewConversation: () => void;\n sendMessage: (\n text: string,\n currentContext?: ChatbotCurrentContext\n ) => Promise<void>;\n deleteConversation: (id: string) => Promise<void>;\n reloadConversations: () => Promise<void>;\n}\n\n/**\n * Resolver for the current `ChatbotApiClient`. Accepts either a concrete\n * client (captured once) or a getter invoked on every call so consumers\n * can swap clients over time without recreating the hook.\n */\nexport type ChatbotApiClientResolver =\n | ChatbotApiClient\n | (() => ChatbotApiClient);\n\n/**\n * Factory that wires the hook to a concrete API client. Returned hook\n * owns the chatbot UI state (open/close, messages, conversations).\n *\n * Implemented as a factory so consumer apps can bind a typed client once\n * (usually near composition root) and keep the hook implementation free\n * of app-specific transport concerns. The factory also accepts a **getter**\n * — pass `() => apiClientRef.current` to let the hook always see the\n * latest client even if the parent swaps references between renders.\n */\nexport function createUseChatbot(resolver: ChatbotApiClientResolver) {\n const resolveClient: () => ChatbotApiClient =\n typeof resolver === 'function'\n ? (resolver as () => ChatbotApiClient)\n : () => resolver;\n\n return function useChatbot(): UseChatbotReturn {\n const [isOpen, setIsOpen] = useState(false);\n const [isSending, setIsSending] = useState(false);\n const [isLoadingHistory, setIsLoadingHistory] = useState(false);\n const [isLoadingMessages, setIsLoadingMessages] = useState(false);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n const [conversations, setConversations] = useState<ChatbotConversation[]>(\n []\n );\n const [activeConversationId, setActiveConversationId] = useState<\n string | null\n >(null);\n const [messages, setMessages] = useState<ChatbotMessage[]>([]);\n\n // Resolve the latest apiClient on every call so the hook respects\n // callers that pass a getter (e.g., bound to a React ref).\n const apiClientRef = useRef<ChatbotApiClient>(resolveClient());\n apiClientRef.current = resolveClient();\n\n // Guard against state updates after unmount. The ref is re-armed on\n // mount so the hook works correctly under React 18/19 Strict Mode\n // (which mounts, unmounts and remounts during development).\n const mountedRef = useRef(false);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n // Monotonic sequence ids per load-op. Each call captures its token and\n // only commits state when still current — avoids stale responses from\n // overwriting newer ones when the user switches conversations quickly.\n const conversationsSeqRef = useRef(0);\n const messagesSeqRef = useRef(0);\n // Synchronous lock around `sendMessage` — the `isSending` state alone\n // cannot block a second call fired in the same tick (React batches\n // state updates), so we mirror it with a ref that flips immediately.\n const isSendingRef = useRef(false);\n\n /**\n * Bump the messages sequence and clear the loading flag so any\n * in-flight `loadMessages()` resolving afterward fails the sequence\n * check and does not repopulate state tied to a conversation the\n * user has already left.\n */\n const invalidateMessagesLoad = () => {\n messagesSeqRef.current += 1;\n setIsLoadingMessages(false);\n };\n\n const loadConversations = useCallback(async () => {\n const seq = ++conversationsSeqRef.current;\n setIsLoadingHistory(true);\n setErrorMessage(null);\n try {\n const result = await apiClientRef.current.listConversations({\n page: 1,\n limit: DEFAULT_CONVERSATIONS_PAGE_SIZE,\n });\n if (!mountedRef.current || seq !== conversationsSeqRef.current) return;\n setConversations(result.conversations);\n } catch (err) {\n if (!mountedRef.current || seq !== conversationsSeqRef.current) return;\n setErrorMessage(toUserErrorMessage(err, 'Falha ao carregar conversas'));\n } finally {\n if (mountedRef.current && seq === conversationsSeqRef.current) {\n setIsLoadingHistory(false);\n }\n }\n }, []);\n\n const loadMessages = useCallback(async (conversationId: string) => {\n const seq = ++messagesSeqRef.current;\n setIsLoadingMessages(true);\n setErrorMessage(null);\n try {\n const result = await apiClientRef.current.getMessages(conversationId, {\n page: 1,\n limit: DEFAULT_MESSAGES_PAGE_SIZE,\n });\n if (!mountedRef.current || seq !== messagesSeqRef.current) return;\n setMessages(result.messages);\n } catch (err) {\n if (!mountedRef.current || seq !== messagesSeqRef.current) return;\n setErrorMessage(toUserErrorMessage(err, 'Falha ao carregar mensagens'));\n } finally {\n if (mountedRef.current && seq === messagesSeqRef.current) {\n setIsLoadingMessages(false);\n }\n }\n }, []);\n\n const openPanel = useCallback(() => {\n setIsOpen(true);\n void loadConversations();\n }, [loadConversations]);\n\n const closePanel = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const togglePanel = useCallback(() => {\n setIsOpen((prev) => {\n if (!prev) void loadConversations();\n return !prev;\n });\n }, [loadConversations]);\n\n const selectConversation = useCallback(\n (id: string | null) => {\n setActiveConversationId(id);\n if (id) {\n void loadMessages(id);\n } else {\n invalidateMessagesLoad();\n setMessages([]);\n }\n },\n [loadMessages]\n );\n\n const startNewConversation = useCallback(() => {\n invalidateMessagesLoad();\n setActiveConversationId(null);\n setMessages([]);\n }, []);\n\n const sendMessage = useCallback(\n async (text: string, currentContext?: ChatbotCurrentContext) => {\n const trimmed = text.trim();\n // Ref-based guard: `isSending` state alone can't block a second\n // call fired in the same tick (React batches updates), so we\n // mirror the flag in a ref that flips synchronously.\n if (!trimmed || isSendingRef.current) return;\n isSendingRef.current = true;\n\n // Capture the messages-seq so we can tell if the user switched\n // conversations while the request is in flight. `messagesSeqRef`\n // is bumped by `selectConversation`, `startNewConversation`, and\n // `deleteConversation` via `invalidateMessagesLoad` — any of those\n // mid-flight means the current UI no longer reflects this send.\n const sendSeq = messagesSeqRef.current;\n\n const placeholderId = generatePlaceholderId();\n const placeholder: ChatbotMessage = {\n id: placeholderId,\n conversationId: activeConversationId ?? '',\n role: CHATBOT_MESSAGE_ROLES.USER,\n content: trimmed,\n createdAt: new Date(),\n };\n\n setMessages((prev) => [...prev, placeholder]);\n setIsSending(true);\n setErrorMessage(null);\n\n try {\n const result = await apiClientRef.current.sendMessage({\n message: trimmed,\n conversationId: activeConversationId ?? undefined,\n currentContext,\n });\n if (!mountedRef.current) return;\n\n if (sendSeq !== messagesSeqRef.current) {\n // User switched conversations mid-flight. The server still\n // persisted the new conversation — refresh the list so it\n // appears in history — but do NOT touch the currently visible\n // messages or `activeConversationId` (that would silently\n // yank the user back to the old conversation).\n void loadConversations();\n return;\n }\n\n setMessages((prev) =>\n replacePlaceholder(prev, placeholderId, result)\n );\n setActiveConversationId(result.conversationId);\n\n // Refresh conversations list so title / lastMessageAt update\n void loadConversations();\n } catch (err) {\n if (!mountedRef.current) return;\n // Only clean up the placeholder / surface the error if the user\n // is still viewing the conversation this send belongs to.\n if (sendSeq !== messagesSeqRef.current) return;\n setMessages((prev) => removePlaceholder(prev, placeholderId));\n setErrorMessage(toUserErrorMessage(err, 'Falha ao enviar mensagem'));\n } finally {\n isSendingRef.current = false;\n if (mountedRef.current) setIsSending(false);\n }\n },\n [activeConversationId, loadConversations]\n );\n\n const deleteConversation = useCallback(\n async (id: string) => {\n setErrorMessage(null);\n try {\n await apiClientRef.current.deleteConversation(id);\n if (!mountedRef.current) return;\n setConversations((prev) => removeById(prev, id));\n if (activeConversationId === id) {\n invalidateMessagesLoad();\n setActiveConversationId(null);\n setMessages([]);\n }\n } catch (err) {\n if (!mountedRef.current) return;\n setErrorMessage(toUserErrorMessage(err, 'Falha ao excluir conversa'));\n }\n },\n [activeConversationId]\n );\n\n return {\n isOpen,\n isSending,\n isLoadingHistory,\n isLoadingMessages,\n errorMessage,\n conversations,\n activeConversationId,\n messages,\n openPanel,\n closePanel,\n togglePanel,\n selectConversation,\n startNewConversation,\n sendMessage,\n deleteConversation,\n reloadConversations: loadConversations,\n };\n };\n}\n"],"mappings":";;;;;AAAA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAUzD,IAAM,kCAAkC;AACxC,IAAM,6BAA6B;AAMnC,IAAI,qBAAqB;AAOzB,SAAS,wBAAgC;AACvC,QAAM,eAAgB,WAAmC;AACzD,MAAI,gBAAgB,OAAO,aAAa,eAAe,YAAY;AACjE,WAAO,WAAW,aAAa,WAAW,CAAC;AAAA,EAC7C;AACA,wBAAsB;AACtB,SAAO,WAAW,KAAK,IAAI,CAAC,IAAI,kBAAkB;AACpD;AAKA,SAAS,mBACP,MACA,eACA,QACkB;AAClB,SAAO;AAAA,IACL,GAAG,KAAK,OAAO,CAAC,MAAM,EAAE,OAAO,aAAa;AAAA,IAC5C,OAAO;AAAA,IACP,OAAO;AAAA,EACT;AACF;AAKA,SAAS,kBACP,MACA,eACkB;AAClB,SAAO,KAAK,OAAO,CAAC,MAAM,EAAE,OAAO,aAAa;AAClD;AAKA,SAAS,WAAqC,OAAY,IAAiB;AACzE,SAAO,MAAM,OAAO,CAAC,SAAS,KAAK,OAAO,EAAE;AAC9C;AAWA,SAAS,mBAAmB,MAAe,UAA0B;AACnE,SAAO;AACT;AA8CO,SAAS,iBAAiB,UAAoC;AACnE,QAAM,gBACJ,OAAO,aAAa,aACf,WACD,MAAM;AAEZ,SAAO,SAAS,aAA+B;AAC7C,UAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,UAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,UAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,KAAK;AAC9D,UAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAS,KAAK;AAChE,UAAM,CAAC,cAAc,eAAe,IAAI,SAAwB,IAAI;AACpE,UAAM,CAAC,eAAe,gBAAgB,IAAI;AAAA,MACxC,CAAC;AAAA,IACH;AACA,UAAM,CAAC,sBAAsB,uBAAuB,IAAI,SAEtD,IAAI;AACN,UAAM,CAAC,UAAU,WAAW,IAAI,SAA2B,CAAC,CAAC;AAI7D,UAAM,eAAe,OAAyB,cAAc,CAAC;AAC7D,iBAAa,UAAU,cAAc;AAKrC,UAAM,aAAa,OAAO,KAAK;AAC/B,cAAU,MAAM;AACd,iBAAW,UAAU;AACrB,aAAO,MAAM;AACX,mBAAW,UAAU;AAAA,MACvB;AAAA,IACF,GAAG,CAAC,CAAC;AAKL,UAAM,sBAAsB,OAAO,CAAC;AACpC,UAAM,iBAAiB,OAAO,CAAC;AAI/B,UAAM,eAAe,OAAO,KAAK;AAQjC,UAAM,yBAAyB,MAAM;AACnC,qBAAe,WAAW;AAC1B,2BAAqB,KAAK;AAAA,IAC5B;AAEA,UAAM,oBAAoB,YAAY,YAAY;AAChD,YAAM,MAAM,EAAE,oBAAoB;AAClC,0BAAoB,IAAI;AACxB,sBAAgB,IAAI;AACpB,UAAI;AACF,cAAM,SAAS,MAAM,aAAa,QAAQ,kBAAkB;AAAA,UAC1D,MAAM;AAAA,UACN,OAAO;AAAA,QACT,CAAC;AACD,YAAI,CAAC,WAAW,WAAW,QAAQ,oBAAoB,QAAS;AAChE,yBAAiB,OAAO,aAAa;AAAA,MACvC,SAAS,KAAK;AACZ,YAAI,CAAC,WAAW,WAAW,QAAQ,oBAAoB,QAAS;AAChE,wBAAgB,mBAAmB,KAAK,6BAA6B,CAAC;AAAA,MACxE,UAAE;AACA,YAAI,WAAW,WAAW,QAAQ,oBAAoB,SAAS;AAC7D,8BAAoB,KAAK;AAAA,QAC3B;AAAA,MACF;AAAA,IACF,GAAG,CAAC,CAAC;AAEL,UAAM,eAAe,YAAY,OAAO,mBAA2B;AACjE,YAAM,MAAM,EAAE,eAAe;AAC7B,2BAAqB,IAAI;AACzB,sBAAgB,IAAI;AACpB,UAAI;AACF,cAAM,SAAS,MAAM,aAAa,QAAQ,YAAY,gBAAgB;AAAA,UACpE,MAAM;AAAA,UACN,OAAO;AAAA,QACT,CAAC;AACD,YAAI,CAAC,WAAW,WAAW,QAAQ,eAAe,QAAS;AAC3D,oBAAY,OAAO,QAAQ;AAAA,MAC7B,SAAS,KAAK;AACZ,YAAI,CAAC,WAAW,WAAW,QAAQ,eAAe,QAAS;AAC3D,wBAAgB,mBAAmB,KAAK,6BAA6B,CAAC;AAAA,MACxE,UAAE;AACA,YAAI,WAAW,WAAW,QAAQ,eAAe,SAAS;AACxD,+BAAqB,KAAK;AAAA,QAC5B;AAAA,MACF;AAAA,IACF,GAAG,CAAC,CAAC;AAEL,UAAM,YAAY,YAAY,MAAM;AAClC,gBAAU,IAAI;AACd,WAAK,kBAAkB;AAAA,IACzB,GAAG,CAAC,iBAAiB,CAAC;AAEtB,UAAM,aAAa,YAAY,MAAM;AACnC,gBAAU,KAAK;AAAA,IACjB,GAAG,CAAC,CAAC;AAEL,UAAM,cAAc,YAAY,MAAM;AACpC,gBAAU,CAAC,SAAS;AAClB,YAAI,CAAC,KAAM,MAAK,kBAAkB;AAClC,eAAO,CAAC;AAAA,MACV,CAAC;AAAA,IACH,GAAG,CAAC,iBAAiB,CAAC;AAEtB,UAAM,qBAAqB;AAAA,MACzB,CAAC,OAAsB;AACrB,gCAAwB,EAAE;AAC1B,YAAI,IAAI;AACN,eAAK,aAAa,EAAE;AAAA,QACtB,OAAO;AACL,iCAAuB;AACvB,sBAAY,CAAC,CAAC;AAAA,QAChB;AAAA,MACF;AAAA,MACA,CAAC,YAAY;AAAA,IACf;AAEA,UAAM,uBAAuB,YAAY,MAAM;AAC7C,6BAAuB;AACvB,8BAAwB,IAAI;AAC5B,kBAAY,CAAC,CAAC;AAAA,IAChB,GAAG,CAAC,CAAC;AAEL,UAAM,cAAc;AAAA,MAClB,OAAO,MAAc,mBAA2C;AAC9D,cAAM,UAAU,KAAK,KAAK;AAI1B,YAAI,CAAC,WAAW,aAAa,QAAS;AACtC,qBAAa,UAAU;AAOvB,cAAM,UAAU,eAAe;AAE/B,cAAM,gBAAgB,sBAAsB;AAC5C,cAAM,cAA8B;AAAA,UAClC,IAAI;AAAA,UACJ,gBAAgB,wBAAwB;AAAA,UACxC,MAAM,sBAAsB;AAAA,UAC5B,SAAS;AAAA,UACT,WAAW,oBAAI,KAAK;AAAA,QACtB;AAEA,oBAAY,CAAC,SAAS,CAAC,GAAG,MAAM,WAAW,CAAC;AAC5C,qBAAa,IAAI;AACjB,wBAAgB,IAAI;AAEpB,YAAI;AACF,gBAAM,SAAS,MAAM,aAAa,QAAQ,YAAY;AAAA,YACpD,SAAS;AAAA,YACT,gBAAgB,wBAAwB;AAAA,YACxC;AAAA,UACF,CAAC;AACD,cAAI,CAAC,WAAW,QAAS;AAEzB,cAAI,YAAY,eAAe,SAAS;AAMtC,iBAAK,kBAAkB;AACvB;AAAA,UACF;AAEA;AAAA,YAAY,CAAC,SACX,mBAAmB,MAAM,eAAe,MAAM;AAAA,UAChD;AACA,kCAAwB,OAAO,cAAc;AAG7C,eAAK,kBAAkB;AAAA,QACzB,SAAS,KAAK;AACZ,cAAI,CAAC,WAAW,QAAS;AAGzB,cAAI,YAAY,eAAe,QAAS;AACxC,sBAAY,CAAC,SAAS,kBAAkB,MAAM,aAAa,CAAC;AAC5D,0BAAgB,mBAAmB,KAAK,0BAA0B,CAAC;AAAA,QACrE,UAAE;AACA,uBAAa,UAAU;AACvB,cAAI,WAAW,QAAS,cAAa,KAAK;AAAA,QAC5C;AAAA,MACF;AAAA,MACA,CAAC,sBAAsB,iBAAiB;AAAA,IAC1C;AAEA,UAAM,qBAAqB;AAAA,MACzB,OAAO,OAAe;AACpB,wBAAgB,IAAI;AACpB,YAAI;AACF,gBAAM,aAAa,QAAQ,mBAAmB,EAAE;AAChD,cAAI,CAAC,WAAW,QAAS;AACzB,2BAAiB,CAAC,SAAS,WAAW,MAAM,EAAE,CAAC;AAC/C,cAAI,yBAAyB,IAAI;AAC/B,mCAAuB;AACvB,oCAAwB,IAAI;AAC5B,wBAAY,CAAC,CAAC;AAAA,UAChB;AAAA,QACF,SAAS,KAAK;AACZ,cAAI,CAAC,WAAW,QAAS;AACzB,0BAAgB,mBAAmB,KAAK,2BAA2B,CAAC;AAAA,QACtE;AAAA,MACF;AAAA,MACA,CAAC,oBAAoB;AAAA,IACvB;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,qBAAqB;AAAA,IACvB;AAAA,EACF;AACF;","names":[]}