{"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":"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","names":[]}