@cossistant/react 0.0.29 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_virtual/rolldown_runtime.js +9 -23
- package/hooks/index.d.ts +2 -2
- package/hooks/private/store/use-conversations-store.d.ts +2 -0
- package/hooks/private/store/use-conversations-store.d.ts.map +1 -1
- package/hooks/private/store/use-conversations-store.js +15 -8
- package/hooks/private/store/use-conversations-store.js.map +1 -1
- package/hooks/private/store/use-store-selector.d.ts +3 -0
- package/hooks/private/store/use-store-selector.d.ts.map +1 -1
- package/hooks/private/store/use-store-selector.js +4 -8
- package/hooks/private/store/use-store-selector.js.map +1 -1
- package/hooks/private/store/use-website-store.d.ts +3 -1
- package/hooks/private/store/use-website-store.d.ts.map +1 -1
- package/hooks/private/store/use-website-store.js +14 -6
- package/hooks/private/store/use-website-store.js.map +1 -1
- package/hooks/private/use-client-query.d.ts +1 -1
- package/hooks/private/use-client-query.d.ts.map +1 -1
- package/hooks/private/use-client-query.js +1 -0
- package/hooks/private/use-client-query.js.map +1 -1
- package/hooks/private/use-default-messages.d.ts +1 -1
- package/hooks/private/use-grouped-messages.d.ts +2 -2
- package/hooks/private/use-rest-client.d.ts +13 -3
- package/hooks/private/use-rest-client.d.ts.map +1 -1
- package/hooks/private/use-rest-client.js +49 -22
- package/hooks/private/use-rest-client.js.map +1 -1
- package/hooks/private/use-visitor-typing-reporter.d.ts +1 -1
- package/hooks/use-conversation-auto-seen.d.ts +1 -1
- package/hooks/use-conversation-page.d.ts +1 -1
- package/hooks/use-conversation-page.d.ts.map +1 -1
- package/hooks/use-conversation-page.js +10 -3
- package/hooks/use-conversation-page.js.map +1 -1
- package/hooks/use-conversation-preview.d.ts +3 -1
- package/hooks/use-conversation-preview.d.ts.map +1 -1
- package/hooks/use-conversation-preview.js +6 -3
- package/hooks/use-conversation-preview.js.map +1 -1
- package/hooks/use-conversation-seen.d.ts +1 -1
- package/hooks/use-conversation-timeline-items.d.ts +1 -1
- package/hooks/use-conversation-timeline-items.js +2 -3
- package/hooks/use-conversation-timeline-items.js.map +1 -1
- package/hooks/use-conversation-timeline.d.ts +1 -1
- package/hooks/use-conversation.d.ts +1 -1
- package/hooks/use-conversation.js +2 -3
- package/hooks/use-conversation.js.map +1 -1
- package/hooks/use-conversations.d.ts +1 -1
- package/hooks/use-conversations.js +5 -3
- package/hooks/use-conversations.js.map +1 -1
- package/hooks/use-create-conversation.d.ts +3 -3
- package/hooks/use-create-conversation.js +1 -0
- package/hooks/use-create-conversation.js.map +1 -1
- package/hooks/use-file-upload.d.ts +1 -1
- package/hooks/use-file-upload.js +3 -3
- package/hooks/use-file-upload.js.map +1 -1
- package/hooks/use-home-page.js +3 -3
- package/hooks/use-home-page.js.map +1 -1
- package/hooks/use-message-composer.d.ts +10 -3
- package/hooks/use-message-composer.d.ts.map +1 -1
- package/hooks/use-message-composer.js +5 -2
- package/hooks/use-message-composer.js.map +1 -1
- package/hooks/use-realtime-support.d.ts +1 -1
- package/hooks/use-send-message.d.ts +8 -2
- package/hooks/use-send-message.d.ts.map +1 -1
- package/hooks/use-send-message.js +5 -3
- package/hooks/use-send-message.js.map +1 -1
- package/hooks/use-visitor.js +2 -2
- package/hooks/use-visitor.js.map +1 -1
- package/identify-visitor.d.ts.map +1 -1
- package/identify-visitor.js +15 -1
- package/identify-visitor.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/package.json +5 -3
- package/{conversation.d.ts → packages/types/src/api/conversation.d.ts} +365 -61
- package/packages/types/src/api/conversation.d.ts.map +1 -0
- package/packages/types/src/api/timeline-item.d.ts +460 -0
- package/packages/types/src/api/timeline-item.d.ts.map +1 -0
- package/{realtime-events.d.ts → packages/types/src/realtime-events.d.ts} +443 -45
- package/packages/types/src/realtime-events.d.ts.map +1 -0
- package/{schemas3.d.ts → packages/types/src/schemas.d.ts} +94 -18
- package/packages/types/src/schemas.d.ts.map +1 -0
- package/primitives/avatar/avatar.js +1 -1
- package/primitives/avatar/avatar.js.map +1 -1
- package/primitives/avatar/fallback.js +1 -1
- package/primitives/avatar/fallback.js.map +1 -1
- package/primitives/avatar/image.js +1 -1
- package/primitives/avatar/image.js.map +1 -1
- package/primitives/button.js +1 -1
- package/primitives/button.js.map +1 -1
- package/primitives/conversation-timeline.d.ts +1 -1
- package/primitives/conversation-timeline.js +4 -4
- package/primitives/conversation-timeline.js.map +1 -1
- package/primitives/day-separator.js +3 -3
- package/primitives/day-separator.js.map +1 -1
- package/primitives/multimodal-input.js +2 -2
- package/primitives/multimodal-input.js.map +1 -1
- package/primitives/timeline-item-attachments.d.ts +1 -1
- package/primitives/timeline-item-attachments.js +6 -7
- package/primitives/timeline-item-attachments.js.map +1 -1
- package/primitives/timeline-item-group.d.ts +1 -1
- package/primitives/timeline-item-group.js +7 -7
- package/primitives/timeline-item-group.js.map +1 -1
- package/primitives/timeline-item.d.ts +1 -1
- package/primitives/timeline-item.d.ts.map +1 -1
- package/primitives/timeline-item.js +33 -8
- package/primitives/timeline-item.js.map +1 -1
- package/primitives/trigger.js +1 -1
- package/primitives/trigger.js.map +1 -1
- package/primitives/window.js +1 -1
- package/primitives/window.js.map +1 -1
- package/provider.d.ts +4 -2
- package/provider.d.ts.map +1 -1
- package/provider.js +56 -8
- package/provider.js.map +1 -1
- package/realtime/event-filter.d.ts +4 -1
- package/realtime/event-filter.d.ts.map +1 -1
- package/realtime/event-filter.js +14 -0
- package/realtime/event-filter.js.map +1 -1
- package/realtime/provider.d.ts +1 -1
- package/realtime/provider.d.ts.map +1 -1
- package/realtime/provider.js +1 -2
- package/realtime/provider.js.map +1 -1
- package/realtime/seen-store.d.ts +2 -2
- package/realtime/support-provider.js +3 -2
- package/realtime/support-provider.js.map +1 -1
- package/realtime/typing-store.d.ts +1 -1
- package/realtime/use-realtime.d.ts +1 -1
- package/support/components/avatar-stack.d.ts.map +1 -1
- package/support/components/avatar-stack.js +32 -12
- package/support/components/avatar-stack.js.map +1 -1
- package/support/components/avatar.d.ts +34 -3
- package/support/components/avatar.d.ts.map +1 -1
- package/support/components/avatar.js +61 -8
- package/support/components/avatar.js.map +1 -1
- package/support/components/button.d.ts +3 -1
- package/support/components/button.d.ts.map +1 -1
- package/support/components/button.js +3 -3
- package/support/components/button.js.map +1 -1
- package/support/components/configuration-error.d.ts +16 -0
- package/support/components/configuration-error.d.ts.map +1 -0
- package/support/components/configuration-error.js +162 -0
- package/support/components/configuration-error.js.map +1 -0
- package/support/components/content.js +1 -2
- package/support/components/content.js.map +1 -1
- package/support/components/conversation-button-link.js +18 -23
- package/support/components/conversation-button-link.js.map +1 -1
- package/support/components/conversation-event.d.ts.map +1 -1
- package/support/components/conversation-event.js +7 -5
- package/support/components/conversation-event.js.map +1 -1
- package/support/components/conversation-timeline.d.ts +1 -1
- package/support/components/conversation-timeline.js +3 -3
- package/support/components/conversation-timeline.js.map +1 -1
- package/support/components/header.js +1 -1
- package/support/components/image-lightbox.d.ts +1 -1
- package/support/components/image-lightbox.js +1 -2
- package/support/components/image-lightbox.js.map +1 -1
- package/support/components/index.js +1 -1
- package/support/components/multimodal-input.js +0 -1
- package/support/components/multimodal-input.js.map +1 -1
- package/support/components/navigation-tab.js +1 -1
- package/support/components/online-indicator.d.ts +50 -0
- package/support/components/online-indicator.d.ts.map +1 -0
- package/support/components/online-indicator.js +65 -0
- package/support/components/online-indicator.js.map +1 -0
- package/support/components/root.js +0 -1
- package/support/components/root.js.map +1 -1
- package/support/components/timeline-identification-tool.js +4 -4
- package/support/components/timeline-identification-tool.js.map +1 -1
- package/support/components/timeline-message-group.d.ts +1 -1
- package/support/components/timeline-message-group.d.ts.map +1 -1
- package/support/components/timeline-message-group.js +6 -4
- package/support/components/timeline-message-group.js.map +1 -1
- package/support/components/timeline-message-item.d.ts +1 -1
- package/support/components/timeline-message-item.js +4 -4
- package/support/components/timeline-message-item.js.map +1 -1
- package/support/components/trigger.js +1 -2
- package/support/components/trigger.js.map +1 -1
- package/support/components/typing-indicator.js +1 -1
- package/support/components/typing-indicator.js.map +1 -1
- package/support/context/controlled-state.js +0 -1
- package/support/context/controlled-state.js.map +1 -1
- package/support/context/events.d.ts +1 -1
- package/support/context/events.js +0 -1
- package/support/context/events.js.map +1 -1
- package/support/context/handle.js +0 -1
- package/support/context/handle.js.map +1 -1
- package/support/context/identification.d.ts +33 -0
- package/support/context/identification.d.ts.map +1 -0
- package/support/context/identification.js +34 -0
- package/support/context/identification.js.map +1 -0
- package/support/context/positioning.js +0 -1
- package/support/context/positioning.js.map +1 -1
- package/support/context/slots.js +0 -1
- package/support/context/slots.js.map +1 -1
- package/support/context/websocket.d.ts +1 -1
- package/support/context/websocket.js +0 -1
- package/support/context/websocket.js.map +1 -1
- package/support/index.d.ts.map +1 -1
- package/support/index.js +51 -18
- package/support/index.js.map +1 -1
- package/support/pages/conversation-history.js +2 -1
- package/support/pages/conversation-history.js.map +1 -1
- package/support/pages/conversation.d.ts +1 -1
- package/support/pages/conversation.js +1 -1
- package/support/pages/conversation.js.map +1 -1
- package/support/pages/home.js +5 -3
- package/support/pages/home.js.map +1 -1
- package/support/router.d.ts.map +1 -1
- package/support/router.js +4 -0
- package/support/router.js.map +1 -1
- package/support/store/support-store.js +0 -1
- package/support/store/support-store.js.map +1 -1
- package/support/{support-C7Xaw-N6.css → support-DmViRaga.css} +2 -2
- package/support/{support-C7Xaw-N6.css.map → support-DmViRaga.css.map} +1 -1
- package/support/text/index.js +1 -1
- package/support/text/index.js.map +1 -1
- package/support/text/locales/en.js +1 -1
- package/support/text/locales/en.js.map +1 -1
- package/support/text/locales/es.js +1 -1
- package/support/text/locales/es.js.map +1 -1
- package/support/text/locales/fr.js +1 -1
- package/support/text/locales/fr.js.map +1 -1
- package/support/utils/index.d.ts +1 -1
- package/support-config.js +0 -1
- package/support-config.js.map +1 -1
- package/support.css +1 -1
- package/tailwind.css +1 -1
- package/utils/conversation.d.ts.map +1 -1
- package/utils/conversation.js +1 -3
- package/utils/conversation.js.map +1 -1
- package/utils/use-render-element.js +2 -2
- package/utils/use-render-element.js.map +1 -1
- package/api.d.ts +0 -71
- package/api.d.ts.map +0 -1
- package/checks.d.ts +0 -189
- package/checks.d.ts.map +0 -1
- package/clsx.d.ts +0 -7
- package/clsx.d.ts.map +0 -1
- package/coerce.d.ts +0 -9
- package/coerce.d.ts.map +0 -1
- package/conversation.d.ts.map +0 -1
- package/core.d.ts +0 -35
- package/core.d.ts.map +0 -1
- package/errors.d.ts +0 -130
- package/errors.d.ts.map +0 -1
- package/errors2.d.ts +0 -24
- package/errors2.d.ts.map +0 -1
- package/index2.d.ts +0 -4
- package/index3.d.ts +0 -1
- package/json-schema.d.ts +0 -70
- package/json-schema.d.ts.map +0 -1
- package/metadata.d.ts +0 -1
- package/openapi-generator.d.ts +0 -1
- package/openapi-generator2.d.ts +0 -1
- package/openapi-generator3.d.ts +0 -1
- package/openapi30.d.ts +0 -125
- package/openapi30.d.ts.map +0 -1
- package/openapi31.d.ts +0 -131
- package/openapi31.d.ts.map +0 -1
- package/parse.d.ts +0 -17
- package/parse.d.ts.map +0 -1
- package/realtime-events.d.ts.map +0 -1
- package/registries.d.ts +0 -32
- package/registries.d.ts.map +0 -1
- package/schemas.d.ts +0 -971
- package/schemas.d.ts.map +0 -1
- package/schemas2.d.ts +0 -345
- package/schemas2.d.ts.map +0 -1
- package/schemas3.d.ts.map +0 -1
- package/specification-extension.d.ts +0 -9
- package/specification-extension.d.ts.map +0 -1
- package/standard-schema.d.ts +0 -121
- package/standard-schema.d.ts.map +0 -1
- package/timeline-item.d.ts +0 -227
- package/timeline-item.d.ts.map +0 -1
- package/to-json-schema.d.ts +0 -96
- package/to-json-schema.d.ts.map +0 -1
- package/util.d.ts +0 -45
- package/util.d.ts.map +0 -1
- package/versions.d.ts +0 -9
- package/versions.d.ts.map +0 -1
- package/zod-extensions.d.ts +0 -39
- package/zod-extensions.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversation-page.js","names":["identificationItem: TimelineItem"],"sources":["../../src/hooks/use-conversation-page.ts"],"sourcesContent":["import type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport {\n\tConversationTimelineType,\n\tTimelineItemVisibility,\n} from \"@cossistant/types/enums\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useWebSocketSafe } from \"../support/context/websocket\";\nimport { useDefaultMessages } from \"./private/use-default-messages\";\nimport { useConversationAutoSeen } from \"./use-conversation-auto-seen\";\nimport { useConversationLifecycle } from \"./use-conversation-lifecycle\";\nimport { useConversationTimelineItems } from \"./use-conversation-timeline-items\";\nimport { useMessageComposer } from \"./use-message-composer\";\n\nexport type UseConversationPageOptions = {\n\t/**\n\t * Initial conversation ID (from URL params, navigation state, etc.)\n\t * Can be PENDING_CONVERSATION_ID or a real ID.\n\t */\n\tconversationId: string;\n\n\t/**\n\t * Optional initial message to send when the conversation opens.\n\t */\n\tinitialMessage?: string;\n\n\t/**\n\t * Callback when conversation ID changes (e.g., after creation).\n\t * Use this to update navigation state or URL.\n\t */\n\tonConversationIdChange?: (conversationId: string) => void;\n\n\t/**\n\t * Optional timeline items to pass through (e.g., optimistic updates).\n\t */\n\titems?: TimelineItem[];\n\n\t/**\n\t * Whether automatic \"seen\" tracking should be enabled.\n\t * Set to false when the conversation isn't visible (e.g. widget closed).\n\t * Default: true\n\t */\n\tautoSeenEnabled?: boolean;\n};\n\nexport type UseConversationPageReturn = {\n\t// Conversation state\n\tconversationId: string;\n\tisPending: boolean;\n\titems: TimelineItem[];\n\tisLoading: boolean;\n\terror: Error | null;\n\n\t// Message composer\n\tcomposer: {\n\t\tmessage: string;\n\t\tfiles: File[];\n\t\tisSubmitting: boolean;\n\t\tisUploading: boolean;\n\t\tcanSubmit: boolean;\n\t\tsetMessage: (message: string) => void;\n\t\taddFiles: (files: File[]) => void;\n\t\tremoveFile: (index: number) => void;\n\t\tsubmit: () => void;\n\t};\n\n\t// UI helpers\n\thasItems: boolean;\n\tlastTimelineItem: TimelineItem | null;\n};\n\n/**\n * Main orchestrator hook for the conversation page.\n *\n * This hook combines all conversation-related logic:\n * - Lifecycle management (pending → real conversation)\n * - Message fetching and display\n * - Message composition and sending\n * - Automatic seen tracking\n * - Default/welcome messages before conversation is created\n *\n * It provides a clean, simple API for building conversation UIs.\n *\n * @example\n * ```tsx\n * export function ConversationPage({ conversationId: initialId }) {\n * const conversation = useConversationPage({\n * conversationId: initialId,\n * onConversationIdChange: (newId) => {\n * // Update URL or navigation state\n * navigate(`/conversation/${newId}`);\n * },\n * });\n *\n * return (\n * <>\n * <MessageList messages={conversation.messages} />\n * <MessageInput\n * value={conversation.composer.message}\n * onChange={conversation.composer.setMessage}\n * onSubmit={conversation.composer.submit}\n * />\n * </>\n * );\n * }\n * ```\n */\nexport function useConversationPage(\n\toptions: UseConversationPageOptions\n): UseConversationPageReturn {\n\tconst {\n\t\tconversationId: initialConversationId,\n\t\tinitialMessage,\n\t\tonConversationIdChange,\n\t\titems: passedItems = [],\n\t\tautoSeenEnabled = true,\n\t} = options;\n\n\tconst { client, visitor } = useSupport();\n\tconst websocket = useWebSocketSafe();\n\n\tconst trimmedInitialMessage = initialMessage?.trim() ?? \"\";\n\tconst hasInitialMessage = trimmedInitialMessage.length > 0;\n\n\t// 1. Manage conversation lifecycle (pending vs real)\n\tconst lifecycle = useConversationLifecycle({\n\t\tinitialConversationId,\n\t\tonConversationCreated: onConversationIdChange,\n\t});\n\n\t// 2. Get default timeline items for pending state\n\tconst defaultTimelineItems = useDefaultMessages({\n\t\tconversationId: lifecycle.conversationId,\n\t});\n\n\tconst effectiveDefaultTimelineItems = hasInitialMessage\n\t\t? []\n\t\t: defaultTimelineItems;\n\n\t// 3. Fetch timeline items from backend if real conversation exists\n\tconst timelineQuery = useConversationTimelineItems(lifecycle.conversationId, {\n\t\tenabled: !lifecycle.isPending,\n\t});\n\n\t// 4. Determine which items to display\n\tconst baseItems = useMemo(() => {\n\t\t// If we have fetched timeline items, use them\n\t\tif (timelineQuery.items.length > 0) {\n\t\t\treturn timelineQuery.items;\n\t\t}\n\n\t\t// If pending, use default timeline items\n\t\tif (lifecycle.isPending && effectiveDefaultTimelineItems.length > 0) {\n\t\t\treturn effectiveDefaultTimelineItems;\n\t\t}\n\n\t\t// Use passed items as fallback\n\t\tif (passedItems.length > 0) {\n\t\t\treturn passedItems;\n\t\t}\n\n\t\treturn [];\n\t}, [\n\t\ttimelineQuery.items,\n\t\tlifecycle.isPending,\n\t\teffectiveDefaultTimelineItems,\n\t\tpassedItems,\n\t]);\n\n\tconst shouldShowIdentificationTool = useMemo(() => {\n\t\tif (lifecycle.isPending) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (visitor?.contact) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn !baseItems.some(\n\t\t\t(item) => item.type === ConversationTimelineType.IDENTIFICATION\n\t\t);\n\t}, [baseItems, lifecycle.isPending, visitor?.contact]);\n\n\tconst displayItems = useMemo(() => {\n\t\tif (!shouldShowIdentificationTool) {\n\t\t\treturn baseItems;\n\t\t}\n\n\t\tconst organizationId =\n\t\t\tbaseItems.at(-1)?.organizationId ??\n\t\t\tclient.getConfiguration().organizationId ??\n\t\t\t\"\";\n\n\t\tconst identificationItem: TimelineItem = {\n\t\t\tid: `identification-${lifecycle.conversationId}`,\n\t\t\tconversationId: lifecycle.conversationId,\n\t\t\torganizationId,\n\t\t\tvisibility: TimelineItemVisibility.PUBLIC,\n\t\t\ttype: ConversationTimelineType.IDENTIFICATION,\n\t\t\ttext: null,\n\t\t\ttool: \"identification\",\n\t\t\tparts: [],\n\t\t\tuserId: null,\n\t\t\tvisitorId: visitor?.id ?? null,\n\t\t\taiAgentId: null,\n\t\t\tcreatedAt: typeof window !== \"undefined\" ? new Date().toISOString() : \"\",\n\t\t\tdeletedAt: null,\n\t\t};\n\n\t\treturn [...baseItems, identificationItem];\n\t}, [\n\t\tbaseItems,\n\t\tclient,\n\t\tlifecycle.conversationId,\n\t\tshouldShowIdentificationTool,\n\t\tvisitor?.id,\n\t]);\n\n\tconst lastTimelineItem = useMemo(\n\t\t() => displayItems.at(-1) ?? null,\n\t\t[displayItems]\n\t);\n\n\t// 5. Set up message composer\n\tconst composer = useMessageComposer({\n\t\tclient,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tdefaultTimelineItems: effectiveDefaultTimelineItems,\n\t\tvisitorId: visitor?.id,\n\t\tonMessageSent: (newConversationId) => {\n\t\t\t// Transition from pending to real conversation\n\t\t\tif (lifecycle.isPending) {\n\t\t\t\tlifecycle.setConversationId(newConversationId);\n\t\t\t}\n\t\t},\n\t\t// Pass WebSocket connection for real-time typing events\n\t\trealtimeSend: websocket?.send ?? null,\n\t\tisRealtimeConnected: websocket?.isConnected ?? false,\n\t});\n\n\tconst initialMessageSubmittedRef = useRef(false);\n\tconst lastInitialMessageRef = useRef<string | null>(null);\n\n\tuseEffect(() => {\n\t\tif (!hasInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\tif (lastInitialMessageRef.current !== trimmedInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = trimmedInitialMessage;\n\t\t}\n\n\t\tif (!lifecycle.isPending) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (composer.message !== trimmedInitialMessage) {\n\t\t\tcomposer.setMessage(trimmedInitialMessage);\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\tinitialMessageSubmittedRef.current ||\n\t\t\tcomposer.isSubmitting ||\n\t\t\t!composer.canSubmit\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tinitialMessageSubmittedRef.current = true;\n\t\tcomposer.submit();\n\t}, [\n\t\thasInitialMessage,\n\t\tlifecycle.isPending,\n\t\tcomposer.message,\n\t\tcomposer.setMessage,\n\t\tcomposer.isSubmitting,\n\t\tcomposer.canSubmit,\n\t\tcomposer.submit,\n\t\ttrimmedInitialMessage,\n\t]);\n\n\t// 6. Auto-mark messages as seen\n\tuseConversationAutoSeen({\n\t\tclient,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tvisitorId: visitor?.id,\n\t\tlastTimelineItem,\n\t\tenabled: autoSeenEnabled,\n\t\tisWidgetOpen: autoSeenEnabled,\n\t});\n\n\treturn {\n\t\tconversationId: lifecycle.conversationId,\n\t\tisPending: lifecycle.isPending,\n\t\titems: displayItems,\n\t\tisLoading: timelineQuery.isLoading,\n\t\terror: timelineQuery.error || composer.error,\n\t\tcomposer: {\n\t\t\tmessage: composer.message,\n\t\t\tfiles: composer.files,\n\t\t\tisSubmitting: composer.isSubmitting,\n\t\t\tisUploading: composer.isUploading,\n\t\t\tcanSubmit: composer.canSubmit,\n\t\t\tsetMessage: composer.setMessage,\n\t\t\taddFiles: composer.addFiles,\n\t\t\tremoveFile: composer.removeFile,\n\t\t\tsubmit: composer.submit,\n\t\t},\n\t\thasItems: displayItems.length > 0,\n\t\tlastTimelineItem,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-conversation-page.js","names":["identificationItem: TimelineItem"],"sources":["../../src/hooks/use-conversation-page.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport {\n\tConversationTimelineType,\n\tTimelineItemVisibility,\n} from \"@cossistant/types/enums\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useIdentificationState } from \"../support/context/identification\";\nimport { useWebSocketSafe } from \"../support/context/websocket\";\nimport { useDefaultMessages } from \"./private/use-default-messages\";\nimport { useConversationAutoSeen } from \"./use-conversation-auto-seen\";\nimport { useConversationLifecycle } from \"./use-conversation-lifecycle\";\nimport { useConversationTimelineItems } from \"./use-conversation-timeline-items\";\nimport { useMessageComposer } from \"./use-message-composer\";\n\nexport type UseConversationPageOptions = {\n\t/**\n\t * Initial conversation ID (from URL params, navigation state, etc.)\n\t * Can be PENDING_CONVERSATION_ID or a real ID.\n\t */\n\tconversationId: string;\n\n\t/**\n\t * Optional initial message to send when the conversation opens.\n\t */\n\tinitialMessage?: string;\n\n\t/**\n\t * Callback when conversation ID changes (e.g., after creation).\n\t * Use this to update navigation state or URL.\n\t */\n\tonConversationIdChange?: (conversationId: string) => void;\n\n\t/**\n\t * Optional timeline items to pass through (e.g., optimistic updates).\n\t */\n\titems?: TimelineItem[];\n\n\t/**\n\t * Whether automatic \"seen\" tracking should be enabled.\n\t * Set to false when the conversation isn't visible (e.g. widget closed).\n\t * Default: true\n\t */\n\tautoSeenEnabled?: boolean;\n};\n\nexport type UseConversationPageReturn = {\n\t// Conversation state\n\tconversationId: string;\n\tisPending: boolean;\n\titems: TimelineItem[];\n\tisLoading: boolean;\n\terror: Error | null;\n\n\t// Message composer\n\tcomposer: {\n\t\tmessage: string;\n\t\tfiles: File[];\n\t\tisSubmitting: boolean;\n\t\tisUploading: boolean;\n\t\tcanSubmit: boolean;\n\t\tsetMessage: (message: string) => void;\n\t\taddFiles: (files: File[]) => void;\n\t\tremoveFile: (index: number) => void;\n\t\tsubmit: () => void;\n\t};\n\n\t// UI helpers\n\thasItems: boolean;\n\tlastTimelineItem: TimelineItem | null;\n};\n\n/**\n * Main orchestrator hook for the conversation page.\n *\n * This hook combines all conversation-related logic:\n * - Lifecycle management (pending → real conversation)\n * - Message fetching and display\n * - Message composition and sending\n * - Automatic seen tracking\n * - Default/welcome messages before conversation is created\n *\n * It provides a clean, simple API for building conversation UIs.\n *\n * @example\n * ```tsx\n * export function ConversationPage({ conversationId: initialId }) {\n * const conversation = useConversationPage({\n * conversationId: initialId,\n * onConversationIdChange: (newId) => {\n * // Update URL or navigation state\n * navigate(`/conversation/${newId}`);\n * },\n * });\n *\n * return (\n * <>\n * <MessageList messages={conversation.messages} />\n * <MessageInput\n * value={conversation.composer.message}\n * onChange={conversation.composer.setMessage}\n * onSubmit={conversation.composer.submit}\n * />\n * </>\n * );\n * }\n * ```\n */\nexport function useConversationPage(\n\toptions: UseConversationPageOptions\n): UseConversationPageReturn {\n\tconst {\n\t\tconversationId: initialConversationId,\n\t\tinitialMessage,\n\t\tonConversationIdChange,\n\t\titems: passedItems = [],\n\t\tautoSeenEnabled = true,\n\t} = options;\n\n\tconst { client, visitor } = useSupport();\n\tconst websocket = useWebSocketSafe();\n\tconst identificationState = useIdentificationState();\n\n\tconst trimmedInitialMessage = initialMessage?.trim() ?? \"\";\n\tconst hasInitialMessage = trimmedInitialMessage.length > 0;\n\n\t// 1. Manage conversation lifecycle (pending vs real)\n\tconst lifecycle = useConversationLifecycle({\n\t\tinitialConversationId,\n\t\tonConversationCreated: onConversationIdChange,\n\t});\n\n\t// 2. Get default timeline items for pending state\n\tconst defaultTimelineItems = useDefaultMessages({\n\t\tconversationId: lifecycle.conversationId,\n\t});\n\n\tconst effectiveDefaultTimelineItems = hasInitialMessage\n\t\t? []\n\t\t: defaultTimelineItems;\n\n\t// 3. Fetch timeline items from backend if real conversation exists\n\tconst timelineQuery = useConversationTimelineItems(lifecycle.conversationId, {\n\t\tenabled: !lifecycle.isPending,\n\t});\n\n\t// 4. Determine which items to display\n\tconst baseItems = useMemo(() => {\n\t\t// If we have fetched timeline items, use them\n\t\tif (timelineQuery.items.length > 0) {\n\t\t\treturn timelineQuery.items;\n\t\t}\n\n\t\t// If pending, use default timeline items\n\t\tif (lifecycle.isPending && effectiveDefaultTimelineItems.length > 0) {\n\t\t\treturn effectiveDefaultTimelineItems;\n\t\t}\n\n\t\t// Use passed items as fallback\n\t\tif (passedItems.length > 0) {\n\t\t\treturn passedItems;\n\t\t}\n\n\t\treturn [];\n\t}, [\n\t\ttimelineQuery.items,\n\t\tlifecycle.isPending,\n\t\teffectiveDefaultTimelineItems,\n\t\tpassedItems,\n\t]);\n\n\tconst shouldShowIdentificationTool = useMemo(() => {\n\t\tif (lifecycle.isPending) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Don't show identification form while identification is in progress\n\t\t// This prevents the form from flashing when an authenticated user opens the widget\n\t\tif (identificationState?.isIdentifying) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (visitor?.contact) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn !baseItems.some(\n\t\t\t(item) => item.type === ConversationTimelineType.IDENTIFICATION\n\t\t);\n\t}, [\n\t\tbaseItems,\n\t\tlifecycle.isPending,\n\t\tvisitor?.contact,\n\t\tidentificationState?.isIdentifying,\n\t]);\n\n\tconst displayItems = useMemo(() => {\n\t\tif (!shouldShowIdentificationTool) {\n\t\t\treturn baseItems;\n\t\t}\n\n\t\tconst organizationId =\n\t\t\tbaseItems.at(-1)?.organizationId ??\n\t\t\tclient?.getConfiguration().organizationId ??\n\t\t\t\"\";\n\n\t\tconst identificationItem: TimelineItem = {\n\t\t\tid: `identification-${lifecycle.conversationId}`,\n\t\t\tconversationId: lifecycle.conversationId,\n\t\t\torganizationId,\n\t\t\tvisibility: TimelineItemVisibility.PUBLIC,\n\t\t\ttype: ConversationTimelineType.IDENTIFICATION,\n\t\t\ttext: null,\n\t\t\ttool: \"identification\",\n\t\t\tparts: [],\n\t\t\tuserId: null,\n\t\t\tvisitorId: visitor?.id ?? null,\n\t\t\taiAgentId: null,\n\t\t\tcreatedAt: typeof window !== \"undefined\" ? new Date().toISOString() : \"\",\n\t\t\tdeletedAt: null,\n\t\t};\n\n\t\treturn [...baseItems, identificationItem];\n\t}, [\n\t\tbaseItems,\n\t\tclient,\n\t\tlifecycle.conversationId,\n\t\tshouldShowIdentificationTool,\n\t\tvisitor?.id,\n\t]);\n\n\tconst lastTimelineItem = useMemo(\n\t\t() => displayItems.at(-1) ?? null,\n\t\t[displayItems]\n\t);\n\n\t// 5. Set up message composer\n\tconst composer = useMessageComposer({\n\t\tclient: client ?? undefined,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tdefaultTimelineItems: effectiveDefaultTimelineItems,\n\t\tvisitorId: visitor?.id,\n\t\tonConversationInitiated: (newConversationId) => {\n\t\t\t// Immediately switch to new conversation ID for optimistic updates\n\t\t\t// This happens BEFORE the API call, so the UI starts reading from\n\t\t\t// the correct store key right away\n\t\t\tif (lifecycle.isPending) {\n\t\t\t\tlifecycle.setConversationId(newConversationId);\n\t\t\t}\n\t\t},\n\t\tonMessageSent: (newConversationId) => {\n\t\t\t// Also handle this for completeness (API call completed)\n\t\t\tif (lifecycle.isPending) {\n\t\t\t\tlifecycle.setConversationId(newConversationId);\n\t\t\t}\n\t\t},\n\t\t// Pass WebSocket connection for real-time typing events\n\t\trealtimeSend: websocket?.send ?? null,\n\t\tisRealtimeConnected: websocket?.isConnected ?? false,\n\t});\n\n\tconst initialMessageSubmittedRef = useRef(false);\n\tconst lastInitialMessageRef = useRef<string | null>(null);\n\n\tuseEffect(() => {\n\t\tif (!hasInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\tif (lastInitialMessageRef.current !== trimmedInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = trimmedInitialMessage;\n\t\t}\n\n\t\tif (!lifecycle.isPending) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (composer.message !== trimmedInitialMessage) {\n\t\t\tcomposer.setMessage(trimmedInitialMessage);\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\tinitialMessageSubmittedRef.current ||\n\t\t\tcomposer.isSubmitting ||\n\t\t\t!composer.canSubmit\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tinitialMessageSubmittedRef.current = true;\n\t\tcomposer.submit();\n\t}, [\n\t\thasInitialMessage,\n\t\tlifecycle.isPending,\n\t\tcomposer.message,\n\t\tcomposer.setMessage,\n\t\tcomposer.isSubmitting,\n\t\tcomposer.canSubmit,\n\t\tcomposer.submit,\n\t\ttrimmedInitialMessage,\n\t]);\n\n\t// 6. Auto-mark messages as seen\n\tuseConversationAutoSeen({\n\t\tclient,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tvisitorId: visitor?.id,\n\t\tlastTimelineItem,\n\t\tenabled: autoSeenEnabled,\n\t\tisWidgetOpen: autoSeenEnabled,\n\t});\n\n\treturn {\n\t\tconversationId: lifecycle.conversationId,\n\t\tisPending: lifecycle.isPending,\n\t\titems: displayItems,\n\t\tisLoading: timelineQuery.isLoading,\n\t\terror: timelineQuery.error || composer.error,\n\t\tcomposer: {\n\t\t\tmessage: composer.message,\n\t\t\tfiles: composer.files,\n\t\t\tisSubmitting: composer.isSubmitting,\n\t\t\tisUploading: composer.isUploading,\n\t\t\tcanSubmit: composer.canSubmit,\n\t\t\tsetMessage: composer.setMessage,\n\t\t\taddFiles: composer.addFiles,\n\t\t\tremoveFile: composer.removeFile,\n\t\t\tsubmit: composer.submit,\n\t\t},\n\t\thasItems: displayItems.length > 0,\n\t\tlastTimelineItem,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TimelineItem } from "../timeline-item.js";
|
|
1
|
+
import { TimelineItem } from "../packages/types/src/api/timeline-item.js";
|
|
2
2
|
import { PreviewTypingParticipant } from "./private/typing.js";
|
|
3
3
|
import { useConversationTimelineItems } from "./use-conversation-timeline-items.js";
|
|
4
4
|
import { Conversation } from "@cossistant/types";
|
|
@@ -15,6 +15,8 @@ type ConversationPreviewAssignedAgent = {
|
|
|
15
15
|
name: string;
|
|
16
16
|
image: string | null;
|
|
17
17
|
type: "human" | "ai" | "fallback";
|
|
18
|
+
/** Last seen timestamp for human agents, used for online status indicator */
|
|
19
|
+
lastSeenAt?: string | null;
|
|
18
20
|
};
|
|
19
21
|
type ConversationPreviewTypingParticipant = PreviewTypingParticipant;
|
|
20
22
|
type ConversationPreviewTypingState = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversation-preview.d.ts","names":[],"sources":["../../src/hooks/use-conversation-preview.ts"],"sourcesContent":[],"mappings":";;;;;;KAcY,8BAAA;;EAAA,IAAA,EAAA,MAAA;EAQA,aAAA,EAAA,OAAA;
|
|
1
|
+
{"version":3,"file":"use-conversation-preview.d.ts","names":[],"sources":["../../src/hooks/use-conversation-preview.ts"],"sourcesContent":[],"mappings":";;;;;;KAcY,8BAAA;;EAAA,IAAA,EAAA,MAAA;EAQA,aAAA,EAAA,OAAA;EAQA,UAAA,CAAA,EAAA,MAAA;EAEA,WAAA,CAAA,EAAA,MAAA,GAAA,IAAA;AAOZ,CAAA;AAsBY,KAvCA,gCAAA,GAuC4B;EACzB,IAAA,EAAA,MAAA;EAED,KAAA,EAAA,MAAA,GAAA,IAAA;EACE,IAAA,EAAA,OAAA,GAAA,IAAA,GAAA,UAAA;EACP;EACoB,UAAA,CAAA,EAAA,MAAA,GAAA,IAAA;CAAlB;AAAU,KArCT,oCAAA,GAAuC,wBAqC9B;AA0BL,KA7DJ,8BAAA,GA8DF;gBA7DK;sBACM;;;;KAKT,6BAAA;gBACG;;;;;;;;;;yBAUS;;;;;;;;;;KAWZ,4BAAA;gBACG;;eAED;iBACE;UACP;YACE,kBAAkB;;;;;;iBA0Bb,sBAAA,UACN,gCACP"}
|
|
@@ -76,12 +76,14 @@ function useConversationPreview(options) {
|
|
|
76
76
|
if (human) return {
|
|
77
77
|
type: "human",
|
|
78
78
|
name: human.name,
|
|
79
|
-
image: human.image ?? null
|
|
79
|
+
image: human.image ?? null,
|
|
80
|
+
lastSeenAt: human.lastSeenAt ?? null
|
|
80
81
|
};
|
|
81
82
|
return {
|
|
82
83
|
type: "human",
|
|
83
84
|
name: supportFallbackName,
|
|
84
|
-
image: null
|
|
85
|
+
image: null,
|
|
86
|
+
lastSeenAt: null
|
|
85
87
|
};
|
|
86
88
|
}
|
|
87
89
|
if (lastAgentItem?.aiAgentId) {
|
|
@@ -101,7 +103,8 @@ function useConversationPreview(options) {
|
|
|
101
103
|
if (fallbackHuman) return {
|
|
102
104
|
type: "human",
|
|
103
105
|
name: fallbackHuman.name,
|
|
104
|
-
image: fallbackHuman.image ?? null
|
|
106
|
+
image: fallbackHuman.image ?? null,
|
|
107
|
+
lastSeenAt: fallbackHuman.lastSeenAt ?? null
|
|
105
108
|
};
|
|
106
109
|
const fallbackAi = availableAIAgents[0];
|
|
107
110
|
if (fallbackAi) return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversation-preview.js","names":["senderImage: string | null","typingState: ConversationPreviewTypingState"],"sources":["../../src/hooks/use-conversation-preview.ts"],"sourcesContent":["import type { Conversation } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useMemo } from \"react\";\n\nimport { useSupport } from \"../provider\";\nimport { useSupportText } from \"../support/text\";\nimport { formatTimeAgo } from \"../support/utils/time\";\nimport {\n\tmapTypingEntriesToPreviewParticipants,\n\ttype PreviewTypingParticipant,\n} from \"./private/typing\";\nimport { useConversationTimelineItems } from \"./use-conversation-timeline-items\";\nimport { useConversationTyping } from \"./use-conversation-typing\";\n\nexport type ConversationPreviewLastMessage = {\n\tcontent: string;\n\ttime: string;\n\tisFromVisitor: boolean;\n\tsenderName?: string;\n\tsenderImage?: string | null;\n};\n\nexport type ConversationPreviewAssignedAgent = {\n\tname: string;\n\timage: string | null;\n\ttype: \"human\" | \"ai\" | \"fallback\";\n};\n\nexport type ConversationPreviewTypingParticipant = PreviewTypingParticipant;\n\nexport type ConversationPreviewTypingState = {\n\tparticipants: ConversationPreviewTypingParticipant[];\n\tprimaryParticipant: ConversationPreviewTypingParticipant | null;\n\tlabel: string | null;\n\tisTyping: boolean;\n};\n\nexport type UseConversationPreviewOptions = {\n\tconversation: Conversation;\n\t/**\n\t * Whether the hook should fetch timeline items for the conversation.\n\t * Disabled by default to reduce API calls - conversation.lastTimelineItem\n\t * is typically sufficient for previews.\n\t */\n\tincludeTimelineItems?: boolean;\n\t/**\n\t * Optional timeline items to merge with the live ones (e.g. optimistic items).\n\t */\n\tinitialTimelineItems?: TimelineItem[];\n\t/**\n\t * Typing state configuration (mainly exclusions for the current visitor).\n\t */\n\ttyping?: {\n\t\texcludeVisitorId?: string | null;\n\t\texcludeUserId?: string | null;\n\t\texcludeAiAgentId?: string | null;\n\t};\n};\n\nexport type UseConversationPreviewReturn = {\n\tconversation: Conversation;\n\ttitle: string;\n\tlastMessage: ConversationPreviewLastMessage | null;\n\tassignedAgent: ConversationPreviewAssignedAgent;\n\ttyping: ConversationPreviewTypingState;\n\ttimeline: ReturnType<typeof useConversationTimelineItems>;\n};\n\nfunction resolveLastTimelineMessage(\n\titems: TimelineItem[],\n\tfallback: TimelineItem | null\n) {\n\tfor (let index = items.length - 1; index >= 0; index--) {\n\t\tconst item = items[index];\n\n\t\tif (item?.type === \"message\") {\n\t\t\treturn item;\n\t\t}\n\t}\n\n\tif (fallback?.type === \"message\") {\n\t\treturn fallback;\n\t}\n\n\treturn null;\n}\n\n/**\n * Composes conversation metadata including derived titles, last message\n * snippets and typing state for use in lists.\n */\nexport function useConversationPreview(\n\toptions: UseConversationPreviewOptions\n): UseConversationPreviewReturn {\n\tconst {\n\t\tconversation,\n\t\tincludeTimelineItems = false,\n\t\tinitialTimelineItems = [],\n\t\ttyping,\n\t} = options;\n\tconst { availableHumanAgents, availableAIAgents, visitor } = useSupport();\n\tconst text = useSupportText();\n\n\tconst timeline = useConversationTimelineItems(conversation.id, {\n\t\tenabled: includeTimelineItems,\n\t});\n\n\tconst mergedTimelineItems = useMemo(() => {\n\t\tif (timeline.items.length > 0) {\n\t\t\treturn timeline.items;\n\t\t}\n\n\t\tif (initialTimelineItems.length > 0) {\n\t\t\treturn initialTimelineItems;\n\t\t}\n\n\t\treturn [] as TimelineItem[];\n\t}, [timeline.items, initialTimelineItems]);\n\n\tconst knownTimelineItems = useMemo(() => {\n\t\tconst items = [...mergedTimelineItems];\n\n\t\tif (\n\t\t\tconversation.lastTimelineItem &&\n\t\t\t!items.some((item) => item.id === conversation.lastTimelineItem?.id)\n\t\t) {\n\t\t\titems.push(conversation.lastTimelineItem);\n\t\t}\n\n\t\treturn items;\n\t}, [mergedTimelineItems, conversation.lastTimelineItem]);\n\n\tconst lastTimelineMessage = useMemo(\n\t\t() =>\n\t\t\tresolveLastTimelineMessage(\n\t\t\t\tmergedTimelineItems,\n\t\t\t\tconversation.lastTimelineItem ?? null\n\t\t\t),\n\t\t[mergedTimelineItems, conversation.lastTimelineItem]\n\t);\n\n\tconst lastMessage = useMemo<ConversationPreviewLastMessage | null>(() => {\n\t\tif (!lastTimelineMessage) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst isFromVisitor = lastTimelineMessage.visitorId !== null;\n\n\t\tlet senderName = text(\"common.fallbacks.unknown\");\n\t\tlet senderImage: string | null = null;\n\n\t\tif (isFromVisitor) {\n\t\t\tsenderName = text(\"common.fallbacks.you\");\n\t\t} else if (lastTimelineMessage.userId) {\n\t\t\tconst agent = availableHumanAgents.find(\n\t\t\t\t(a) => a.id === lastTimelineMessage.userId\n\t\t\t);\n\t\t\tif (agent) {\n\t\t\t\tsenderName = agent.name;\n\t\t\t\tsenderImage = agent.image;\n\t\t\t} else {\n\t\t\t\tsenderName = text(\"common.fallbacks.supportTeam\");\n\t\t\t}\n\t\t} else if (lastTimelineMessage.aiAgentId) {\n\t\t\tconst aiAgent = availableAIAgents.find(\n\t\t\t\t(agent) => agent.id === lastTimelineMessage.aiAgentId\n\t\t\t);\n\t\t\tif (aiAgent) {\n\t\t\t\tsenderName = aiAgent.name;\n\t\t\t\tsenderImage = aiAgent.image;\n\t\t\t} else {\n\t\t\t\tsenderName = text(\"common.fallbacks.aiAssistant\");\n\t\t\t}\n\t\t} else {\n\t\t\tsenderName = text(\"common.fallbacks.supportTeam\");\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: lastTimelineMessage.text || \"\",\n\t\t\ttime: formatTimeAgo(lastTimelineMessage.createdAt),\n\t\t\tisFromVisitor,\n\t\t\tsenderName,\n\t\t\tsenderImage,\n\t\t};\n\t}, [lastTimelineMessage, availableHumanAgents, availableAIAgents, text]);\n\n\tconst assignedAgent = useMemo<ConversationPreviewAssignedAgent>(() => {\n\t\tconst supportFallbackName = text(\"common.fallbacks.supportTeam\");\n\t\tconst aiFallbackName = text(\"common.fallbacks.aiAssistant\");\n\n\t\tconst lastAgentItem = [...knownTimelineItems]\n\t\t\t.reverse()\n\t\t\t.find((item) => item.userId !== null || item.aiAgentId !== null);\n\n\t\tif (lastAgentItem?.userId) {\n\t\t\tconst human = availableHumanAgents.find(\n\t\t\t\t(agent) => agent.id === lastAgentItem.userId\n\t\t\t);\n\n\t\t\tif (human) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"human\" as const,\n\t\t\t\t\tname: human.name,\n\t\t\t\t\timage: human.image ?? null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: \"human\" as const,\n\t\t\t\tname: supportFallbackName,\n\t\t\t\timage: null,\n\t\t\t};\n\t\t}\n\n\t\tif (lastAgentItem?.aiAgentId) {\n\t\t\tconst ai = availableAIAgents.find(\n\t\t\t\t(agent) => agent.id === lastAgentItem.aiAgentId\n\t\t\t);\n\n\t\t\tif (ai) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"ai\" as const,\n\t\t\t\t\tname: ai.name,\n\t\t\t\t\timage: ai.image ?? null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: \"ai\" as const,\n\t\t\t\tname: aiFallbackName,\n\t\t\t\timage: null,\n\t\t\t};\n\t\t}\n\n\t\tconst fallbackHuman = availableHumanAgents[0];\n\t\tif (fallbackHuman) {\n\t\t\treturn {\n\t\t\t\ttype: \"human\" as const,\n\t\t\t\tname: fallbackHuman.name,\n\t\t\t\timage: fallbackHuman.image ?? null,\n\t\t\t};\n\t\t}\n\n\t\tconst fallbackAi = availableAIAgents[0];\n\t\tif (fallbackAi) {\n\t\t\treturn {\n\t\t\t\ttype: \"ai\" as const,\n\t\t\t\tname: fallbackAi.name,\n\t\t\t\timage: fallbackAi.image ?? null,\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"fallback\" as const,\n\t\t\tname: supportFallbackName,\n\t\t\timage: null,\n\t\t};\n\t}, [knownTimelineItems, availableHumanAgents, availableAIAgents, text]);\n\n\tconst typingEntries = useConversationTyping(conversation.id, {\n\t\texcludeVisitorId: typing?.excludeVisitorId ?? visitor?.id ?? null,\n\t\texcludeUserId: typing?.excludeUserId ?? null,\n\t\texcludeAiAgentId: typing?.excludeAiAgentId ?? null,\n\t});\n\n\tconst typingParticipants = useMemo(\n\t\t() =>\n\t\t\tmapTypingEntriesToPreviewParticipants(typingEntries, {\n\t\t\t\tavailableHumanAgents,\n\t\t\t\tavailableAIAgents,\n\t\t\t\ttext,\n\t\t\t}),\n\t\t[typingEntries, availableHumanAgents, availableAIAgents, text]\n\t);\n\n\tconst primaryTypingParticipant = typingParticipants[0] ?? null;\n\n\tconst typingLabel = useMemo(() => {\n\t\tif (!primaryTypingParticipant) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn text(\"component.conversationButtonLink.typing\", {\n\t\t\tname: primaryTypingParticipant.name,\n\t\t});\n\t}, [primaryTypingParticipant, text]);\n\n\tconst typingState: ConversationPreviewTypingState = useMemo(\n\t\t() => ({\n\t\t\tparticipants: typingParticipants,\n\t\t\tprimaryParticipant: primaryTypingParticipant,\n\t\t\tlabel: typingLabel,\n\t\t\tisTyping: typingParticipants.length > 0,\n\t\t}),\n\t\t[typingParticipants, primaryTypingParticipant, typingLabel]\n\t);\n\n\tconst title = useMemo(() => {\n\t\tif (conversation.title) {\n\t\t\treturn conversation.title;\n\t\t}\n\n\t\tif (lastMessage?.content) {\n\t\t\treturn lastMessage.content;\n\t\t}\n\n\t\treturn text(\"component.conversationButtonLink.fallbackTitle\");\n\t}, [conversation.title, lastMessage?.content, text]);\n\n\treturn {\n\t\tconversation,\n\t\ttitle,\n\t\tlastMessage,\n\t\tassignedAgent,\n\t\ttyping: typingState,\n\t\ttimeline,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-conversation-preview.js","names":["senderImage: string | null","typingState: ConversationPreviewTypingState"],"sources":["../../src/hooks/use-conversation-preview.ts"],"sourcesContent":["import type { Conversation } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useMemo } from \"react\";\n\nimport { useSupport } from \"../provider\";\nimport { useSupportText } from \"../support/text\";\nimport { formatTimeAgo } from \"../support/utils/time\";\nimport {\n\tmapTypingEntriesToPreviewParticipants,\n\ttype PreviewTypingParticipant,\n} from \"./private/typing\";\nimport { useConversationTimelineItems } from \"./use-conversation-timeline-items\";\nimport { useConversationTyping } from \"./use-conversation-typing\";\n\nexport type ConversationPreviewLastMessage = {\n\tcontent: string;\n\ttime: string;\n\tisFromVisitor: boolean;\n\tsenderName?: string;\n\tsenderImage?: string | null;\n};\n\nexport type ConversationPreviewAssignedAgent = {\n\tname: string;\n\timage: string | null;\n\ttype: \"human\" | \"ai\" | \"fallback\";\n\t/** Last seen timestamp for human agents, used for online status indicator */\n\tlastSeenAt?: string | null;\n};\n\nexport type ConversationPreviewTypingParticipant = PreviewTypingParticipant;\n\nexport type ConversationPreviewTypingState = {\n\tparticipants: ConversationPreviewTypingParticipant[];\n\tprimaryParticipant: ConversationPreviewTypingParticipant | null;\n\tlabel: string | null;\n\tisTyping: boolean;\n};\n\nexport type UseConversationPreviewOptions = {\n\tconversation: Conversation;\n\t/**\n\t * Whether the hook should fetch timeline items for the conversation.\n\t * Disabled by default to reduce API calls - conversation.lastTimelineItem\n\t * is typically sufficient for previews.\n\t */\n\tincludeTimelineItems?: boolean;\n\t/**\n\t * Optional timeline items to merge with the live ones (e.g. optimistic items).\n\t */\n\tinitialTimelineItems?: TimelineItem[];\n\t/**\n\t * Typing state configuration (mainly exclusions for the current visitor).\n\t */\n\ttyping?: {\n\t\texcludeVisitorId?: string | null;\n\t\texcludeUserId?: string | null;\n\t\texcludeAiAgentId?: string | null;\n\t};\n};\n\nexport type UseConversationPreviewReturn = {\n\tconversation: Conversation;\n\ttitle: string;\n\tlastMessage: ConversationPreviewLastMessage | null;\n\tassignedAgent: ConversationPreviewAssignedAgent;\n\ttyping: ConversationPreviewTypingState;\n\ttimeline: ReturnType<typeof useConversationTimelineItems>;\n};\n\nfunction resolveLastTimelineMessage(\n\titems: TimelineItem[],\n\tfallback: TimelineItem | null\n) {\n\tfor (let index = items.length - 1; index >= 0; index--) {\n\t\tconst item = items[index];\n\n\t\tif (item?.type === \"message\") {\n\t\t\treturn item;\n\t\t}\n\t}\n\n\tif (fallback?.type === \"message\") {\n\t\treturn fallback;\n\t}\n\n\treturn null;\n}\n\n/**\n * Composes conversation metadata including derived titles, last message\n * snippets and typing state for use in lists.\n */\nexport function useConversationPreview(\n\toptions: UseConversationPreviewOptions\n): UseConversationPreviewReturn {\n\tconst {\n\t\tconversation,\n\t\tincludeTimelineItems = false,\n\t\tinitialTimelineItems = [],\n\t\ttyping,\n\t} = options;\n\tconst { availableHumanAgents, availableAIAgents, visitor } = useSupport();\n\tconst text = useSupportText();\n\n\tconst timeline = useConversationTimelineItems(conversation.id, {\n\t\tenabled: includeTimelineItems,\n\t});\n\n\tconst mergedTimelineItems = useMemo(() => {\n\t\tif (timeline.items.length > 0) {\n\t\t\treturn timeline.items;\n\t\t}\n\n\t\tif (initialTimelineItems.length > 0) {\n\t\t\treturn initialTimelineItems;\n\t\t}\n\n\t\treturn [] as TimelineItem[];\n\t}, [timeline.items, initialTimelineItems]);\n\n\tconst knownTimelineItems = useMemo(() => {\n\t\tconst items = [...mergedTimelineItems];\n\n\t\tif (\n\t\t\tconversation.lastTimelineItem &&\n\t\t\t!items.some((item) => item.id === conversation.lastTimelineItem?.id)\n\t\t) {\n\t\t\titems.push(conversation.lastTimelineItem);\n\t\t}\n\n\t\treturn items;\n\t}, [mergedTimelineItems, conversation.lastTimelineItem]);\n\n\tconst lastTimelineMessage = useMemo(\n\t\t() =>\n\t\t\tresolveLastTimelineMessage(\n\t\t\t\tmergedTimelineItems,\n\t\t\t\tconversation.lastTimelineItem ?? null\n\t\t\t),\n\t\t[mergedTimelineItems, conversation.lastTimelineItem]\n\t);\n\n\tconst lastMessage = useMemo<ConversationPreviewLastMessage | null>(() => {\n\t\tif (!lastTimelineMessage) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst isFromVisitor = lastTimelineMessage.visitorId !== null;\n\n\t\tlet senderName = text(\"common.fallbacks.unknown\");\n\t\tlet senderImage: string | null = null;\n\n\t\tif (isFromVisitor) {\n\t\t\tsenderName = text(\"common.fallbacks.you\");\n\t\t} else if (lastTimelineMessage.userId) {\n\t\t\tconst agent = availableHumanAgents.find(\n\t\t\t\t(a) => a.id === lastTimelineMessage.userId\n\t\t\t);\n\t\t\tif (agent) {\n\t\t\t\tsenderName = agent.name;\n\t\t\t\tsenderImage = agent.image;\n\t\t\t} else {\n\t\t\t\tsenderName = text(\"common.fallbacks.supportTeam\");\n\t\t\t}\n\t\t} else if (lastTimelineMessage.aiAgentId) {\n\t\t\tconst aiAgent = availableAIAgents.find(\n\t\t\t\t(agent) => agent.id === lastTimelineMessage.aiAgentId\n\t\t\t);\n\t\t\tif (aiAgent) {\n\t\t\t\tsenderName = aiAgent.name;\n\t\t\t\tsenderImage = aiAgent.image;\n\t\t\t} else {\n\t\t\t\tsenderName = text(\"common.fallbacks.aiAssistant\");\n\t\t\t}\n\t\t} else {\n\t\t\tsenderName = text(\"common.fallbacks.supportTeam\");\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: lastTimelineMessage.text || \"\",\n\t\t\ttime: formatTimeAgo(lastTimelineMessage.createdAt),\n\t\t\tisFromVisitor,\n\t\t\tsenderName,\n\t\t\tsenderImage,\n\t\t};\n\t}, [lastTimelineMessage, availableHumanAgents, availableAIAgents, text]);\n\n\tconst assignedAgent = useMemo<ConversationPreviewAssignedAgent>(() => {\n\t\tconst supportFallbackName = text(\"common.fallbacks.supportTeam\");\n\t\tconst aiFallbackName = text(\"common.fallbacks.aiAssistant\");\n\n\t\tconst lastAgentItem = [...knownTimelineItems]\n\t\t\t.reverse()\n\t\t\t.find((item) => item.userId !== null || item.aiAgentId !== null);\n\n\t\tif (lastAgentItem?.userId) {\n\t\t\tconst human = availableHumanAgents.find(\n\t\t\t\t(agent) => agent.id === lastAgentItem.userId\n\t\t\t);\n\n\t\t\tif (human) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"human\" as const,\n\t\t\t\t\tname: human.name,\n\t\t\t\t\timage: human.image ?? null,\n\t\t\t\t\tlastSeenAt: human.lastSeenAt ?? null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: \"human\" as const,\n\t\t\t\tname: supportFallbackName,\n\t\t\t\timage: null,\n\t\t\t\tlastSeenAt: null,\n\t\t\t};\n\t\t}\n\n\t\tif (lastAgentItem?.aiAgentId) {\n\t\t\tconst ai = availableAIAgents.find(\n\t\t\t\t(agent) => agent.id === lastAgentItem.aiAgentId\n\t\t\t);\n\n\t\t\tif (ai) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"ai\" as const,\n\t\t\t\t\tname: ai.name,\n\t\t\t\t\timage: ai.image ?? null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: \"ai\" as const,\n\t\t\t\tname: aiFallbackName,\n\t\t\t\timage: null,\n\t\t\t};\n\t\t}\n\n\t\tconst fallbackHuman = availableHumanAgents[0];\n\t\tif (fallbackHuman) {\n\t\t\treturn {\n\t\t\t\ttype: \"human\" as const,\n\t\t\t\tname: fallbackHuman.name,\n\t\t\t\timage: fallbackHuman.image ?? null,\n\t\t\t\tlastSeenAt: fallbackHuman.lastSeenAt ?? null,\n\t\t\t};\n\t\t}\n\n\t\tconst fallbackAi = availableAIAgents[0];\n\t\tif (fallbackAi) {\n\t\t\treturn {\n\t\t\t\ttype: \"ai\" as const,\n\t\t\t\tname: fallbackAi.name,\n\t\t\t\timage: fallbackAi.image ?? null,\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"fallback\" as const,\n\t\t\tname: supportFallbackName,\n\t\t\timage: null,\n\t\t};\n\t}, [knownTimelineItems, availableHumanAgents, availableAIAgents, text]);\n\n\tconst typingEntries = useConversationTyping(conversation.id, {\n\t\texcludeVisitorId: typing?.excludeVisitorId ?? visitor?.id ?? null,\n\t\texcludeUserId: typing?.excludeUserId ?? null,\n\t\texcludeAiAgentId: typing?.excludeAiAgentId ?? null,\n\t});\n\n\tconst typingParticipants = useMemo(\n\t\t() =>\n\t\t\tmapTypingEntriesToPreviewParticipants(typingEntries, {\n\t\t\t\tavailableHumanAgents,\n\t\t\t\tavailableAIAgents,\n\t\t\t\ttext,\n\t\t\t}),\n\t\t[typingEntries, availableHumanAgents, availableAIAgents, text]\n\t);\n\n\tconst primaryTypingParticipant = typingParticipants[0] ?? null;\n\n\tconst typingLabel = useMemo(() => {\n\t\tif (!primaryTypingParticipant) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn text(\"component.conversationButtonLink.typing\", {\n\t\t\tname: primaryTypingParticipant.name,\n\t\t});\n\t}, [primaryTypingParticipant, text]);\n\n\tconst typingState: ConversationPreviewTypingState = useMemo(\n\t\t() => ({\n\t\t\tparticipants: typingParticipants,\n\t\t\tprimaryParticipant: primaryTypingParticipant,\n\t\t\tlabel: typingLabel,\n\t\t\tisTyping: typingParticipants.length > 0,\n\t\t}),\n\t\t[typingParticipants, primaryTypingParticipant, typingLabel]\n\t);\n\n\tconst title = useMemo(() => {\n\t\tif (conversation.title) {\n\t\t\treturn conversation.title;\n\t\t}\n\n\t\tif (lastMessage?.content) {\n\t\t\treturn lastMessage.content;\n\t\t}\n\n\t\treturn text(\"component.conversationButtonLink.fallbackTitle\");\n\t}, [conversation.title, lastMessage?.content, text]);\n\n\treturn {\n\t\tconversation,\n\t\ttitle,\n\t\tlastMessage,\n\t\tassignedAgent,\n\t\ttyping: typingState,\n\t\ttimeline,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GetConversationTimelineItemsRequest, GetConversationTimelineItemsResponse } from "../timeline-item.js";
|
|
1
|
+
import { GetConversationTimelineItemsRequest, GetConversationTimelineItemsResponse } from "../packages/types/src/api/timeline-item.js";
|
|
2
2
|
import { ConversationTimelineItemsState } from "@cossistant/core";
|
|
3
3
|
|
|
4
4
|
//#region src/hooks/use-conversation-timeline-items.d.ts
|
|
@@ -17,10 +17,9 @@ const NO_CONVERSATION_ID = "__no_conversation__";
|
|
|
17
17
|
*/
|
|
18
18
|
function useConversationTimelineItems(conversationId, options = {}) {
|
|
19
19
|
const { client } = useSupport();
|
|
20
|
-
const store = client
|
|
21
|
-
if (!store) throw new Error("Timeline items store is not available on the client instance");
|
|
20
|
+
const store = client?.timelineItemsStore ?? null;
|
|
22
21
|
const stableConversationId = conversationId ?? NO_CONVERSATION_ID;
|
|
23
|
-
const selection = useStoreSelector(store, (state) => state.conversations[stableConversationId] ?? EMPTY_STATE);
|
|
22
|
+
const selection = useStoreSelector(store, (state) => state ? state.conversations[stableConversationId] ?? EMPTY_STATE : EMPTY_STATE);
|
|
24
23
|
const baseArgs = useMemo(() => ({
|
|
25
24
|
limit: options.limit ?? DEFAULT_LIMIT,
|
|
26
25
|
cursor: options.cursor ?? void 0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversation-timeline-items.js","names":["EMPTY_STATE: ConversationTimelineItemsState"],"sources":["../../src/hooks/use-conversation-timeline-items.ts"],"sourcesContent":["import type { ConversationTimelineItemsState } from \"@cossistant/core\";\nimport type {\n\tGetConversationTimelineItemsRequest,\n\tGetConversationTimelineItemsResponse,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useMemo } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\nconst EMPTY_STATE: ConversationTimelineItemsState = {\n\titems: [],\n\thasNextPage: false,\n\tnextCursor: undefined,\n};\n\nconst DEFAULT_LIMIT = 50;\n\nconst NO_CONVERSATION_ID = \"__no_conversation__\" as const;\n\nexport type UseConversationTimelineItemsOptions = {\n\tlimit?: number;\n\tcursor?: string | null;\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationTimelineItemsResult =\n\tConversationTimelineItemsState & {\n\t\tisLoading: boolean;\n\t\terror: Error | null;\n\t\trefetch: (\n\t\t\targs?: Pick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">\n\t\t) => Promise<GetConversationTimelineItemsResponse | undefined>;\n\t\tfetchNextPage: () => Promise<\n\t\t\tGetConversationTimelineItemsResponse | undefined\n\t\t>;\n\t};\n\n/**\n * Fetches timeline items for a conversation and keeps the local store in sync\n * with pagination helpers.\n */\nexport function useConversationTimelineItems(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationTimelineItemsOptions = {}\n): UseConversationTimelineItemsResult {\n\tconst { client } = useSupport();\n\tconst store = client
|
|
1
|
+
{"version":3,"file":"use-conversation-timeline-items.js","names":["EMPTY_STATE: ConversationTimelineItemsState"],"sources":["../../src/hooks/use-conversation-timeline-items.ts"],"sourcesContent":["import type { ConversationTimelineItemsState } from \"@cossistant/core\";\nimport type {\n\tGetConversationTimelineItemsRequest,\n\tGetConversationTimelineItemsResponse,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useMemo } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\nconst EMPTY_STATE: ConversationTimelineItemsState = {\n\titems: [],\n\thasNextPage: false,\n\tnextCursor: undefined,\n};\n\nconst DEFAULT_LIMIT = 50;\n\nconst NO_CONVERSATION_ID = \"__no_conversation__\" as const;\n\nexport type UseConversationTimelineItemsOptions = {\n\tlimit?: number;\n\tcursor?: string | null;\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationTimelineItemsResult =\n\tConversationTimelineItemsState & {\n\t\tisLoading: boolean;\n\t\terror: Error | null;\n\t\trefetch: (\n\t\t\targs?: Pick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">\n\t\t) => Promise<GetConversationTimelineItemsResponse | undefined>;\n\t\tfetchNextPage: () => Promise<\n\t\t\tGetConversationTimelineItemsResponse | undefined\n\t\t>;\n\t};\n\n/**\n * Fetches timeline items for a conversation and keeps the local store in sync\n * with pagination helpers.\n */\nexport function useConversationTimelineItems(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationTimelineItemsOptions = {}\n): UseConversationTimelineItemsResult {\n\tconst { client } = useSupport();\n\tconst store = client?.timelineItemsStore ?? null;\n\n\tconst stableConversationId = conversationId ?? NO_CONVERSATION_ID;\n\n\tconst selection = useStoreSelector(store, (state) =>\n\t\tstate\n\t\t\t? (state.conversations[stableConversationId] ?? EMPTY_STATE)\n\t\t\t: EMPTY_STATE\n\t);\n\n\tconst baseArgs = useMemo(\n\t\t() =>\n\t\t\t({\n\t\t\t\tlimit: options.limit ?? DEFAULT_LIMIT,\n\t\t\t\tcursor: options.cursor ?? undefined,\n\t\t\t}) satisfies Pick<\n\t\t\t\tGetConversationTimelineItemsRequest,\n\t\t\t\t\"limit\" | \"cursor\"\n\t\t\t>,\n\t\t[options.cursor, options.limit]\n\t);\n\n\tconst {\n\t\trefetch: queryRefetch,\n\t\tisLoading: queryLoading,\n\t\terror,\n\t} = useClientQuery<\n\t\tGetConversationTimelineItemsResponse,\n\t\tPick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">\n\t>({\n\t\tclient,\n\t\tqueryKey: conversationId\n\t\t\t? `timeline:${conversationId}:${baseArgs.limit}:${baseArgs.cursor ?? \"\"}`\n\t\t\t: undefined,\n\t\tqueryFn: (instance, args) => {\n\t\t\tif (!conversationId) {\n\t\t\t\treturn Promise.resolve({\n\t\t\t\t\titems: [],\n\t\t\t\t\thasNextPage: false,\n\t\t\t\t\tnextCursor: null,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn instance.getConversationTimelineItems({\n\t\t\t\tconversationId,\n\t\t\t\tlimit: args?.limit ?? baseArgs.limit,\n\t\t\t\tcursor: args?.cursor ?? baseArgs.cursor ?? undefined,\n\t\t\t});\n\t\t},\n\t\tenabled: Boolean(conversationId) && (options.enabled ?? true),\n\t\trefetchInterval: options.refetchInterval ?? false,\n\t\trefetchOnWindowFocus: options.refetchOnWindowFocus ?? false,\n\t\trefetchOnMount: selection.items.length === 0,\n\t\tinitialArgs: baseArgs,\n\t\tdependencies: [\n\t\t\tstableConversationId,\n\t\t\tbaseArgs.limit,\n\t\t\tbaseArgs.cursor ?? null,\n\t\t],\n\t});\n\n\tconst refetch = useCallback(\n\t\t(args?: Pick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">) => {\n\t\t\tif (!conversationId) {\n\t\t\t\treturn Promise.resolve(undefined);\n\t\t\t}\n\n\t\t\treturn queryRefetch({\n\t\t\t\tlimit: baseArgs.limit,\n\t\t\t\tcursor: baseArgs.cursor,\n\t\t\t\t...args,\n\t\t\t});\n\t\t},\n\t\t[queryRefetch, baseArgs, conversationId]\n\t);\n\n\tconst fetchNextPage = useCallback(() => {\n\t\tif (!(selection.hasNextPage && selection.nextCursor)) {\n\t\t\treturn Promise.resolve(undefined);\n\t\t}\n\n\t\treturn refetch({ cursor: selection.nextCursor, limit: baseArgs.limit });\n\t}, [selection.hasNextPage, selection.nextCursor, refetch, baseArgs.limit]);\n\n\tconst isInitialLoad = selection.items.length === 0;\n\tconst isLoading = isInitialLoad ? queryLoading : false;\n\n\treturn {\n\t\titems: selection.items,\n\t\thasNextPage: selection.hasNextPage,\n\t\tnextCursor: selection.nextCursor,\n\t\tisLoading,\n\t\terror,\n\t\trefetch,\n\t\tfetchNextPage,\n\t};\n}\n"],"mappings":";;;;;;AAUA,MAAMA,cAA8C;CACnD,OAAO,EAAE;CACT,aAAa;CACb,YAAY;CACZ;AAED,MAAM,gBAAgB;AAEtB,MAAM,qBAAqB;;;;;AA0B3B,SAAgB,6BACf,gBACA,UAA+C,EAAE,EACZ;CACrC,MAAM,EAAE,WAAW,YAAY;CAC/B,MAAM,QAAQ,QAAQ,sBAAsB;CAE5C,MAAM,uBAAuB,kBAAkB;CAE/C,MAAM,YAAY,iBAAiB,QAAQ,UAC1C,QACI,MAAM,cAAc,yBAAyB,cAC9C,YACH;CAED,MAAM,WAAW,eAEd;EACA,OAAO,QAAQ,SAAS;EACxB,QAAQ,QAAQ,UAAU;EAC1B,GAIF,CAAC,QAAQ,QAAQ,QAAQ,MAAM,CAC/B;CAED,MAAM,EACL,SAAS,cACT,WAAW,cACX,UACG,eAGF;EACD;EACA,UAAU,iBACP,YAAY,eAAe,GAAG,SAAS,MAAM,GAAG,SAAS,UAAU,OACnE;EACH,UAAU,UAAU,SAAS;AAC5B,OAAI,CAAC,eACJ,QAAO,QAAQ,QAAQ;IACtB,OAAO,EAAE;IACT,aAAa;IACb,YAAY;IACZ,CAAC;AAGH,UAAO,SAAS,6BAA6B;IAC5C;IACA,OAAO,MAAM,SAAS,SAAS;IAC/B,QAAQ,MAAM,UAAU,SAAS,UAAU;IAC3C,CAAC;;EAEH,SAAS,QAAQ,eAAe,KAAK,QAAQ,WAAW;EACxD,iBAAiB,QAAQ,mBAAmB;EAC5C,sBAAsB,QAAQ,wBAAwB;EACtD,gBAAgB,UAAU,MAAM,WAAW;EAC3C,aAAa;EACb,cAAc;GACb;GACA,SAAS;GACT,SAAS,UAAU;GACnB;EACD,CAAC;CAEF,MAAM,UAAU,aACd,SAAyE;AACzE,MAAI,CAAC,eACJ,QAAO,QAAQ,QAAQ,OAAU;AAGlC,SAAO,aAAa;GACnB,OAAO,SAAS;GAChB,QAAQ,SAAS;GACjB,GAAG;GACH,CAAC;IAEH;EAAC;EAAc;EAAU;EAAe,CACxC;CAED,MAAM,gBAAgB,kBAAkB;AACvC,MAAI,EAAE,UAAU,eAAe,UAAU,YACxC,QAAO,QAAQ,QAAQ,OAAU;AAGlC,SAAO,QAAQ;GAAE,QAAQ,UAAU;GAAY,OAAO,SAAS;GAAO,CAAC;IACrE;EAAC,UAAU;EAAa,UAAU;EAAY;EAAS,SAAS;EAAM,CAAC;CAG1E,MAAM,YADgB,UAAU,MAAM,WAAW,IACf,eAAe;AAEjD,QAAO;EACN,OAAO,UAAU;EACjB,aAAa,UAAU;EACvB,YAAY,UAAU;EACtB;EACA;EACA;EACA;EACA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TimelineItem } from "../timeline-item.js";
|
|
1
|
+
import { TimelineItem } from "../packages/types/src/api/timeline-item.js";
|
|
2
2
|
import { useGroupedMessages } from "./private/use-grouped-messages.js";
|
|
3
3
|
import { TimelineTypingParticipant } from "./private/typing.js";
|
|
4
4
|
import { useDebouncedConversationSeen } from "./use-conversation-seen.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GetConversationRequest, GetConversationResponse } from "../conversation.js";
|
|
1
|
+
import { GetConversationRequest, GetConversationResponse } from "../packages/types/src/api/conversation.js";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-conversation.d.ts
|
|
4
4
|
type UseConversationOptions = {
|
|
@@ -16,9 +16,8 @@ import { useSupport } from "../provider.js";
|
|
|
16
16
|
*/
|
|
17
17
|
function useConversation(conversationId, options = {}) {
|
|
18
18
|
const { client } = useSupport();
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
if (!conversationId) return null;
|
|
19
|
+
const conversation = useStoreSelector(client?.conversationsStore ?? null, (state) => {
|
|
20
|
+
if (!(state && conversationId)) return null;
|
|
22
21
|
return state.byId[conversationId] ?? null;
|
|
23
22
|
});
|
|
24
23
|
const request = conversationId ? { conversationId } : void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversation.js","names":["request: GetConversationRequest | undefined"],"sources":["../../src/hooks/use-conversation.ts"],"sourcesContent":["import type {\n\tGetConversationRequest,\n\tGetConversationResponse,\n} from \"@cossistant/types/api/conversation\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\nexport type UseConversationOptions = {\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationResult = {\n\tconversation: GetConversationResponse[\"conversation\"] | null;\n\tisLoading: boolean;\n\terror: Error | null;\n\trefetch: (\n\t\targs?: GetConversationRequest\n\t) => Promise<GetConversationResponse | undefined>;\n};\n\n/**\n * Loads and caches a single conversation identified by `conversationId`.\n *\n * The hook keeps the conversations store hydrated, exposes a derived loading\n * state that respects cached data and provides a `refetch` helper to manually\n * refresh the thread.\n *\n * @param conversationId The conversation to retrieve; when `null` the hook\n * skips requests and returns `null` data.\n * @param options Additional react-query style controls for the request.\n */\nexport function useConversation(\n\tconversationId: string | null,\n\toptions: UseConversationOptions = {}\n): UseConversationResult {\n\tconst { client } = useSupport();\n\tconst store = client
|
|
1
|
+
{"version":3,"file":"use-conversation.js","names":["request: GetConversationRequest | undefined"],"sources":["../../src/hooks/use-conversation.ts"],"sourcesContent":["import type {\n\tGetConversationRequest,\n\tGetConversationResponse,\n} from \"@cossistant/types/api/conversation\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\nexport type UseConversationOptions = {\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationResult = {\n\tconversation: GetConversationResponse[\"conversation\"] | null;\n\tisLoading: boolean;\n\terror: Error | null;\n\trefetch: (\n\t\targs?: GetConversationRequest\n\t) => Promise<GetConversationResponse | undefined>;\n};\n\n/**\n * Loads and caches a single conversation identified by `conversationId`.\n *\n * The hook keeps the conversations store hydrated, exposes a derived loading\n * state that respects cached data and provides a `refetch` helper to manually\n * refresh the thread.\n *\n * @param conversationId The conversation to retrieve; when `null` the hook\n * skips requests and returns `null` data.\n * @param options Additional react-query style controls for the request.\n */\nexport function useConversation(\n\tconversationId: string | null,\n\toptions: UseConversationOptions = {}\n): UseConversationResult {\n\tconst { client } = useSupport();\n\tconst store = client?.conversationsStore ?? null;\n\n\tconst conversation = useStoreSelector(store, (state) => {\n\t\tif (!(state && conversationId)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn state.byId[conversationId] ?? null;\n\t});\n\n\tconst request: GetConversationRequest | undefined = conversationId\n\t\t? { conversationId }\n\t\t: undefined;\n\n\tconst {\n\t\trefetch: queryRefetch,\n\t\tisLoading: queryLoading,\n\t\terror,\n\t} = useClientQuery<GetConversationResponse, GetConversationRequest>({\n\t\tclient,\n\t\tqueryKey: conversationId ? `conversation:${conversationId}` : undefined,\n\t\tqueryFn: (instance) => {\n\t\t\tif (!request) {\n\t\t\t\tthrow new Error(\"Conversation ID is required\");\n\t\t\t}\n\t\t\treturn instance.getConversation(request);\n\t\t},\n\t\tenabled: Boolean(conversationId && (options.enabled ?? true)),\n\t\trefetchInterval: options.refetchInterval ?? false,\n\t\trefetchOnWindowFocus: options.refetchOnWindowFocus ?? false,\n\t\trefetchOnMount: !conversation,\n\t\tinitialArgs: request,\n\t\tdependencies: [conversationId ?? \"null\"],\n\t});\n\n\tconst refetch = (args?: GetConversationRequest) => {\n\t\tif (!conversationId) {\n\t\t\treturn Promise.resolve(undefined);\n\t\t}\n\n\t\treturn queryRefetch({\n\t\t\tconversationId,\n\t\t\t...args,\n\t\t});\n\t};\n\n\tconst isLoading = conversation ? false : queryLoading;\n\n\treturn {\n\t\tconversation,\n\t\tisLoading,\n\t\terror,\n\t\trefetch,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkCA,SAAgB,gBACf,gBACA,UAAkC,EAAE,EACZ;CACxB,MAAM,EAAE,WAAW,YAAY;CAG/B,MAAM,eAAe,iBAFP,QAAQ,sBAAsB,OAEE,UAAU;AACvD,MAAI,EAAE,SAAS,gBACd,QAAO;AAER,SAAO,MAAM,KAAK,mBAAmB;GACpC;CAEF,MAAMA,UAA8C,iBACjD,EAAE,gBAAgB,GAClB;CAEH,MAAM,EACL,SAAS,cACT,WAAW,cACX,UACG,eAAgE;EACnE;EACA,UAAU,iBAAiB,gBAAgB,mBAAmB;EAC9D,UAAU,aAAa;AACtB,OAAI,CAAC,QACJ,OAAM,IAAI,MAAM,8BAA8B;AAE/C,UAAO,SAAS,gBAAgB,QAAQ;;EAEzC,SAAS,QAAQ,mBAAmB,QAAQ,WAAW,MAAM;EAC7D,iBAAiB,QAAQ,mBAAmB;EAC5C,sBAAsB,QAAQ,wBAAwB;EACtD,gBAAgB,CAAC;EACjB,aAAa;EACb,cAAc,CAAC,kBAAkB,OAAO;EACxC,CAAC;CAEF,MAAM,WAAW,SAAkC;AAClD,MAAI,CAAC,eACJ,QAAO,QAAQ,QAAQ,OAAU;AAGlC,SAAO,aAAa;GACnB;GACA,GAAG;GACH,CAAC;;AAKH,QAAO;EACN;EACA,WAJiB,eAAe,QAAQ;EAKxC;EACA;EACA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ListConversationsRequest, ListConversationsResponse } from "../conversation.js";
|
|
1
|
+
import { ListConversationsRequest, ListConversationsResponse } from "../packages/types/src/api/conversation.js";
|
|
2
2
|
import { ConversationPagination } from "@cossistant/core";
|
|
3
3
|
|
|
4
4
|
//#region src/hooks/use-conversations.d.ts
|
|
@@ -37,11 +37,13 @@ function useConversations(options = {}) {
|
|
|
37
37
|
orderBy,
|
|
38
38
|
order
|
|
39
39
|
]);
|
|
40
|
-
const
|
|
41
|
-
const selection = useStoreSelector(store, (state) => ({
|
|
40
|
+
const selection = useStoreSelector(client?.conversationsStore ?? null, (state) => state ? {
|
|
42
41
|
conversations: state.ids.map((id) => state.byId[id]).filter((conversation) => Boolean(conversation)),
|
|
43
42
|
pagination: state.pagination
|
|
44
|
-
}
|
|
43
|
+
} : {
|
|
44
|
+
conversations: [],
|
|
45
|
+
pagination: null
|
|
46
|
+
}, areSelectionsEqual);
|
|
45
47
|
const { refetch: queryRefetch, isLoading: queryLoading, error } = useClientQuery({
|
|
46
48
|
client,
|
|
47
49
|
queryKey: `conversations:${limit ?? ""}:${page ?? ""}:${status ?? ""}:${orderBy ?? ""}:${order ?? ""}`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversations.js","names":[],"sources":["../../src/hooks/use-conversations.ts"],"sourcesContent":["import type { ConversationPagination } from \"@cossistant/core\";\nimport type {\n\tListConversationsRequest,\n\tListConversationsResponse,\n} from \"@cossistant/types/api/conversation\";\nimport { useCallback, useMemo } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\ntype ConversationsSelection = {\n\tconversations: ListConversationsResponse[\"conversations\"];\n\tpagination: ConversationPagination | null;\n};\n\nfunction areSelectionsEqual(\n\ta: ConversationsSelection,\n\tb: ConversationsSelection\n): boolean {\n\tconst samePagination =\n\t\ta.pagination === b.pagination ||\n\t\t(Boolean(a.pagination) &&\n\t\t\tBoolean(b.pagination) &&\n\t\t\ta.pagination?.page === b.pagination?.page &&\n\t\t\ta.pagination?.limit === b.pagination?.limit &&\n\t\t\ta.pagination?.total === b.pagination?.total &&\n\t\t\ta.pagination?.totalPages === b.pagination?.totalPages &&\n\t\t\ta.pagination?.hasMore === b.pagination?.hasMore);\n\n\tif (!samePagination) {\n\t\treturn false;\n\t}\n\n\tif (a.conversations.length !== b.conversations.length) {\n\t\treturn false;\n\t}\n\n\treturn a.conversations.every(\n\t\t(conversation, index) => conversation === b.conversations[index]\n\t);\n}\n\nexport type UseConversationsOptions = Partial<\n\tOmit<ListConversationsRequest, \"visitorId\">\n> & {\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationsResult = {\n\tconversations: ListConversationsResponse[\"conversations\"];\n\tpagination: ConversationPagination | null;\n\tisLoading: boolean;\n\terror: Error | null;\n\trefetch: (\n\t\targs?: Partial<ListConversationsRequest>\n\t) => Promise<ListConversationsResponse | undefined>;\n};\n\n/**\n * Fetches and subscribes to the authenticated visitor's conversation list.\n *\n * The hook keeps the store in sync with the REST client and exposes\n * pagination metadata plus a refetch helper for manual refreshes. The\n * `options` mirror the public API filters so the UI can request slices of the\n * inbox without duplicating data-fetching logic.\n *\n * @param options Filtering and lifecycle controls for the query.\n * @returns Conversations, pagination data, loading state, and a refetch\n * helper.\n */\nexport function useConversations(\n\toptions: UseConversationsOptions = {}\n): UseConversationsResult {\n\tconst { client } = useSupport();\n\n\tconst {\n\t\tlimit,\n\t\tpage,\n\t\torder,\n\t\torderBy,\n\t\tstatus,\n\t\tenabled = true,\n\t\trefetchInterval = false,\n\t\trefetchOnWindowFocus = true,\n\t} = options;\n\n\tconst requestDefaults = useMemo(\n\t\t() => ({ limit, page, status, orderBy, order }),\n\t\t[limit, page, status, orderBy, order]\n\t);\n\n\tconst store = client
|
|
1
|
+
{"version":3,"file":"use-conversations.js","names":[],"sources":["../../src/hooks/use-conversations.ts"],"sourcesContent":["import type { ConversationPagination } from \"@cossistant/core\";\nimport type {\n\tListConversationsRequest,\n\tListConversationsResponse,\n} from \"@cossistant/types/api/conversation\";\nimport { useCallback, useMemo } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\ntype ConversationsSelection = {\n\tconversations: ListConversationsResponse[\"conversations\"];\n\tpagination: ConversationPagination | null;\n};\n\nfunction areSelectionsEqual(\n\ta: ConversationsSelection,\n\tb: ConversationsSelection\n): boolean {\n\tconst samePagination =\n\t\ta.pagination === b.pagination ||\n\t\t(Boolean(a.pagination) &&\n\t\t\tBoolean(b.pagination) &&\n\t\t\ta.pagination?.page === b.pagination?.page &&\n\t\t\ta.pagination?.limit === b.pagination?.limit &&\n\t\t\ta.pagination?.total === b.pagination?.total &&\n\t\t\ta.pagination?.totalPages === b.pagination?.totalPages &&\n\t\t\ta.pagination?.hasMore === b.pagination?.hasMore);\n\n\tif (!samePagination) {\n\t\treturn false;\n\t}\n\n\tif (a.conversations.length !== b.conversations.length) {\n\t\treturn false;\n\t}\n\n\treturn a.conversations.every(\n\t\t(conversation, index) => conversation === b.conversations[index]\n\t);\n}\n\nexport type UseConversationsOptions = Partial<\n\tOmit<ListConversationsRequest, \"visitorId\">\n> & {\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationsResult = {\n\tconversations: ListConversationsResponse[\"conversations\"];\n\tpagination: ConversationPagination | null;\n\tisLoading: boolean;\n\terror: Error | null;\n\trefetch: (\n\t\targs?: Partial<ListConversationsRequest>\n\t) => Promise<ListConversationsResponse | undefined>;\n};\n\n/**\n * Fetches and subscribes to the authenticated visitor's conversation list.\n *\n * The hook keeps the store in sync with the REST client and exposes\n * pagination metadata plus a refetch helper for manual refreshes. The\n * `options` mirror the public API filters so the UI can request slices of the\n * inbox without duplicating data-fetching logic.\n *\n * @param options Filtering and lifecycle controls for the query.\n * @returns Conversations, pagination data, loading state, and a refetch\n * helper.\n */\nexport function useConversations(\n\toptions: UseConversationsOptions = {}\n): UseConversationsResult {\n\tconst { client } = useSupport();\n\n\tconst {\n\t\tlimit,\n\t\tpage,\n\t\torder,\n\t\torderBy,\n\t\tstatus,\n\t\tenabled = true,\n\t\trefetchInterval = false,\n\t\trefetchOnWindowFocus = true,\n\t} = options;\n\n\tconst requestDefaults = useMemo(\n\t\t() => ({ limit, page, status, orderBy, order }),\n\t\t[limit, page, status, orderBy, order]\n\t);\n\n\tconst store = client?.conversationsStore ?? null;\n\n\tconst selection = useStoreSelector(\n\t\tstore,\n\t\t(state): ConversationsSelection =>\n\t\t\tstate\n\t\t\t\t? {\n\t\t\t\t\t\tconversations: state.ids\n\t\t\t\t\t\t\t.map((id) => state.byId[id])\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\t\tconversation\n\t\t\t\t\t\t\t\t): conversation is ListConversationsResponse[\"conversations\"][number] =>\n\t\t\t\t\t\t\t\t\tBoolean(conversation)\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\tpagination: state.pagination,\n\t\t\t\t\t}\n\t\t\t\t: { conversations: [], pagination: null },\n\t\tareSelectionsEqual\n\t);\n\n\tconst {\n\t\trefetch: queryRefetch,\n\t\tisLoading: queryLoading,\n\t\terror,\n\t} = useClientQuery<\n\t\tListConversationsResponse,\n\t\tPartial<ListConversationsRequest>\n\t>({\n\t\tclient,\n\t\tqueryKey: `conversations:${limit ?? \"\"}:${page ?? \"\"}:${status ?? \"\"}:${orderBy ?? \"\"}:${order ?? \"\"}`,\n\t\tqueryFn: (instance, args) =>\n\t\t\tinstance.listConversations({\n\t\t\t\t...requestDefaults,\n\t\t\t\t...args,\n\t\t\t}),\n\t\tenabled,\n\t\trefetchInterval,\n\t\trefetchOnWindowFocus,\n\t\trefetchOnMount: selection.conversations.length === 0,\n\t\tinitialArgs: requestDefaults,\n\t\tdependencies: [limit, page, status, orderBy, order],\n\t});\n\n\tconst refetch = useCallback(\n\t\t(args?: Partial<ListConversationsRequest>) =>\n\t\t\tqueryRefetch({\n\t\t\t\t...requestDefaults,\n\t\t\t\t...args,\n\t\t\t}),\n\t\t[queryRefetch, requestDefaults]\n\t);\n\n\tconst isInitialLoad = selection.conversations.length === 0;\n\tconst isLoading = isInitialLoad ? queryLoading : false;\n\n\treturn {\n\t\tconversations: selection.conversations,\n\t\tpagination: selection.pagination,\n\t\tisLoading,\n\t\terror,\n\t\trefetch,\n\t};\n}\n"],"mappings":";;;;;;AAeA,SAAS,mBACR,GACA,GACU;AAWV,KAAI,EATH,EAAE,eAAe,EAAE,cAClB,QAAQ,EAAE,WAAW,IACrB,QAAQ,EAAE,WAAW,IACrB,EAAE,YAAY,SAAS,EAAE,YAAY,QACrC,EAAE,YAAY,UAAU,EAAE,YAAY,SACtC,EAAE,YAAY,UAAU,EAAE,YAAY,SACtC,EAAE,YAAY,eAAe,EAAE,YAAY,cAC3C,EAAE,YAAY,YAAY,EAAE,YAAY,SAGzC,QAAO;AAGR,KAAI,EAAE,cAAc,WAAW,EAAE,cAAc,OAC9C,QAAO;AAGR,QAAO,EAAE,cAAc,OACrB,cAAc,UAAU,iBAAiB,EAAE,cAAc,OAC1D;;;;;;;;;;;;;;AAiCF,SAAgB,iBACf,UAAmC,EAAE,EACZ;CACzB,MAAM,EAAE,WAAW,YAAY;CAE/B,MAAM,EACL,OACA,MACA,OACA,SACA,QACA,UAAU,MACV,kBAAkB,OAClB,uBAAuB,SACpB;CAEJ,MAAM,kBAAkB,eAChB;EAAE;EAAO;EAAM;EAAQ;EAAS;EAAO,GAC9C;EAAC;EAAO;EAAM;EAAQ;EAAS;EAAM,CACrC;CAID,MAAM,YAAY,iBAFJ,QAAQ,sBAAsB,OAI1C,UACA,QACG;EACA,eAAe,MAAM,IACnB,KAAK,OAAO,MAAM,KAAK,IAAI,CAC3B,QAEC,iBAEA,QAAQ,aAAa,CACtB;EACF,YAAY,MAAM;EAClB,GACA;EAAE,eAAe,EAAE;EAAE,YAAY;EAAM,EAC3C,mBACA;CAED,MAAM,EACL,SAAS,cACT,WAAW,cACX,UACG,eAGF;EACD;EACA,UAAU,iBAAiB,SAAS,GAAG,GAAG,QAAQ,GAAG,GAAG,UAAU,GAAG,GAAG,WAAW,GAAG,GAAG,SAAS;EAClG,UAAU,UAAU,SACnB,SAAS,kBAAkB;GAC1B,GAAG;GACH,GAAG;GACH,CAAC;EACH;EACA;EACA;EACA,gBAAgB,UAAU,cAAc,WAAW;EACnD,aAAa;EACb,cAAc;GAAC;GAAO;GAAM;GAAQ;GAAS;GAAM;EACnD,CAAC;CAEF,MAAM,UAAU,aACd,SACA,aAAa;EACZ,GAAG;EACH,GAAG;EACH,CAAC,EACH,CAAC,cAAc,gBAAgB,CAC/B;CAGD,MAAM,YADgB,UAAU,cAAc,WAAW,IACvB,eAAe;AAEjD,QAAO;EACN,eAAe,UAAU;EACzB,YAAY,UAAU;EACtB;EACA;EACA;EACA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { TimelineItem } from "../timeline-item.js";
|
|
2
|
-
import { Conversation } from "../
|
|
3
|
-
import { CreateConversationResponseBody } from "../conversation.js";
|
|
1
|
+
import { TimelineItem } from "../packages/types/src/api/timeline-item.js";
|
|
2
|
+
import { Conversation } from "../packages/types/src/schemas.js";
|
|
3
|
+
import { CreateConversationResponseBody } from "../packages/types/src/api/conversation.js";
|
|
4
4
|
import { CossistantClient } from "@cossistant/core";
|
|
5
5
|
|
|
6
6
|
//#region src/hooks/use-create-conversation.d.ts
|
|
@@ -23,6 +23,7 @@ function useCreateConversation(options = {}) {
|
|
|
23
23
|
setError(null);
|
|
24
24
|
try {
|
|
25
25
|
const { websiteId, status, title, conversationId: providedConversationId, defaultTimelineItems = [], visitorId } = variables;
|
|
26
|
+
if (!client) throw new Error("Cossistant client is not available. Please ensure you have configured your API key.");
|
|
26
27
|
const initiated = client.initiateConversation({
|
|
27
28
|
conversationId: providedConversationId ?? void 0,
|
|
28
29
|
defaultTimelineItems,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-create-conversation.js","names":["response: CreateConversationResponseBody"],"sources":["../../src/hooks/use-create-conversation.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { CreateConversationResponseBody } from \"@cossistant/types/api/conversation\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport type { Conversation } from \"@cossistant/types/schemas\";\nimport { useCallback, useState } from \"react\";\nimport { useSupport } from \"../provider\";\n\nexport type UseCreateConversationOptions = {\n\tclient?: CossistantClient;\n\tonSuccess?: (data: CreateConversationResponseBody) => void;\n\tonError?: (error: Error) => void;\n};\n\nexport type CreateConversationVariables = {\n\tconversationId?: string;\n\tdefaultTimelineItems?: TimelineItem[];\n\tvisitorId?: string;\n\twebsiteId?: string | null;\n\tstatus?: Conversation[\"status\"];\n\ttitle?: string | null;\n};\n\nexport type UseCreateConversationResult = {\n\tmutate: (variables?: CreateConversationVariables) => void;\n\tmutateAsync: (\n\t\tvariables?: CreateConversationVariables\n\t) => Promise<CreateConversationResponseBody | null>;\n\tisPending: boolean;\n\terror: Error | null;\n\treset: () => void;\n};\n\nfunction toError(error: unknown): Error {\n\tif (error instanceof Error) {\n\t\treturn error;\n\t}\n\n\tif (typeof error === \"string\") {\n\t\treturn new Error(error);\n\t}\n\n\treturn new Error(\"Unknown error\");\n}\n\n/**\n * Imperative helper for bootstrapping a new conversation locally before the\n * backend persists it. Mirrors react-query's mutate API to simplify\n * integration with forms or buttons.\n */\nexport function useCreateConversation(\n\toptions: UseCreateConversationOptions = {}\n): UseCreateConversationResult {\n\tconst { client: contextClient } = useSupport();\n\tconst { client: overrideClient, onError, onSuccess } = options;\n\tconst client = overrideClient ?? contextClient;\n\n\tconst [isPending, setIsPending] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst mutateAsync = useCallback(\n\t\tasync (\n\t\t\tvariables: CreateConversationVariables = {}\n\t\t): Promise<CreateConversationResponseBody | null> => {\n\t\t\tsetIsPending(true);\n\t\t\tsetError(null);\n\n\t\t\ttry {\n\t\t\t\tconst {\n\t\t\t\t\twebsiteId,\n\t\t\t\t\tstatus,\n\t\t\t\t\ttitle,\n\t\t\t\t\tconversationId: providedConversationId,\n\t\t\t\t\tdefaultTimelineItems = [],\n\t\t\t\t\tvisitorId,\n\t\t\t\t} = variables;\n\n\t\t\t\tconst initiated = client.initiateConversation({\n\t\t\t\t\tconversationId: providedConversationId ?? undefined,\n\t\t\t\t\tdefaultTimelineItems,\n\t\t\t\t\tvisitorId: visitorId ?? undefined,\n\t\t\t\t\twebsiteId: websiteId ?? undefined,\n\t\t\t\t\tstatus: status ?? undefined,\n\t\t\t\t\ttitle: title ?? undefined,\n\t\t\t\t});\n\n\t\t\t\tconst response: CreateConversationResponseBody = {\n\t\t\t\t\tconversation: initiated.conversation,\n\t\t\t\t\tinitialTimelineItems: initiated.defaultTimelineItems,\n\t\t\t\t};\n\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(null);\n\t\t\t\tonSuccess?.(response);\n\t\t\t\treturn response;\n\t\t\t} catch (raw) {\n\t\t\t\tconst normalised = toError(raw);\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(normalised);\n\t\t\t\tonError?.(normalised);\n\t\t\t\tthrow normalised;\n\t\t\t}\n\t\t},\n\t\t[client, onError, onSuccess]\n\t);\n\n\tconst mutate = useCallback(\n\t\t(variables?: CreateConversationVariables) => {\n\t\t\tvoid mutateAsync(variables).catch(() => {\n\t\t\t\t// Intentionally swallow to match react-query semantics\n\t\t\t});\n\t\t},\n\t\t[mutateAsync]\n\t);\n\n\tconst reset = useCallback(() => {\n\t\tsetError(null);\n\t\tsetIsPending(false);\n\t}, []);\n\n\treturn {\n\t\tmutate,\n\t\tmutateAsync,\n\t\tisPending,\n\t\terror,\n\t\treset,\n\t};\n}\n"],"mappings":";;;;AAgCA,SAAS,QAAQ,OAAuB;AACvC,KAAI,iBAAiB,MACpB,QAAO;AAGR,KAAI,OAAO,UAAU,SACpB,QAAO,IAAI,MAAM,MAAM;AAGxB,wBAAO,IAAI,MAAM,gBAAgB;;;;;;;AAQlC,SAAgB,sBACf,UAAwC,EAAE,EACZ;CAC9B,MAAM,EAAE,QAAQ,kBAAkB,YAAY;CAC9C,MAAM,EAAE,QAAQ,gBAAgB,SAAS,cAAc;CACvD,MAAM,SAAS,kBAAkB;CAEjC,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CACjD,MAAM,CAAC,OAAO,YAAY,SAAuB,KAAK;CAEtD,MAAM,cAAc,YACnB,OACC,YAAyC,EAAE,KACS;AACpD,eAAa,KAAK;AAClB,WAAS,KAAK;AAEd,MAAI;GACH,MAAM,EACL,WACA,QACA,OACA,gBAAgB,wBAChB,uBAAuB,EAAE,EACzB,cACG;
|
|
1
|
+
{"version":3,"file":"use-create-conversation.js","names":["response: CreateConversationResponseBody"],"sources":["../../src/hooks/use-create-conversation.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { CreateConversationResponseBody } from \"@cossistant/types/api/conversation\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport type { Conversation } from \"@cossistant/types/schemas\";\nimport { useCallback, useState } from \"react\";\nimport { useSupport } from \"../provider\";\n\nexport type UseCreateConversationOptions = {\n\tclient?: CossistantClient;\n\tonSuccess?: (data: CreateConversationResponseBody) => void;\n\tonError?: (error: Error) => void;\n};\n\nexport type CreateConversationVariables = {\n\tconversationId?: string;\n\tdefaultTimelineItems?: TimelineItem[];\n\tvisitorId?: string;\n\twebsiteId?: string | null;\n\tstatus?: Conversation[\"status\"];\n\ttitle?: string | null;\n};\n\nexport type UseCreateConversationResult = {\n\tmutate: (variables?: CreateConversationVariables) => void;\n\tmutateAsync: (\n\t\tvariables?: CreateConversationVariables\n\t) => Promise<CreateConversationResponseBody | null>;\n\tisPending: boolean;\n\terror: Error | null;\n\treset: () => void;\n};\n\nfunction toError(error: unknown): Error {\n\tif (error instanceof Error) {\n\t\treturn error;\n\t}\n\n\tif (typeof error === \"string\") {\n\t\treturn new Error(error);\n\t}\n\n\treturn new Error(\"Unknown error\");\n}\n\n/**\n * Imperative helper for bootstrapping a new conversation locally before the\n * backend persists it. Mirrors react-query's mutate API to simplify\n * integration with forms or buttons.\n */\nexport function useCreateConversation(\n\toptions: UseCreateConversationOptions = {}\n): UseCreateConversationResult {\n\tconst { client: contextClient } = useSupport();\n\tconst { client: overrideClient, onError, onSuccess } = options;\n\tconst client = overrideClient ?? contextClient;\n\n\tconst [isPending, setIsPending] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst mutateAsync = useCallback(\n\t\tasync (\n\t\t\tvariables: CreateConversationVariables = {}\n\t\t): Promise<CreateConversationResponseBody | null> => {\n\t\t\tsetIsPending(true);\n\t\t\tsetError(null);\n\n\t\t\ttry {\n\t\t\t\tconst {\n\t\t\t\t\twebsiteId,\n\t\t\t\t\tstatus,\n\t\t\t\t\ttitle,\n\t\t\t\t\tconversationId: providedConversationId,\n\t\t\t\t\tdefaultTimelineItems = [],\n\t\t\t\t\tvisitorId,\n\t\t\t\t} = variables;\n\n\t\t\t\tif (!client) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"Cossistant client is not available. Please ensure you have configured your API key.\"\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst initiated = client.initiateConversation({\n\t\t\t\t\tconversationId: providedConversationId ?? undefined,\n\t\t\t\t\tdefaultTimelineItems,\n\t\t\t\t\tvisitorId: visitorId ?? undefined,\n\t\t\t\t\twebsiteId: websiteId ?? undefined,\n\t\t\t\t\tstatus: status ?? undefined,\n\t\t\t\t\ttitle: title ?? undefined,\n\t\t\t\t});\n\n\t\t\t\tconst response: CreateConversationResponseBody = {\n\t\t\t\t\tconversation: initiated.conversation,\n\t\t\t\t\tinitialTimelineItems: initiated.defaultTimelineItems,\n\t\t\t\t};\n\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(null);\n\t\t\t\tonSuccess?.(response);\n\t\t\t\treturn response;\n\t\t\t} catch (raw) {\n\t\t\t\tconst normalised = toError(raw);\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(normalised);\n\t\t\t\tonError?.(normalised);\n\t\t\t\tthrow normalised;\n\t\t\t}\n\t\t},\n\t\t[client, onError, onSuccess]\n\t);\n\n\tconst mutate = useCallback(\n\t\t(variables?: CreateConversationVariables) => {\n\t\t\tvoid mutateAsync(variables).catch(() => {\n\t\t\t\t// Intentionally swallow to match react-query semantics\n\t\t\t});\n\t\t},\n\t\t[mutateAsync]\n\t);\n\n\tconst reset = useCallback(() => {\n\t\tsetError(null);\n\t\tsetIsPending(false);\n\t}, []);\n\n\treturn {\n\t\tmutate,\n\t\tmutateAsync,\n\t\tisPending,\n\t\terror,\n\t\treset,\n\t};\n}\n"],"mappings":";;;;AAgCA,SAAS,QAAQ,OAAuB;AACvC,KAAI,iBAAiB,MACpB,QAAO;AAGR,KAAI,OAAO,UAAU,SACpB,QAAO,IAAI,MAAM,MAAM;AAGxB,wBAAO,IAAI,MAAM,gBAAgB;;;;;;;AAQlC,SAAgB,sBACf,UAAwC,EAAE,EACZ;CAC9B,MAAM,EAAE,QAAQ,kBAAkB,YAAY;CAC9C,MAAM,EAAE,QAAQ,gBAAgB,SAAS,cAAc;CACvD,MAAM,SAAS,kBAAkB;CAEjC,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CACjD,MAAM,CAAC,OAAO,YAAY,SAAuB,KAAK;CAEtD,MAAM,cAAc,YACnB,OACC,YAAyC,EAAE,KACS;AACpD,eAAa,KAAK;AAClB,WAAS,KAAK;AAEd,MAAI;GACH,MAAM,EACL,WACA,QACA,OACA,gBAAgB,wBAChB,uBAAuB,EAAE,EACzB,cACG;AAEJ,OAAI,CAAC,OACJ,OAAM,IAAI,MACT,sFACA;GAGF,MAAM,YAAY,OAAO,qBAAqB;IAC7C,gBAAgB,0BAA0B;IAC1C;IACA,WAAW,aAAa;IACxB,WAAW,aAAa;IACxB,QAAQ,UAAU;IAClB,OAAO,SAAS;IAChB,CAAC;GAEF,MAAMA,WAA2C;IAChD,cAAc,UAAU;IACxB,sBAAsB,UAAU;IAChC;AAED,gBAAa,MAAM;AACnB,YAAS,KAAK;AACd,eAAY,SAAS;AACrB,UAAO;WACC,KAAK;GACb,MAAM,aAAa,QAAQ,IAAI;AAC/B,gBAAa,MAAM;AACnB,YAAS,WAAW;AACpB,aAAU,WAAW;AACrB,SAAM;;IAGR;EAAC;EAAQ;EAAS;EAAU,CAC5B;AAgBD,QAAO;EACN,QAfc,aACb,cAA4C;AAC5C,GAAK,YAAY,UAAU,CAAC,YAAY,GAEtC;KAEH,CAAC,YAAY,CACb;EASA;EACA;EACA;EACA,OAVa,kBAAkB;AAC/B,YAAS,KAAK;AACd,gBAAa,MAAM;KACjB,EAAE,CAAC;EAQL"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TimelinePartFile, TimelinePartImage } from "../timeline-item.js";
|
|
1
|
+
import { TimelinePartFile, TimelinePartImage } from "../packages/types/src/api/timeline-item.js";
|
|
2
2
|
import { CossistantClient } from "@cossistant/core";
|
|
3
3
|
|
|
4
4
|
//#region src/hooks/use-file-upload.d.ts
|
package/hooks/use-file-upload.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
import { useSupport } from "../provider.js";
|
|
5
4
|
import { useCallback, useState } from "react";
|
|
6
5
|
import { MAX_FILES_PER_MESSAGE, isImageMimeType, validateFiles } from "@cossistant/core";
|
|
@@ -51,6 +50,7 @@ function useFileUpload(options = {}) {
|
|
|
51
50
|
setProgress(0);
|
|
52
51
|
setError(null);
|
|
53
52
|
try {
|
|
53
|
+
if (!client) throw new Error("Cossistant client is not available. Please ensure you have configured your API key.");
|
|
54
54
|
const totalFiles = files.length;
|
|
55
55
|
let completedFiles = 0;
|
|
56
56
|
const uploadPromises = files.map(async (file) => {
|
|
@@ -66,14 +66,14 @@ function useFileUpload(options = {}) {
|
|
|
66
66
|
type: "image",
|
|
67
67
|
url: uploadInfo.publicUrl,
|
|
68
68
|
mediaType: file.type,
|
|
69
|
-
|
|
69
|
+
filename: file.name,
|
|
70
70
|
size: file.size
|
|
71
71
|
};
|
|
72
72
|
return {
|
|
73
73
|
type: "file",
|
|
74
74
|
url: uploadInfo.publicUrl,
|
|
75
75
|
mediaType: file.type,
|
|
76
|
-
|
|
76
|
+
filename: file.name,
|
|
77
77
|
size: file.size
|
|
78
78
|
};
|
|
79
79
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-file-upload.js","names":[],"sources":["../../src/hooks/use-file-upload.ts"],"sourcesContent":["\"use client\";\n\nimport type { CossistantClient } from \"@cossistant/core\";\nimport {\n\tisImageMimeType,\n\tMAX_FILES_PER_MESSAGE,\n\tvalidateFiles,\n} from \"@cossistant/core\";\nimport type {\n\tTimelinePartFile,\n\tTimelinePartImage,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useState } from \"react\";\nimport { useSupport } from \"../provider\";\n\nexport type FileUploadPart = TimelinePartImage | TimelinePartFile;\n\nexport type UseFileUploadOptions = {\n\t/**\n\t * Optional Cossistant client instance.\n\t * If not provided, uses the client from SupportProvider context.\n\t */\n\tclient?: CossistantClient;\n};\n\nexport type UseFileUploadReturn = {\n\t/**\n\t * Upload files and return timeline parts ready to include in a message.\n\t * Files are uploaded to S3 in parallel.\n\t */\n\tuploadFiles: (\n\t\tfiles: File[],\n\t\tconversationId: string\n\t) => Promise<FileUploadPart[]>;\n\n\t/**\n\t * Whether an upload is currently in progress.\n\t */\n\tisUploading: boolean;\n\n\t/**\n\t * Upload progress (0-100). Updates as files complete.\n\t */\n\tprogress: number;\n\n\t/**\n\t * Error from the most recent upload attempt, if any.\n\t */\n\terror: Error | null;\n\n\t/**\n\t * Reset the upload state (clear errors and progress).\n\t */\n\treset: () => void;\n};\n\n/**\n * Hook for uploading files to S3 for inclusion in chat messages.\n * Handles validation, upload progress tracking, and error management.\n *\n * @example\n * ```tsx\n * const { uploadFiles, isUploading, error } = useFileUpload();\n *\n * const handleSend = async () => {\n * if (files.length > 0) {\n * const parts = await uploadFiles(files, conversationId);\n * // Include parts in message...\n * }\n * };\n * ```\n */\nexport function useFileUpload(\n\toptions: UseFileUploadOptions = {}\n): UseFileUploadReturn {\n\tconst { client: contextClient } = useSupport();\n\tconst client = options.client ?? contextClient;\n\n\tconst [isUploading, setIsUploading] = useState(false);\n\tconst [progress, setProgress] = useState(0);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst reset = useCallback(() => {\n\t\tsetIsUploading(false);\n\t\tsetProgress(0);\n\t\tsetError(null);\n\t}, []);\n\n\tconst uploadFiles = useCallback(\n\t\tasync (\n\t\t\tfiles: File[],\n\t\t\tconversationId: string\n\t\t): Promise<FileUploadPart[]> => {\n\t\t\tif (files.length === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\t// Validate files before upload\n\t\t\tconst validationError = validateFiles(files);\n\t\t\tif (validationError) {\n\t\t\t\tconst err = new Error(validationError);\n\t\t\t\tsetError(err);\n\t\t\t\tthrow err;\n\t\t\t}\n\n\t\t\tif (files.length > MAX_FILES_PER_MESSAGE) {\n\t\t\t\tconst err = new Error(\n\t\t\t\t\t`Cannot upload more than ${MAX_FILES_PER_MESSAGE} files at once`\n\t\t\t\t);\n\t\t\t\tsetError(err);\n\t\t\t\tthrow err;\n\t\t\t}\n\n\t\t\tsetIsUploading(true);\n\t\t\tsetProgress(0);\n\t\t\tsetError(null);\n\n\t\t\ttry {\n\t\t\t\tconst totalFiles = files.length;\n\t\t\t\tlet completedFiles = 0;\n\n\t\t\t\t// Upload files in parallel\n\t\t\t\tconst uploadPromises = files.map(async (file) => {\n\t\t\t\t\t// Generate presigned URL\n\t\t\t\t\tconst uploadInfo = await client.generateUploadUrl({\n\t\t\t\t\t\tconversationId,\n\t\t\t\t\t\tcontentType: file.type,\n\t\t\t\t\t\tfileName: file.name,\n\t\t\t\t\t});\n\n\t\t\t\t\t// Upload file to S3\n\t\t\t\t\tawait client.uploadFile(file, uploadInfo.uploadUrl, file.type);\n\n\t\t\t\t\t// Update progress\n\t\t\t\t\tcompletedFiles += 1;\n\t\t\t\t\tsetProgress(Math.round((completedFiles / totalFiles) * 100));\n\n\t\t\t\t\t// Return timeline part based on file type\n\t\t\t\t\tconst isImage = isImageMimeType(file.type);\n\n\t\t\t\t\tif (isImage) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"image\" as const,\n\t\t\t\t\t\t\turl: uploadInfo.publicUrl,\n\t\t\t\t\t\t\tmediaType: file.type,\n\t\t\t\t\t\t\
|
|
1
|
+
{"version":3,"file":"use-file-upload.js","names":[],"sources":["../../src/hooks/use-file-upload.ts"],"sourcesContent":["\"use client\";\n\nimport type { CossistantClient } from \"@cossistant/core\";\nimport {\n\tisImageMimeType,\n\tMAX_FILES_PER_MESSAGE,\n\tvalidateFiles,\n} from \"@cossistant/core\";\nimport type {\n\tTimelinePartFile,\n\tTimelinePartImage,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useState } from \"react\";\nimport { useSupport } from \"../provider\";\n\nexport type FileUploadPart = TimelinePartImage | TimelinePartFile;\n\nexport type UseFileUploadOptions = {\n\t/**\n\t * Optional Cossistant client instance.\n\t * If not provided, uses the client from SupportProvider context.\n\t */\n\tclient?: CossistantClient;\n};\n\nexport type UseFileUploadReturn = {\n\t/**\n\t * Upload files and return timeline parts ready to include in a message.\n\t * Files are uploaded to S3 in parallel.\n\t */\n\tuploadFiles: (\n\t\tfiles: File[],\n\t\tconversationId: string\n\t) => Promise<FileUploadPart[]>;\n\n\t/**\n\t * Whether an upload is currently in progress.\n\t */\n\tisUploading: boolean;\n\n\t/**\n\t * Upload progress (0-100). Updates as files complete.\n\t */\n\tprogress: number;\n\n\t/**\n\t * Error from the most recent upload attempt, if any.\n\t */\n\terror: Error | null;\n\n\t/**\n\t * Reset the upload state (clear errors and progress).\n\t */\n\treset: () => void;\n};\n\n/**\n * Hook for uploading files to S3 for inclusion in chat messages.\n * Handles validation, upload progress tracking, and error management.\n *\n * @example\n * ```tsx\n * const { uploadFiles, isUploading, error } = useFileUpload();\n *\n * const handleSend = async () => {\n * if (files.length > 0) {\n * const parts = await uploadFiles(files, conversationId);\n * // Include parts in message...\n * }\n * };\n * ```\n */\nexport function useFileUpload(\n\toptions: UseFileUploadOptions = {}\n): UseFileUploadReturn {\n\tconst { client: contextClient } = useSupport();\n\tconst client = options.client ?? contextClient;\n\n\tconst [isUploading, setIsUploading] = useState(false);\n\tconst [progress, setProgress] = useState(0);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst reset = useCallback(() => {\n\t\tsetIsUploading(false);\n\t\tsetProgress(0);\n\t\tsetError(null);\n\t}, []);\n\n\tconst uploadFiles = useCallback(\n\t\tasync (\n\t\t\tfiles: File[],\n\t\t\tconversationId: string\n\t\t): Promise<FileUploadPart[]> => {\n\t\t\tif (files.length === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\t// Validate files before upload\n\t\t\tconst validationError = validateFiles(files);\n\t\t\tif (validationError) {\n\t\t\t\tconst err = new Error(validationError);\n\t\t\t\tsetError(err);\n\t\t\t\tthrow err;\n\t\t\t}\n\n\t\t\tif (files.length > MAX_FILES_PER_MESSAGE) {\n\t\t\t\tconst err = new Error(\n\t\t\t\t\t`Cannot upload more than ${MAX_FILES_PER_MESSAGE} files at once`\n\t\t\t\t);\n\t\t\t\tsetError(err);\n\t\t\t\tthrow err;\n\t\t\t}\n\n\t\t\tsetIsUploading(true);\n\t\t\tsetProgress(0);\n\t\t\tsetError(null);\n\n\t\t\ttry {\n\t\t\t\tif (!client) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"Cossistant client is not available. Please ensure you have configured your API key.\"\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst totalFiles = files.length;\n\t\t\t\tlet completedFiles = 0;\n\n\t\t\t\t// Upload files in parallel\n\t\t\t\tconst uploadPromises = files.map(async (file) => {\n\t\t\t\t\t// Generate presigned URL\n\t\t\t\t\tconst uploadInfo = await client.generateUploadUrl({\n\t\t\t\t\t\tconversationId,\n\t\t\t\t\t\tcontentType: file.type,\n\t\t\t\t\t\tfileName: file.name,\n\t\t\t\t\t});\n\n\t\t\t\t\t// Upload file to S3\n\t\t\t\t\tawait client.uploadFile(file, uploadInfo.uploadUrl, file.type);\n\n\t\t\t\t\t// Update progress\n\t\t\t\t\tcompletedFiles += 1;\n\t\t\t\t\tsetProgress(Math.round((completedFiles / totalFiles) * 100));\n\n\t\t\t\t\t// Return timeline part based on file type\n\t\t\t\t\tconst isImage = isImageMimeType(file.type);\n\n\t\t\t\t\tif (isImage) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"image\" as const,\n\t\t\t\t\t\t\turl: uploadInfo.publicUrl,\n\t\t\t\t\t\t\tmediaType: file.type,\n\t\t\t\t\t\t\tfilename: file.name,\n\t\t\t\t\t\t\tsize: file.size,\n\t\t\t\t\t\t} satisfies TimelinePartImage;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttype: \"file\" as const,\n\t\t\t\t\t\turl: uploadInfo.publicUrl,\n\t\t\t\t\t\tmediaType: file.type,\n\t\t\t\t\t\tfilename: file.name,\n\t\t\t\t\t\tsize: file.size,\n\t\t\t\t\t} satisfies TimelinePartFile;\n\t\t\t\t});\n\n\t\t\t\tconst parts = await Promise.all(uploadPromises);\n\n\t\t\t\tsetIsUploading(false);\n\t\t\t\tsetProgress(100);\n\n\t\t\t\treturn parts;\n\t\t\t} catch (err) {\n\t\t\t\tconst normalizedError =\n\t\t\t\t\terr instanceof Error ? err : new Error(\"Upload failed\");\n\t\t\t\tsetError(normalizedError);\n\t\t\t\tsetIsUploading(false);\n\t\t\t\tthrow normalizedError;\n\t\t\t}\n\t\t},\n\t\t[client]\n\t);\n\n\treturn {\n\t\tuploadFiles,\n\t\tisUploading,\n\t\tprogress,\n\t\terror,\n\t\treset,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwEA,SAAgB,cACf,UAAgC,EAAE,EACZ;CACtB,MAAM,EAAE,QAAQ,kBAAkB,YAAY;CAC9C,MAAM,SAAS,QAAQ,UAAU;CAEjC,MAAM,CAAC,aAAa,kBAAkB,SAAS,MAAM;CACrD,MAAM,CAAC,UAAU,eAAe,SAAS,EAAE;CAC3C,MAAM,CAAC,OAAO,YAAY,SAAuB,KAAK;CAEtD,MAAM,QAAQ,kBAAkB;AAC/B,iBAAe,MAAM;AACrB,cAAY,EAAE;AACd,WAAS,KAAK;IACZ,EAAE,CAAC;AAgGN,QAAO;EACN,aA/FmB,YACnB,OACC,OACA,mBAC+B;AAC/B,OAAI,MAAM,WAAW,EACpB,QAAO,EAAE;GAIV,MAAM,kBAAkB,cAAc,MAAM;AAC5C,OAAI,iBAAiB;IACpB,MAAM,MAAM,IAAI,MAAM,gBAAgB;AACtC,aAAS,IAAI;AACb,UAAM;;AAGP,OAAI,MAAM,SAAS,uBAAuB;IACzC,MAAM,sBAAM,IAAI,MACf,2BAA2B,sBAAsB,gBACjD;AACD,aAAS,IAAI;AACb,UAAM;;AAGP,kBAAe,KAAK;AACpB,eAAY,EAAE;AACd,YAAS,KAAK;AAEd,OAAI;AACH,QAAI,CAAC,OACJ,OAAM,IAAI,MACT,sFACA;IAGF,MAAM,aAAa,MAAM;IACzB,IAAI,iBAAiB;IAGrB,MAAM,iBAAiB,MAAM,IAAI,OAAO,SAAS;KAEhD,MAAM,aAAa,MAAM,OAAO,kBAAkB;MACjD;MACA,aAAa,KAAK;MAClB,UAAU,KAAK;MACf,CAAC;AAGF,WAAM,OAAO,WAAW,MAAM,WAAW,WAAW,KAAK,KAAK;AAG9D,uBAAkB;AAClB,iBAAY,KAAK,MAAO,iBAAiB,aAAc,IAAI,CAAC;AAK5D,SAFgB,gBAAgB,KAAK,KAAK,CAGzC,QAAO;MACN,MAAM;MACN,KAAK,WAAW;MAChB,WAAW,KAAK;MAChB,UAAU,KAAK;MACf,MAAM,KAAK;MACX;AAGF,YAAO;MACN,MAAM;MACN,KAAK,WAAW;MAChB,WAAW,KAAK;MAChB,UAAU,KAAK;MACf,MAAM,KAAK;MACX;MACA;IAEF,MAAM,QAAQ,MAAM,QAAQ,IAAI,eAAe;AAE/C,mBAAe,MAAM;AACrB,gBAAY,IAAI;AAEhB,WAAO;YACC,KAAK;IACb,MAAM,kBACL,eAAe,QAAQ,sBAAM,IAAI,MAAM,gBAAgB;AACxD,aAAS,gBAAgB;AACzB,mBAAe,MAAM;AACrB,UAAM;;KAGR,CAAC,OAAO,CACR;EAIA;EACA;EACA;EACA;EACA"}
|
package/hooks/use-home-page.js
CHANGED
|
@@ -58,10 +58,10 @@ function useHomePage(options = {}) {
|
|
|
58
58
|
});
|
|
59
59
|
const conversations = useMemo(() => allConversations.filter(shouldDisplayConversation), [allConversations]);
|
|
60
60
|
const { lastOpenConversation, availableConversationsCount } = useMemo(() => {
|
|
61
|
-
const
|
|
61
|
+
const conversationToShow = conversations.find((conv) => conv.status === ConversationStatus.OPEN) ?? conversations[0];
|
|
62
62
|
return {
|
|
63
|
-
lastOpenConversation:
|
|
64
|
-
availableConversationsCount: Math.max(conversations.length - (
|
|
63
|
+
lastOpenConversation: conversationToShow,
|
|
64
|
+
availableConversationsCount: Math.max(conversations.length - (conversationToShow ? 1 : 0), 0)
|
|
65
65
|
};
|
|
66
66
|
}, [conversations]);
|
|
67
67
|
const startConversation = useCallback((initialMessage) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-home-page.js","names":[
|
|
1
|
+
{"version":3,"file":"use-home-page.js","names":[],"sources":["../../src/hooks/use-home-page.ts"],"sourcesContent":["import { type Conversation, ConversationStatus } from \"@cossistant/types\";\nimport { useCallback, useMemo } from \"react\";\nimport { shouldDisplayConversation } from \"../utils/conversation\";\nimport { useConversations } from \"./use-conversations\";\n\nexport type UseHomePageOptions = {\n\t/**\n\t * Whether to enable conversations fetching.\n\t * Default: true\n\t */\n\tenabled?: boolean;\n\n\t/**\n\t * Callback when user wants to start a new conversation.\n\t */\n\tonStartConversation?: (initialMessage?: string) => void;\n\n\t/**\n\t * Callback when user wants to open an existing conversation.\n\t */\n\tonOpenConversation?: (conversationId: string) => void;\n\n\t/**\n\t * Callback when user wants to view conversation history.\n\t */\n\tonOpenConversationHistory?: () => void;\n};\n\nexport type UseHomePageReturn = {\n\t// Conversations data\n\tconversations: Conversation[];\n\tisLoading: boolean;\n\terror: Error | null;\n\n\t// Derived state\n\tlastOpenConversation: Conversation | undefined;\n\tavailableConversationsCount: number;\n\thasConversations: boolean;\n\n\t// Actions\n\tstartConversation: (initialMessage?: string) => void;\n\topenConversation: (conversationId: string) => void;\n\topenConversationHistory: () => void;\n};\n\n/**\n * Main hook for the home page of the support widget.\n *\n * This hook:\n * - Fetches and manages conversations list\n * - Derives useful state (last open conversation, conversation counts)\n * - Provides navigation actions for the home page\n *\n * It encapsulates all home page logic, making the component\n * purely presentational.\n *\n * @example\n * ```tsx\n * export function HomePage() {\n * const home = useHomePage({\n * onStartConversation: (msg) => {\n * navigate('conversation', { conversationId: PENDING_CONVERSATION_ID, initialMessage: msg });\n * },\n * onOpenConversation: (id) => {\n * navigate('conversation', { conversationId: id });\n * },\n * onOpenConversationHistory: () => {\n * navigate('conversation-history');\n * },\n * });\n *\n * return (\n * <>\n * <h1>How can we help?</h1>\n *\n * {home.lastOpenConversation && (\n * <ConversationCard\n * conversation={home.lastOpenConversation}\n * onClick={() => home.openConversation(home.lastOpenConversation.id)}\n * />\n * )}\n *\n * <Button onClick={() => home.startConversation()}>\n * Ask a question\n * </Button>\n * </>\n * );\n * }\n * ```\n */\nexport function useHomePage(\n\toptions: UseHomePageOptions = {}\n): UseHomePageReturn {\n\tconst {\n\t\tenabled = true,\n\t\tonStartConversation,\n\t\tonOpenConversation,\n\t\tonOpenConversationHistory,\n\t} = options;\n\n\t// Fetch conversations\n\tconst {\n\t\tconversations: allConversations,\n\t\tisLoading,\n\t\terror,\n\t} = useConversations({\n\t\tenabled,\n\t\t// Fetch most recent conversations first\n\t\torderBy: \"updatedAt\",\n\t\torder: \"desc\",\n\t});\n\n\tconst conversations = useMemo(\n\t\t() => allConversations.filter(shouldDisplayConversation),\n\t\t[allConversations]\n\t);\n\n\t// Derive useful state from conversations\n\tconst { lastOpenConversation, availableConversationsCount } = useMemo(() => {\n\t\t// Find the most recent open conversation first\n\t\tconst openConversation = conversations.find(\n\t\t\t(conv) => conv.status === ConversationStatus.OPEN\n\t\t);\n\n\t\t// If no open conversation, show the most recent one (could be resolved)\n\t\tconst conversationToShow = openConversation ?? conversations[0];\n\n\t\t// Count other conversations (excluding the one we're showing)\n\t\tconst otherCount = Math.max(\n\t\t\tconversations.length - (conversationToShow ? 1 : 0),\n\t\t\t0\n\t\t);\n\n\t\treturn {\n\t\t\tlastOpenConversation: conversationToShow,\n\t\t\tavailableConversationsCount: otherCount,\n\t\t};\n\t}, [conversations]);\n\n\t// Navigation actions\n\tconst startConversation = useCallback(\n\t\t(initialMessage?: string) => {\n\t\t\tonStartConversation?.(initialMessage);\n\t\t},\n\t\t[onStartConversation]\n\t);\n\n\tconst openConversation = useCallback(\n\t\t(conversationId: string) => {\n\t\t\tonOpenConversation?.(conversationId);\n\t\t},\n\t\t[onOpenConversation]\n\t);\n\n\tconst openConversationHistory = useCallback(() => {\n\t\tonOpenConversationHistory?.();\n\t}, [onOpenConversationHistory]);\n\n\treturn {\n\t\tconversations,\n\t\tisLoading,\n\t\terror,\n\t\tlastOpenConversation,\n\t\tavailableConversationsCount,\n\t\thasConversations: conversations.length > 0,\n\t\tstartConversation,\n\t\topenConversation,\n\t\topenConversationHistory,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0FA,SAAgB,YACf,UAA8B,EAAE,EACZ;CACpB,MAAM,EACL,UAAU,MACV,qBACA,oBACA,8BACG;CAGJ,MAAM,EACL,eAAe,kBACf,WACA,UACG,iBAAiB;EACpB;EAEA,SAAS;EACT,OAAO;EACP,CAAC;CAEF,MAAM,gBAAgB,cACf,iBAAiB,OAAO,0BAA0B,EACxD,CAAC,iBAAiB,CAClB;CAGD,MAAM,EAAE,sBAAsB,gCAAgC,cAAc;EAO3E,MAAM,qBALmB,cAAc,MACrC,SAAS,KAAK,WAAW,mBAAmB,KAC7C,IAG8C,cAAc;AAQ7D,SAAO;GACN,sBAAsB;GACtB,6BAPkB,KAAK,IACvB,cAAc,UAAU,qBAAqB,IAAI,IACjD,EACA;GAKA;IACC,CAAC,cAAc,CAAC;CAGnB,MAAM,oBAAoB,aACxB,mBAA4B;AAC5B,wBAAsB,eAAe;IAEtC,CAAC,oBAAoB,CACrB;CAED,MAAM,mBAAmB,aACvB,mBAA2B;AAC3B,uBAAqB,eAAe;IAErC,CAAC,mBAAmB,CACpB;CAED,MAAM,0BAA0B,kBAAkB;AACjD,+BAA6B;IAC3B,CAAC,0BAA0B,CAAC;AAE/B,QAAO;EACN;EACA;EACA;EACA;EACA;EACA,kBAAkB,cAAc,SAAS;EACzC;EACA;EACA;EACA"}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { TimelineItem } from "../timeline-item.js";
|
|
1
|
+
import { TimelineItem } from "../packages/types/src/api/timeline-item.js";
|
|
2
2
|
import { UseMultimodalInputOptions } from "./private/use-multimodal-input.js";
|
|
3
|
-
import { AnyRealtimeEvent } from "../realtime-events.js";
|
|
3
|
+
import { AnyRealtimeEvent } from "../packages/types/src/realtime-events.js";
|
|
4
4
|
import { CossistantClient } from "@cossistant/core";
|
|
5
5
|
|
|
6
6
|
//#region src/hooks/use-message-composer.d.ts
|
|
7
7
|
type UseMessageComposerOptions = {
|
|
8
8
|
/**
|
|
9
9
|
* The Cossistant client instance.
|
|
10
|
+
* Optional - when not provided, the composer will be disabled.
|
|
10
11
|
*/
|
|
11
|
-
client
|
|
12
|
+
client?: CossistantClient;
|
|
12
13
|
/**
|
|
13
14
|
* Current conversation ID. Can be null if no real conversation exists yet.
|
|
14
15
|
* Pass null when showing default timeline items before user sends first message.
|
|
@@ -28,6 +29,12 @@ type UseMessageComposerOptions = {
|
|
|
28
29
|
* @param messageId - The sent message ID
|
|
29
30
|
*/
|
|
30
31
|
onMessageSent?: (conversationId: string, messageId: string) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Called immediately after a new conversation is initiated (before API call).
|
|
34
|
+
* Use this to immediately switch the UI to the new conversation ID for
|
|
35
|
+
* proper optimistic updates display.
|
|
36
|
+
*/
|
|
37
|
+
onConversationInitiated?: (conversationId: string) => void;
|
|
31
38
|
/**
|
|
32
39
|
* Callback when message sending fails.
|
|
33
40
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-message-composer.d.ts","names":[],"sources":["../../src/hooks/use-message-composer.ts"],"sourcesContent":[],"mappings":";;;;;;KAWY,yBAAA;;AAAZ
|
|
1
|
+
{"version":3,"file":"use-message-composer.d.ts","names":[],"sources":["../../src/hooks/use-message-composer.ts"],"sourcesContent":[],"mappings":";;;;;;KAWY,yBAAA;;AAAZ;;;EAwCmB,MAAA,CAAA,EAnCT,gBAmCS;EAMjB;;;;EAgBU,cAAA,EAAA,MAAA,GAAA,IAAwB;EAG5B;;;EAUe,oBAAA,CAAA,EA3DC,YA2DD,EAAA;EAwCP;;;;;;;;;;;;;;;;;;;oBA3EG;;;;gBAKJ,KACb;;;;;0BAQuB;;;;;;KAQb,wBAAA;;SAGJ;SACA;;;;;oBASW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwCH,kBAAA,UACN,4BACP"}
|
|
@@ -38,10 +38,10 @@ import { useCallback, useEffect } from "react";
|
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
40
|
function useMessageComposer(options) {
|
|
41
|
-
const { client, conversationId, defaultTimelineItems = [], visitorId, onMessageSent, onError, fileOptions, realtimeSend, isRealtimeConnected = false } = options;
|
|
41
|
+
const { client, conversationId, defaultTimelineItems = [], visitorId, onMessageSent, onConversationInitiated, onError, fileOptions, realtimeSend, isRealtimeConnected = false } = options;
|
|
42
42
|
const sendMessage = useSendMessage({ client });
|
|
43
43
|
const { handleInputChange: reportTyping, handleSubmit: stopTyping, stop: forceStopTyping } = useVisitorTypingReporter({
|
|
44
|
-
client,
|
|
44
|
+
client: client ?? null,
|
|
45
45
|
conversationId,
|
|
46
46
|
realtimeSend,
|
|
47
47
|
isRealtimeConnected
|
|
@@ -55,6 +55,9 @@ function useMessageComposer(options) {
|
|
|
55
55
|
files,
|
|
56
56
|
defaultTimelineItems,
|
|
57
57
|
visitorId,
|
|
58
|
+
onConversationInitiated: (newConversationId) => {
|
|
59
|
+
onConversationInitiated?.(newConversationId);
|
|
60
|
+
},
|
|
58
61
|
onSuccess: (resultConversationId, messageId) => {
|
|
59
62
|
onMessageSent?.(resultConversationId, messageId);
|
|
60
63
|
},
|