@cossistant/react 0.0.28 → 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/README.md +1 -1
- package/_virtual/rolldown_runtime.js +9 -23
- package/hooks/index.d.ts +3 -3
- 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 +10 -7
- package/hooks/private/use-grouped-messages.d.ts.map +1 -1
- package/hooks/private/use-grouped-messages.js +44 -11
- package/hooks/private/use-grouped-messages.js.map +1 -1
- 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-seen.js +1 -1
- package/hooks/use-conversation-seen.js.map +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-timeline.d.ts.map +1 -1
- package/hooks/use-conversation-timeline.js +1 -3
- package/hooks/use-conversation-timeline.js.map +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 +3 -3
- package/index.js +1 -1
- package/package.json +5 -3
- package/{conversation.d.ts → packages/types/src/api/conversation.d.ts} +368 -64
- 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/packages/types/src/realtime-events.d.ts +1004 -0
- package/packages/types/src/realtime-events.d.ts.map +1 -0
- package/{schemas3.d.ts → packages/types/src/schemas.d.ts} +95 -19
- 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.d.ts.map +1 -1
- package/primitives/avatar/fallback.js +2 -2
- package/primitives/avatar/fallback.js.map +1 -1
- package/primitives/avatar/image.d.ts +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.d.ts +76 -0
- package/primitives/day-separator.d.ts.map +1 -0
- package/primitives/day-separator.js +111 -0
- package/primitives/day-separator.js.map +1 -0
- package/primitives/index.d.ts +3 -2
- package/primitives/index.js +6 -1
- package/primitives/index.parts.d.ts +2 -1
- package/primitives/index.parts.js +2 -1
- package/primitives/multimodal-input.d.ts +2 -2
- package/primitives/multimodal-input.d.ts.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.d.ts.map +1 -1
- package/primitives/timeline-item-group.js +8 -8
- 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 +6 -1
- 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 +4 -2
- 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 +6 -1
- package/support/components/conversation-timeline.d.ts.map +1 -1
- package/support/components/conversation-timeline.js +22 -2
- 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.d.ts +1 -1
- package/support/text/index.d.ts.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.d.ts.map +1 -1
- package/utils/use-render-element.js +20 -5
- 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 -121
- 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/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 +0 -482
- 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 -673
- package/schemas.d.ts.map +0 -1
- package/schemas2.d.ts +0 -320
- 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 -59
- package/standard-schema.d.ts.map +0 -1
- package/timeline-item.d.ts +0 -227
- package/timeline-item.d.ts.map +0 -1
- package/util.d.ts +0 -41
- 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-timeline.js","names":[],"sources":["../../src/hooks/use-conversation-timeline.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"use-conversation-timeline.js","names":[],"sources":["../../src/hooks/use-conversation-timeline.ts"],"sourcesContent":["import type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useMemo } from \"react\";\n\nimport {\n\tmapTypingEntriesToParticipants,\n\ttype TimelineTypingParticipant,\n} from \"./private/typing\";\nimport { useGroupedMessages } from \"./private/use-grouped-messages\";\nimport { useDebouncedConversationSeen } from \"./use-conversation-seen\";\nimport { useConversationTyping } from \"./use-conversation-typing\";\n\nexport type ConversationTimelineTypingParticipant = TimelineTypingParticipant;\n\nexport type UseConversationTimelineOptions = {\n\tconversationId: string;\n\titems: TimelineItem[];\n\tcurrentVisitorId?: string;\n};\n\nexport type UseConversationTimelineReturn = {\n\tgroupedMessages: ReturnType<typeof useGroupedMessages>;\n\tseenData: ReturnType<typeof useDebouncedConversationSeen>;\n\ttypingEntries: ReturnType<typeof useConversationTyping>;\n\ttypingParticipants: ConversationTimelineTypingParticipant[];\n\tlastVisitorMessageGroupIndex: number;\n};\n\n/**\n * Produces grouped timeline items, seen data and typing state suitable for the\n * conversation detail view.\n */\nexport function useConversationTimeline({\n\tconversationId,\n\titems: timelineItems,\n\tcurrentVisitorId,\n}: UseConversationTimelineOptions): UseConversationTimelineReturn {\n\tconst seenData = useDebouncedConversationSeen(conversationId);\n\tconst typingEntries = useConversationTyping(conversationId, {\n\t\texcludeVisitorId: currentVisitorId ?? null,\n\t});\n\n\tconst groupedMessages = useGroupedMessages({\n\t\titems: timelineItems,\n\t\tseenData,\n\t\tcurrentViewerId: currentVisitorId,\n\t});\n\n\tconst lastVisitorMessageGroupIndex = useMemo(() => {\n\t\tfor (let index = groupedMessages.items.length - 1; index >= 0; index--) {\n\t\t\tconst item = groupedMessages.items[index];\n\n\t\t\tif (!item || item.type !== \"message_group\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst firstMessage = item.items?.[0];\n\t\t\tif (firstMessage?.visitorId === currentVisitorId) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t}\n\n\t\treturn -1;\n\t}, [groupedMessages.items, currentVisitorId]);\n\n\tconst typingParticipants = useMemo(\n\t\t() => mapTypingEntriesToParticipants(typingEntries),\n\t\t[typingEntries]\n\t);\n\n\treturn {\n\t\tgroupedMessages,\n\t\tseenData,\n\t\ttypingEntries,\n\t\ttypingParticipants,\n\t\tlastVisitorMessageGroupIndex,\n\t};\n}\n"],"mappings":";;;;;;;;;;;AA+BA,SAAgB,wBAAwB,EACvC,gBACA,OAAO,eACP,oBACiE;CACjE,MAAM,WAAW,6BAA6B,eAAe;CAC7D,MAAM,gBAAgB,sBAAsB,gBAAgB,EAC3D,kBAAkB,oBAAoB,MACtC,CAAC;CAEF,MAAM,kBAAkB,mBAAmB;EAC1C,OAAO;EACP;EACA,iBAAiB;EACjB,CAAC;CAEF,MAAM,+BAA+B,cAAc;AAClD,OAAK,IAAI,QAAQ,gBAAgB,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS;GACvE,MAAM,OAAO,gBAAgB,MAAM;AAEnC,OAAI,CAAC,QAAQ,KAAK,SAAS,gBAC1B;AAID,QADqB,KAAK,QAAQ,KAChB,cAAc,iBAC/B,QAAO;;AAIT,SAAO;IACL,CAAC,gBAAgB,OAAO,iBAAiB,CAAC;AAO7C,QAAO;EACN;EACA;EACA;EACA,oBAT0B,cACpB,+BAA+B,cAAc,EACnD,CAAC,cAAc,CACf;EAOA;EACA"}
|
|
@@ -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
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-message-composer.js","names":[],"sources":["../../src/hooks/use-message-composer.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport type { AnyRealtimeEvent } from \"@cossistant/types/realtime-events\";\nimport { useCallback, useEffect } from \"react\";\nimport {\n\ttype UseMultimodalInputOptions,\n\tuseMultimodalInput,\n} from \"./private/use-multimodal-input\";\nimport { useVisitorTypingReporter } from \"./private/use-visitor-typing-reporter\";\nimport { useSendMessage } from \"./use-send-message\";\n\nexport type UseMessageComposerOptions = {\n\t/**\n\t * The Cossistant client instance.\n\t */\n\tclient
|
|
1
|
+
{"version":3,"file":"use-message-composer.js","names":[],"sources":["../../src/hooks/use-message-composer.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport type { AnyRealtimeEvent } from \"@cossistant/types/realtime-events\";\nimport { useCallback, useEffect } from \"react\";\nimport {\n\ttype UseMultimodalInputOptions,\n\tuseMultimodalInput,\n} from \"./private/use-multimodal-input\";\nimport { useVisitorTypingReporter } from \"./private/use-visitor-typing-reporter\";\nimport { useSendMessage } from \"./use-send-message\";\n\nexport type UseMessageComposerOptions = {\n\t/**\n\t * The Cossistant client instance.\n\t * Optional - when not provided, the composer will be disabled.\n\t */\n\tclient?: CossistantClient;\n\n\t/**\n\t * Current conversation ID. Can be null if no real conversation exists yet.\n\t * Pass null when showing default timeline items before user sends first message.\n\t */\n\tconversationId: string | null;\n\n\t/**\n\t * Default timeline items to include when creating a new conversation.\n\t */\n\tdefaultTimelineItems?: TimelineItem[];\n\n\t/**\n\t * Visitor ID to associate with messages.\n\t */\n\tvisitorId?: string;\n\n\t/**\n\t * Callback when a message is successfully sent.\n\t * @param conversationId - The conversation ID (may be newly created)\n\t * @param messageId - The sent message ID\n\t */\n\tonMessageSent?: (conversationId: string, messageId: string) => void;\n\n\t/**\n\t * Called immediately after a new conversation is initiated (before API call).\n\t * Use this to immediately switch the UI to the new conversation ID for\n\t * proper optimistic updates display.\n\t */\n\tonConversationInitiated?: (conversationId: string) => void;\n\n\t/**\n\t * Callback when message sending fails.\n\t */\n\tonError?: (error: Error) => void;\n\n\t/**\n\t * File upload options (max size, allowed types, etc.)\n\t */\n\tfileOptions?: Pick<\n\t\tUseMultimodalInputOptions,\n\t\t\"maxFileSize\" | \"maxFiles\" | \"allowedFileTypes\"\n\t>;\n\n\t/**\n\t * Optional WebSocket send function for real-time typing events.\n\t * When provided, typing indicators are sent via WebSocket for better performance.\n\t */\n\trealtimeSend?: ((event: AnyRealtimeEvent) => void) | null;\n\n\t/**\n\t * Whether the WebSocket connection is currently established.\n\t */\n\tisRealtimeConnected?: boolean;\n};\n\nexport type UseMessageComposerReturn = {\n\t// Input state\n\tmessage: string;\n\tfiles: File[];\n\terror: Error | null;\n\n\t// Status\n\tisSubmitting: boolean;\n\tisUploading: boolean;\n\tcanSubmit: boolean;\n\n\t// Actions\n\tsetMessage: (message: string) => void;\n\taddFiles: (files: File[]) => void;\n\tremoveFile: (index: number) => void;\n\tclearFiles: () => void;\n\tsubmit: () => void;\n\treset: () => void;\n};\n\n/**\n * Combines message input, typing indicators, and message sending into\n * a single, cohesive hook for building message composers.\n *\n * This hook:\n * - Manages text input and file attachments via useMultimodalInput\n * - Sends typing indicators while user is composing\n * - Handles message submission with proper error handling\n * - Automatically resets input after successful send\n * - Works with both pending and real conversations\n *\n * @example\n * ```tsx\n * const composer = useMessageComposer({\n * client,\n * conversationId: realConversationId, // null if pending\n * defaultMessages,\n * visitorId: visitor?.id,\n * onMessageSent: (convId) => {\n * // Update conversation ID if it was created\n * },\n * });\n *\n * return (\n * <MessageInput\n * value={composer.message}\n * onChange={composer.setMessage}\n * onSubmit={composer.submit}\n * disabled={composer.isSubmitting}\n * />\n * );\n * ```\n */\nexport function useMessageComposer(\n\toptions: UseMessageComposerOptions\n): UseMessageComposerReturn {\n\tconst {\n\t\tclient,\n\t\tconversationId,\n\t\tdefaultTimelineItems = [],\n\t\tvisitorId,\n\t\tonMessageSent,\n\t\tonConversationInitiated,\n\t\tonError,\n\t\tfileOptions,\n\t\trealtimeSend,\n\t\tisRealtimeConnected = false,\n\t} = options;\n\n\tconst sendMessage = useSendMessage({ client });\n\n\tconst {\n\t\thandleInputChange: reportTyping,\n\t\thandleSubmit: stopTyping,\n\t\tstop: forceStopTyping,\n\t} = useVisitorTypingReporter({\n\t\tclient: client ?? null,\n\t\tconversationId,\n\t\trealtimeSend,\n\t\tisRealtimeConnected,\n\t});\n\n\tconst multimodalInput = useMultimodalInput({\n\t\tonSubmit: async ({ message: messageText, files }) => {\n\t\t\t// Stop typing indicator\n\t\t\tstopTyping();\n\n\t\t\t// Send the message\n\t\t\tsendMessage.mutate({\n\t\t\t\tconversationId,\n\t\t\t\tmessage: messageText,\n\t\t\t\tfiles,\n\t\t\t\tdefaultTimelineItems,\n\t\t\t\tvisitorId,\n\t\t\t\tonConversationInitiated: (newConversationId) => {\n\t\t\t\t\t// Immediately switch to new conversation ID for optimistic updates\n\t\t\t\t\tonConversationInitiated?.(newConversationId);\n\t\t\t\t},\n\t\t\t\tonSuccess: (resultConversationId, messageId) => {\n\t\t\t\t\tonMessageSent?.(resultConversationId, messageId);\n\t\t\t\t},\n\t\t\t\tonError: (err) => {\n\t\t\t\t\tonError?.(err);\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t\tonError,\n\t\t...fileOptions,\n\t});\n\n\t// Clean up typing indicator on unmount\n\tuseEffect(\n\t\t() => () => {\n\t\t\tforceStopTyping();\n\t\t},\n\t\t[forceStopTyping]\n\t);\n\n\t// Wrap setMessage to also report typing\n\tconst setMessage = useCallback(\n\t\t(value: string) => {\n\t\t\tmultimodalInput.setMessage(value);\n\t\t\treportTyping(value);\n\t\t},\n\t\t[multimodalInput, reportTyping]\n\t);\n\n\t// Combine submission states\n\tconst isSubmitting = multimodalInput.isSubmitting || sendMessage.isPending;\n\tconst isUploading = sendMessage.isUploading;\n\tconst error = multimodalInput.error || sendMessage.error;\n\tconst canSubmit =\n\t\tmultimodalInput.canSubmit && !sendMessage.isPending && !isUploading;\n\n\treturn {\n\t\tmessage: multimodalInput.message,\n\t\tfiles: multimodalInput.files,\n\t\terror,\n\t\tisSubmitting,\n\t\tisUploading,\n\t\tcanSubmit,\n\t\tsetMessage,\n\t\taddFiles: multimodalInput.addFiles,\n\t\tremoveFile: multimodalInput.removeFile,\n\t\tclearFiles: multimodalInput.clearFiles,\n\t\tsubmit: multimodalInput.submit,\n\t\treset: multimodalInput.reset,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8HA,SAAgB,mBACf,SAC2B;CAC3B,MAAM,EACL,QACA,gBACA,uBAAuB,EAAE,EACzB,WACA,eACA,yBACA,SACA,aACA,cACA,sBAAsB,UACnB;CAEJ,MAAM,cAAc,eAAe,EAAE,QAAQ,CAAC;CAE9C,MAAM,EACL,mBAAmB,cACnB,cAAc,YACd,MAAM,oBACH,yBAAyB;EAC5B,QAAQ,UAAU;EAClB;EACA;EACA;EACA,CAAC;CAEF,MAAM,kBAAkB,mBAAmB;EAC1C,UAAU,OAAO,EAAE,SAAS,aAAa,YAAY;AAEpD,eAAY;AAGZ,eAAY,OAAO;IAClB;IACA,SAAS;IACT;IACA;IACA;IACA,0BAA0B,sBAAsB;AAE/C,+BAA0B,kBAAkB;;IAE7C,YAAY,sBAAsB,cAAc;AAC/C,qBAAgB,sBAAsB,UAAU;;IAEjD,UAAU,QAAQ;AACjB,eAAU,IAAI;;IAEf,CAAC;;EAEH;EACA,GAAG;EACH,CAAC;AAGF,uBACa;AACX,mBAAiB;IAElB,CAAC,gBAAgB,CACjB;CAGD,MAAM,aAAa,aACjB,UAAkB;AAClB,kBAAgB,WAAW,MAAM;AACjC,eAAa,MAAM;IAEpB,CAAC,iBAAiB,aAAa,CAC/B;CAGD,MAAM,eAAe,gBAAgB,gBAAgB,YAAY;CACjE,MAAM,cAAc,YAAY;CAChC,MAAM,QAAQ,gBAAgB,SAAS,YAAY;CACnD,MAAM,YACL,gBAAgB,aAAa,CAAC,YAAY,aAAa,CAAC;AAEzD,QAAO;EACN,SAAS,gBAAgB;EACzB,OAAO,gBAAgB;EACvB;EACA;EACA;EACA;EACA;EACA,UAAU,gBAAgB;EAC1B,YAAY,gBAAgB;EAC5B,YAAY,gBAAgB;EAC5B,QAAQ,gBAAgB;EACxB,OAAO,gBAAgB;EACvB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { TimelineItem } from "../timeline-item.js";
|
|
2
|
-
import { CreateConversationResponseBody } from "../conversation.js";
|
|
1
|
+
import { TimelineItem } from "../packages/types/src/api/timeline-item.js";
|
|
2
|
+
import { CreateConversationResponseBody } from "../packages/types/src/api/conversation.js";
|
|
3
3
|
import { CossistantClient } from "@cossistant/core";
|
|
4
4
|
|
|
5
5
|
//#region src/hooks/use-send-message.d.ts
|
|
@@ -16,6 +16,12 @@ type SendMessageOptions = {
|
|
|
16
16
|
messageId?: string;
|
|
17
17
|
onSuccess?: (conversationId: string, messageId: string) => void;
|
|
18
18
|
onError?: (error: Error) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Called immediately after a new conversation is initiated (before API call).
|
|
21
|
+
* Use this to immediately switch the UI to the new conversation ID for
|
|
22
|
+
* proper optimistic updates display.
|
|
23
|
+
*/
|
|
24
|
+
onConversationInitiated?: (conversationId: string) => void;
|
|
19
25
|
};
|
|
20
26
|
type SendMessageResult = {
|
|
21
27
|
conversationId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-send-message.d.ts","names":[],"sources":["../../src/hooks/use-send-message.ts"],"sourcesContent":[],"mappings":";;;;;KAiBY,kBAAA;;EAAA,OAAA,EAAA,MAAA;EAGH,KAAA,CAAA,EAAA,IAAA,EAAA;EACe,oBAAA,CAAA,EAAA,YAAA,EAAA;EAQL,SAAA,CAAA,EAAA,MAAA;EAAK;
|
|
1
|
+
{"version":3,"file":"use-send-message.d.ts","names":[],"sources":["../../src/hooks/use-send-message.ts"],"sourcesContent":[],"mappings":";;;;;KAiBY,kBAAA;;EAAA,OAAA,EAAA,MAAA;EAGH,KAAA,CAAA,EAAA,IAAA,EAAA;EACe,oBAAA,CAAA,EAAA,YAAA,EAAA;EAQL,SAAA,CAAA,EAAA,MAAA;EAAK;AASxB;AAOA;;EAGW,SAAA,CAAA,EAAA,MAAA;EACG,SAAA,CAAA,EAAA,CAAA,cAAA,EAAA,MAAA,EAAA,SAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAAR,OAAA,CAAA,EAAA,CAAA,KAAA,EApBa,KAoBb,EAAA,GAAA,IAAA;EAGE;;AAIR;AAoHA;;;;KAtIY,iBAAA;;;iBAGI;yBACQ;;KAGZ,oBAAA;oBACO;yBAER,uBACL,QAAQ;;;SAGN;;;KAII,qBAAA;WACF;;;;;;iBAmHM,cAAA,WACN,wBACP"}
|
|
@@ -49,14 +49,14 @@ async function uploadFilesForMessage(client, files, conversationId) {
|
|
|
49
49
|
type: "image",
|
|
50
50
|
url: uploadInfo.publicUrl,
|
|
51
51
|
mediaType: file.type,
|
|
52
|
-
|
|
52
|
+
filename: file.name,
|
|
53
53
|
size: file.size
|
|
54
54
|
};
|
|
55
55
|
return {
|
|
56
56
|
type: "file",
|
|
57
57
|
url: uploadInfo.publicUrl,
|
|
58
58
|
mediaType: file.type,
|
|
59
|
-
|
|
59
|
+
filename: file.name,
|
|
60
60
|
size: file.size
|
|
61
61
|
};
|
|
62
62
|
});
|
|
@@ -73,7 +73,7 @@ function useSendMessage(options = {}) {
|
|
|
73
73
|
const [isUploading, setIsUploading] = useState(false);
|
|
74
74
|
const [error, setError] = useState(null);
|
|
75
75
|
const mutateAsync = useCallback(async (payload) => {
|
|
76
|
-
const { conversationId: providedConversationId, message, files = [], defaultTimelineItems = [], visitorId, messageId: providedMessageId, onSuccess, onError } = payload;
|
|
76
|
+
const { conversationId: providedConversationId, message, files = [], defaultTimelineItems = [], visitorId, messageId: providedMessageId, onSuccess, onError, onConversationInitiated } = payload;
|
|
77
77
|
if (!message.trim() && files.length === 0) {
|
|
78
78
|
const emptyMessageError = /* @__PURE__ */ new Error("Message cannot be empty (or attach files)");
|
|
79
79
|
setError(emptyMessageError);
|
|
@@ -83,6 +83,7 @@ function useSendMessage(options = {}) {
|
|
|
83
83
|
setIsPending(true);
|
|
84
84
|
setError(null);
|
|
85
85
|
try {
|
|
86
|
+
if (!client) throw new Error("Cossistant client is not available. Please ensure you have configured your API key.");
|
|
86
87
|
let conversationId = providedConversationId ?? void 0;
|
|
87
88
|
let preparedDefaultTimelineItems = defaultTimelineItems;
|
|
88
89
|
let initialConversation;
|
|
@@ -94,6 +95,7 @@ function useSendMessage(options = {}) {
|
|
|
94
95
|
conversationId = initiated.conversationId;
|
|
95
96
|
preparedDefaultTimelineItems = initiated.defaultTimelineItems;
|
|
96
97
|
initialConversation = initiated.conversation;
|
|
98
|
+
onConversationInitiated?.(conversationId);
|
|
97
99
|
}
|
|
98
100
|
let fileParts = [];
|
|
99
101
|
if (files.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-send-message.js","names":["parts: TimelineItemParts","initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined","fileParts: Array<TimelinePartImage | TimelinePartFile>","result: SendMessageResult"],"sources":["../../src/hooks/use-send-message.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport {\n\tgenerateMessageId,\n\tisImageMimeType,\n\tvalidateFiles,\n} from \"@cossistant/core\";\nimport type { CreateConversationResponseBody } from \"@cossistant/types/api/conversation\";\nimport type {\n\tTimelineItem,\n\tTimelineItemParts,\n\tTimelinePartFile,\n\tTimelinePartImage,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useState } from \"react\";\n\nimport { useSupport } from \"../provider\";\n\nexport type SendMessageOptions = {\n\tconversationId?: string | null;\n\tmessage: string;\n\tfiles?: File[];\n\tdefaultTimelineItems?: TimelineItem[];\n\tvisitorId?: string;\n\t/**\n\t * Optional message ID to use for the optimistic update and API request.\n\t * When not provided, a ULID will be generated on the client.\n\t */\n\tmessageId?: string;\n\tonSuccess?: (conversationId: string, messageId: string) => void;\n\tonError?: (error: Error) => void;\n};\n\nexport type SendMessageResult = {\n\tconversationId: string;\n\tmessageId: string;\n\tconversation?: CreateConversationResponseBody[\"conversation\"];\n\tinitialTimelineItems?: CreateConversationResponseBody[\"initialTimelineItems\"];\n};\n\nexport type UseSendMessageResult = {\n\tmutate: (options: SendMessageOptions) => void;\n\tmutateAsync: (\n\t\toptions: SendMessageOptions\n\t) => Promise<SendMessageResult | null>;\n\tisPending: boolean;\n\tisUploading: boolean;\n\terror: Error | null;\n\treset: () => void;\n};\n\nexport type UseSendMessageOptions = {\n\tclient?: CossistantClient;\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\ntype BuildTimelineItemPayloadOptions = {\n\tbody: string;\n\tconversationId: string;\n\tvisitorId: string | null;\n\tmessageId?: string;\n\tfileParts?: Array<TimelinePartImage | TimelinePartFile>;\n};\n\nfunction buildTimelineItemPayload({\n\tbody,\n\tconversationId,\n\tvisitorId,\n\tmessageId,\n\tfileParts,\n}: BuildTimelineItemPayloadOptions): TimelineItem {\n\tconst nowIso = typeof window !== \"undefined\" ? new Date().toISOString() : \"\";\n\tconst id = messageId ?? generateMessageId();\n\n\t// Build parts array: text first, then any file/image parts\n\tconst parts: TimelineItemParts = [{ type: \"text\" as const, text: body }];\n\n\tif (fileParts && fileParts.length > 0) {\n\t\tparts.push(...fileParts);\n\t}\n\n\treturn {\n\t\tid,\n\t\tconversationId,\n\t\torganizationId: \"\", // Will be set by backend\n\t\ttype: \"message\" as const,\n\t\ttext: body,\n\t\tparts,\n\t\tvisibility: \"public\" as const,\n\t\tuserId: null,\n\t\taiAgentId: null,\n\t\tvisitorId: visitorId ?? null,\n\t\tcreatedAt: nowIso,\n\t\tdeletedAt: null,\n\t} satisfies TimelineItem;\n}\n\n/**\n * Upload files and return timeline parts for inclusion in a message.\n */\nasync function uploadFilesForMessage(\n\tclient: CossistantClient,\n\tfiles: File[],\n\tconversationId: string\n): Promise<Array<TimelinePartImage | TimelinePartFile>> {\n\tif (files.length === 0) {\n\t\treturn [];\n\t}\n\n\t// Validate files first\n\tconst validationError = validateFiles(files);\n\tif (validationError) {\n\t\tthrow new Error(validationError);\n\t}\n\n\t// Upload files in parallel\n\tconst uploadPromises = files.map(async (file) => {\n\t\t// Generate presigned URL\n\t\tconst uploadInfo = await client.generateUploadUrl({\n\t\t\tconversationId,\n\t\t\tcontentType: file.type,\n\t\t\tfileName: file.name,\n\t\t});\n\n\t\t// Upload file to S3\n\t\tawait client.uploadFile(file, uploadInfo.uploadUrl, file.type);\n\n\t\t// Return timeline part based on file type\n\t\tconst isImage = isImageMimeType(file.type);\n\n\t\tif (isImage) {\n\t\t\treturn {\n\t\t\t\ttype: \"image\" as const,\n\t\t\t\turl: uploadInfo.publicUrl,\n\t\t\t\tmediaType: file.type,\n\t\t\t\tfileName: file.name,\n\t\t\t\tsize: file.size,\n\t\t\t} satisfies TimelinePartImage;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"file\" as const,\n\t\t\turl: uploadInfo.publicUrl,\n\t\t\tmediaType: file.type,\n\t\t\tfileName: file.name,\n\t\t\tsize: file.size,\n\t\t} satisfies TimelinePartFile;\n\t});\n\n\treturn Promise.all(uploadPromises);\n}\n\n/**\n * Sends visitor messages while handling optimistic pending conversations and\n * exposing react-query-like mutation state.\n */\nexport function useSendMessage(\n\toptions: UseSendMessageOptions = {}\n): UseSendMessageResult {\n\tconst { client: contextClient } = useSupport();\n\tconst client = options.client ?? contextClient;\n\n\tconst [isPending, setIsPending] = useState(false);\n\tconst [isUploading, setIsUploading] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst mutateAsync = useCallback(\n\t\tasync (payload: SendMessageOptions): Promise<SendMessageResult | null> => {\n\t\t\tconst {\n\t\t\t\tconversationId: providedConversationId,\n\t\t\t\tmessage,\n\t\t\t\tfiles = [],\n\t\t\t\tdefaultTimelineItems = [],\n\t\t\t\tvisitorId,\n\t\t\t\tmessageId: providedMessageId,\n\t\t\t\tonSuccess,\n\t\t\t\tonError,\n\t\t\t} = payload;\n\n\t\t\t// Allow empty message if there are files\n\t\t\tif (!message.trim() && files.length === 0) {\n\t\t\t\tconst emptyMessageError = new Error(\n\t\t\t\t\t\"Message cannot be empty (or attach files)\"\n\t\t\t\t);\n\t\t\t\tsetError(emptyMessageError);\n\t\t\t\tonError?.(emptyMessageError);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tsetIsPending(true);\n\t\t\tsetError(null);\n\n\t\t\ttry {\n\t\t\t\tlet conversationId = providedConversationId ?? undefined;\n\t\t\t\tlet preparedDefaultTimelineItems = defaultTimelineItems;\n\t\t\t\tlet initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined;\n\n\t\t\t\tif (!conversationId) {\n\t\t\t\t\tconst initiated = client.initiateConversation({\n\t\t\t\t\t\tdefaultTimelineItems,\n\t\t\t\t\t\tvisitorId: visitorId ?? undefined,\n\t\t\t\t\t});\n\t\t\t\t\tconversationId = initiated.conversationId;\n\t\t\t\t\tpreparedDefaultTimelineItems = initiated.defaultTimelineItems;\n\t\t\t\t\tinitialConversation = initiated.conversation;\n\t\t\t\t}\n\n\t\t\t\t// Upload files BEFORE sending the message\n\t\t\t\tlet fileParts: Array<TimelinePartImage | TimelinePartFile> = [];\n\t\t\t\tif (files.length > 0) {\n\t\t\t\t\tsetIsUploading(true);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfileParts = await uploadFilesForMessage(\n\t\t\t\t\t\t\tclient,\n\t\t\t\t\t\t\tfiles,\n\t\t\t\t\t\t\tconversationId\n\t\t\t\t\t\t);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tsetIsUploading(false);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst timelineItemPayload = buildTimelineItemPayload({\n\t\t\t\t\tbody: message,\n\t\t\t\t\tconversationId,\n\t\t\t\t\tvisitorId: visitorId ?? null,\n\t\t\t\t\tmessageId: providedMessageId,\n\t\t\t\t\tfileParts,\n\t\t\t\t});\n\n\t\t\t\tconst response = await client.sendMessage({\n\t\t\t\t\tconversationId,\n\t\t\t\t\titem: {\n\t\t\t\t\t\tid: timelineItemPayload.id,\n\t\t\t\t\t\ttext: timelineItemPayload.text ?? \"\",\n\t\t\t\t\t\ttype:\n\t\t\t\t\t\t\ttimelineItemPayload.type === \"identification\"\n\t\t\t\t\t\t\t\t? \"message\"\n\t\t\t\t\t\t\t\t: timelineItemPayload.type,\n\t\t\t\t\t\tvisibility: timelineItemPayload.visibility,\n\t\t\t\t\t\tuserId: timelineItemPayload.userId,\n\t\t\t\t\t\taiAgentId: timelineItemPayload.aiAgentId,\n\t\t\t\t\t\tvisitorId: timelineItemPayload.visitorId,\n\t\t\t\t\t\tcreatedAt: timelineItemPayload.createdAt,\n\t\t\t\t\t\tparts: timelineItemPayload.parts,\n\t\t\t\t\t},\n\t\t\t\t\tcreateIfPending: true,\n\t\t\t\t});\n\n\t\t\t\tconst messageId = response.item.id;\n\n\t\t\t\tif (!messageId) {\n\t\t\t\t\tthrow new Error(\"SendMessage response missing item.id\");\n\t\t\t\t}\n\n\t\t\t\tconst result: SendMessageResult = {\n\t\t\t\t\tconversationId,\n\t\t\t\t\tmessageId,\n\t\t\t\t};\n\n\t\t\t\tif (\"conversation\" in response && response.conversation) {\n\t\t\t\t\tresult.conversation = response.conversation;\n\t\t\t\t\tresult.initialTimelineItems = response.initialTimelineItems;\n\t\t\t\t} else if (initialConversation) {\n\t\t\t\t\tresult.conversation = initialConversation;\n\t\t\t\t\tresult.initialTimelineItems = preparedDefaultTimelineItems;\n\t\t\t\t}\n\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(null);\n\t\t\t\tonSuccess?.(result.conversationId, result.messageId);\n\t\t\t\treturn result;\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]\n\t);\n\n\tconst mutate = useCallback(\n\t\t(opts: SendMessageOptions) => {\n\t\t\tvoid mutateAsync(opts).catch(() => {\n\t\t\t\t// Swallow errors to mimic react-query behaviour for mutate\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\tsetIsUploading(false);\n\t}, []);\n\n\treturn {\n\t\tmutate,\n\t\tmutateAsync,\n\t\tisPending,\n\t\tisUploading,\n\t\terror,\n\t\treset,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-send-message.js","names":["parts: TimelineItemParts","initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined","fileParts: Array<TimelinePartImage | TimelinePartFile>","result: SendMessageResult"],"sources":["../../src/hooks/use-send-message.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport {\n\tgenerateMessageId,\n\tisImageMimeType,\n\tvalidateFiles,\n} from \"@cossistant/core\";\nimport type { CreateConversationResponseBody } from \"@cossistant/types/api/conversation\";\nimport type {\n\tTimelineItem,\n\tTimelineItemParts,\n\tTimelinePartFile,\n\tTimelinePartImage,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useState } from \"react\";\n\nimport { useSupport } from \"../provider\";\n\nexport type SendMessageOptions = {\n\tconversationId?: string | null;\n\tmessage: string;\n\tfiles?: File[];\n\tdefaultTimelineItems?: TimelineItem[];\n\tvisitorId?: string;\n\t/**\n\t * Optional message ID to use for the optimistic update and API request.\n\t * When not provided, a ULID will be generated on the client.\n\t */\n\tmessageId?: string;\n\tonSuccess?: (conversationId: string, messageId: string) => void;\n\tonError?: (error: Error) => void;\n\t/**\n\t * Called immediately after a new conversation is initiated (before API call).\n\t * Use this to immediately switch the UI to the new conversation ID for\n\t * proper optimistic updates display.\n\t */\n\tonConversationInitiated?: (conversationId: string) => void;\n};\n\nexport type SendMessageResult = {\n\tconversationId: string;\n\tmessageId: string;\n\tconversation?: CreateConversationResponseBody[\"conversation\"];\n\tinitialTimelineItems?: CreateConversationResponseBody[\"initialTimelineItems\"];\n};\n\nexport type UseSendMessageResult = {\n\tmutate: (options: SendMessageOptions) => void;\n\tmutateAsync: (\n\t\toptions: SendMessageOptions\n\t) => Promise<SendMessageResult | null>;\n\tisPending: boolean;\n\tisUploading: boolean;\n\terror: Error | null;\n\treset: () => void;\n};\n\nexport type UseSendMessageOptions = {\n\tclient?: CossistantClient;\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\ntype BuildTimelineItemPayloadOptions = {\n\tbody: string;\n\tconversationId: string;\n\tvisitorId: string | null;\n\tmessageId?: string;\n\tfileParts?: Array<TimelinePartImage | TimelinePartFile>;\n};\n\nfunction buildTimelineItemPayload({\n\tbody,\n\tconversationId,\n\tvisitorId,\n\tmessageId,\n\tfileParts,\n}: BuildTimelineItemPayloadOptions): TimelineItem {\n\tconst nowIso = typeof window !== \"undefined\" ? new Date().toISOString() : \"\";\n\tconst id = messageId ?? generateMessageId();\n\n\t// Build parts array: text first, then any file/image parts\n\tconst parts: TimelineItemParts = [{ type: \"text\" as const, text: body }];\n\n\tif (fileParts && fileParts.length > 0) {\n\t\tparts.push(...fileParts);\n\t}\n\n\treturn {\n\t\tid,\n\t\tconversationId,\n\t\torganizationId: \"\", // Will be set by backend\n\t\ttype: \"message\" as const,\n\t\ttext: body,\n\t\tparts,\n\t\tvisibility: \"public\" as const,\n\t\tuserId: null,\n\t\taiAgentId: null,\n\t\tvisitorId: visitorId ?? null,\n\t\tcreatedAt: nowIso,\n\t\tdeletedAt: null,\n\t} satisfies TimelineItem;\n}\n\n/**\n * Upload files and return timeline parts for inclusion in a message.\n */\nasync function uploadFilesForMessage(\n\tclient: CossistantClient,\n\tfiles: File[],\n\tconversationId: string\n): Promise<Array<TimelinePartImage | TimelinePartFile>> {\n\tif (files.length === 0) {\n\t\treturn [];\n\t}\n\n\t// Validate files first\n\tconst validationError = validateFiles(files);\n\tif (validationError) {\n\t\tthrow new Error(validationError);\n\t}\n\n\t// Upload files in parallel\n\tconst uploadPromises = files.map(async (file) => {\n\t\t// Generate presigned URL\n\t\tconst uploadInfo = await client.generateUploadUrl({\n\t\t\tconversationId,\n\t\t\tcontentType: file.type,\n\t\t\tfileName: file.name,\n\t\t});\n\n\t\t// Upload file to S3\n\t\tawait client.uploadFile(file, uploadInfo.uploadUrl, file.type);\n\n\t\t// Return timeline part based on file type\n\t\tconst isImage = isImageMimeType(file.type);\n\n\t\tif (isImage) {\n\t\t\treturn {\n\t\t\t\ttype: \"image\" as const,\n\t\t\t\turl: uploadInfo.publicUrl,\n\t\t\t\tmediaType: file.type,\n\t\t\t\tfilename: file.name,\n\t\t\t\tsize: file.size,\n\t\t\t} satisfies TimelinePartImage;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"file\" as const,\n\t\t\turl: uploadInfo.publicUrl,\n\t\t\tmediaType: file.type,\n\t\t\tfilename: file.name,\n\t\t\tsize: file.size,\n\t\t} satisfies TimelinePartFile;\n\t});\n\n\treturn Promise.all(uploadPromises);\n}\n\n/**\n * Sends visitor messages while handling optimistic pending conversations and\n * exposing react-query-like mutation state.\n */\nexport function useSendMessage(\n\toptions: UseSendMessageOptions = {}\n): UseSendMessageResult {\n\tconst { client: contextClient } = useSupport();\n\tconst client = options.client ?? contextClient;\n\n\tconst [isPending, setIsPending] = useState(false);\n\tconst [isUploading, setIsUploading] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst mutateAsync = useCallback(\n\t\tasync (payload: SendMessageOptions): Promise<SendMessageResult | null> => {\n\t\t\tconst {\n\t\t\t\tconversationId: providedConversationId,\n\t\t\t\tmessage,\n\t\t\t\tfiles = [],\n\t\t\t\tdefaultTimelineItems = [],\n\t\t\t\tvisitorId,\n\t\t\t\tmessageId: providedMessageId,\n\t\t\t\tonSuccess,\n\t\t\t\tonError,\n\t\t\t\tonConversationInitiated,\n\t\t\t} = payload;\n\n\t\t\t// Allow empty message if there are files\n\t\t\tif (!message.trim() && files.length === 0) {\n\t\t\t\tconst emptyMessageError = new Error(\n\t\t\t\t\t\"Message cannot be empty (or attach files)\"\n\t\t\t\t);\n\t\t\t\tsetError(emptyMessageError);\n\t\t\t\tonError?.(emptyMessageError);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tsetIsPending(true);\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\tlet conversationId = providedConversationId ?? undefined;\n\t\t\t\tlet preparedDefaultTimelineItems = defaultTimelineItems;\n\t\t\t\tlet initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined;\n\n\t\t\t\tif (!conversationId) {\n\t\t\t\t\tconst initiated = client.initiateConversation({\n\t\t\t\t\t\tdefaultTimelineItems,\n\t\t\t\t\t\tvisitorId: visitorId ?? undefined,\n\t\t\t\t\t});\n\t\t\t\t\tconversationId = initiated.conversationId;\n\t\t\t\t\tpreparedDefaultTimelineItems = initiated.defaultTimelineItems;\n\t\t\t\t\tinitialConversation = initiated.conversation;\n\t\t\t\t\t// Immediately notify about the new conversation ID so UI can switch\n\t\t\t\t\t// to reading from the right store key for optimistic updates\n\t\t\t\t\tonConversationInitiated?.(conversationId);\n\t\t\t\t}\n\n\t\t\t\t// Upload files BEFORE sending the message\n\t\t\t\tlet fileParts: Array<TimelinePartImage | TimelinePartFile> = [];\n\t\t\t\tif (files.length > 0) {\n\t\t\t\t\tsetIsUploading(true);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfileParts = await uploadFilesForMessage(\n\t\t\t\t\t\t\tclient,\n\t\t\t\t\t\t\tfiles,\n\t\t\t\t\t\t\tconversationId\n\t\t\t\t\t\t);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tsetIsUploading(false);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst timelineItemPayload = buildTimelineItemPayload({\n\t\t\t\t\tbody: message,\n\t\t\t\t\tconversationId,\n\t\t\t\t\tvisitorId: visitorId ?? null,\n\t\t\t\t\tmessageId: providedMessageId,\n\t\t\t\t\tfileParts,\n\t\t\t\t});\n\n\t\t\t\tconst response = await client.sendMessage({\n\t\t\t\t\tconversationId,\n\t\t\t\t\titem: {\n\t\t\t\t\t\tid: timelineItemPayload.id,\n\t\t\t\t\t\ttext: timelineItemPayload.text ?? \"\",\n\t\t\t\t\t\ttype:\n\t\t\t\t\t\t\ttimelineItemPayload.type === \"identification\"\n\t\t\t\t\t\t\t\t? \"message\"\n\t\t\t\t\t\t\t\t: timelineItemPayload.type,\n\t\t\t\t\t\tvisibility: timelineItemPayload.visibility,\n\t\t\t\t\t\tuserId: timelineItemPayload.userId,\n\t\t\t\t\t\taiAgentId: timelineItemPayload.aiAgentId,\n\t\t\t\t\t\tvisitorId: timelineItemPayload.visitorId,\n\t\t\t\t\t\tcreatedAt: timelineItemPayload.createdAt,\n\t\t\t\t\t\tparts: timelineItemPayload.parts,\n\t\t\t\t\t},\n\t\t\t\t\tcreateIfPending: true,\n\t\t\t\t});\n\n\t\t\t\tconst messageId = response.item.id;\n\n\t\t\t\tif (!messageId) {\n\t\t\t\t\tthrow new Error(\"SendMessage response missing item.id\");\n\t\t\t\t}\n\n\t\t\t\tconst result: SendMessageResult = {\n\t\t\t\t\tconversationId,\n\t\t\t\t\tmessageId,\n\t\t\t\t};\n\n\t\t\t\tif (\"conversation\" in response && response.conversation) {\n\t\t\t\t\tresult.conversation = response.conversation;\n\t\t\t\t\tresult.initialTimelineItems = response.initialTimelineItems;\n\t\t\t\t} else if (initialConversation) {\n\t\t\t\t\tresult.conversation = initialConversation;\n\t\t\t\t\tresult.initialTimelineItems = preparedDefaultTimelineItems;\n\t\t\t\t}\n\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(null);\n\t\t\t\tonSuccess?.(result.conversationId, result.messageId);\n\t\t\t\treturn result;\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]\n\t);\n\n\tconst mutate = useCallback(\n\t\t(opts: SendMessageOptions) => {\n\t\t\tvoid mutateAsync(opts).catch(() => {\n\t\t\t\t// Swallow errors to mimic react-query behaviour for mutate\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\tsetIsUploading(false);\n\t}, []);\n\n\treturn {\n\t\tmutate,\n\t\tmutateAsync,\n\t\tisPending,\n\t\tisUploading,\n\t\terror,\n\t\treset,\n\t};\n}\n"],"mappings":"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"}
|
package/hooks/use-visitor.js
CHANGED
|
@@ -22,7 +22,7 @@ function useVisitor() {
|
|
|
22
22
|
return {
|
|
23
23
|
visitor,
|
|
24
24
|
setVisitorMetadata: useCallback(async (metadata) => {
|
|
25
|
-
if (!visitorId) {
|
|
25
|
+
if (!(visitorId && client)) {
|
|
26
26
|
safeWarn("No visitor is associated with this session; metadata update skipped");
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
@@ -34,7 +34,7 @@ function useVisitor() {
|
|
|
34
34
|
}
|
|
35
35
|
}, [client, visitorId]),
|
|
36
36
|
identify: useCallback(async (params) => {
|
|
37
|
-
if (!visitorId) {
|
|
37
|
+
if (!(visitorId && client)) {
|
|
38
38
|
safeWarn("No visitor is associated with this session; identify skipped");
|
|
39
39
|
return null;
|
|
40
40
|
}
|
package/hooks/use-visitor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-visitor.js","names":[],"sources":["../../src/hooks/use-visitor.ts"],"sourcesContent":["import type {\n\tPublicVisitor,\n\tVisitorMetadata,\n\tVisitorResponse,\n} from \"@cossistant/types\";\nimport { useCallback } from \"react\";\nimport { useSupport } from \"../provider\";\n\nexport type UseVisitorReturn = {\n\tvisitor: PublicVisitor | null;\n\tsetVisitorMetadata: (\n\t\tmetadata: VisitorMetadata\n\t) => Promise<VisitorResponse | null>;\n\tidentify: (params: {\n\t\texternalId?: string;\n\t\temail?: string;\n\t\tname?: string;\n\t\timage?: string;\n\t\tmetadata?: Record<string, unknown>;\n\t}) => Promise<{ contactId: string; visitorId: string } | null>;\n};\n\nfunction safeWarn(message: string): void {\n\tif (typeof console !== \"undefined\" && typeof console.warn === \"function\") {\n\t\tconsole.warn(message);\n\t}\n}\n\nfunction safeError(message: string, error: unknown): void {\n\tif (typeof console !== \"undefined\" && typeof console.error === \"function\") {\n\t\tconsole.error(message, error);\n\t}\n}\n\n/**\n * Exposes the current visitor plus helpers to identify and update metadata.\n *\n * Note: Metadata is stored on contacts, not visitors. When you call\n * setVisitorMetadata, it will update the contact metadata if the visitor\n * has been identified. If not, you must call identify() first.\n */\nexport function useVisitor(): UseVisitorReturn {\n\tconst { website, client } = useSupport();\n\tconst visitor = website?.visitor || null;\n\tconst visitorId = visitor?.id ?? null;\n\n\tconst setVisitorMetadata = useCallback<\n\t\t(metadata: VisitorMetadata) => Promise<VisitorResponse | null>\n\t>(\n\t\tasync (metadata) => {\n\t\t\tif (!visitorId) {\n\t\t\t\tsafeWarn(\n\t\t\t\t\t\"No visitor is associated with this session; metadata update skipped\"\n\t\t\t\t);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn await client.updateVisitorMetadata(metadata);\n\t\t\t} catch (error) {\n\t\t\t\tsafeError(\"Failed to update visitor metadata\", error);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\t\t[client, visitorId]\n\t);\n\n\tconst identify = useCallback<\n\t\t(params: {\n\t\t\texternalId?: string;\n\t\t\temail?: string;\n\t\t\tname?: string;\n\t\t\timage?: string;\n\t\t\tmetadata?: Record<string, unknown>;\n\t\t}) => Promise<{ contactId: string; visitorId: string } | null>\n\t>(\n\t\tasync (params) => {\n\t\t\tif (!visitorId) {\n\t\t\t\tsafeWarn(\n\t\t\t\t\t\"No visitor is associated with this session; identify skipped\"\n\t\t\t\t);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst result = await client.identify(params);\n\n\t\t\t\treturn {\n\t\t\t\t\tcontactId: result.contact.id,\n\t\t\t\t\tvisitorId: result.visitorId,\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\tsafeError(\"Failed to identify visitor\", error);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\t\t[client, visitorId]\n\t);\n\n\treturn {\n\t\tvisitor,\n\t\tsetVisitorMetadata,\n\t\tidentify,\n\t};\n}\n"],"mappings":";;;;AAsBA,SAAS,SAAS,SAAuB;AACxC,KAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,SAAS,WAC7D,SAAQ,KAAK,QAAQ;;AAIvB,SAAS,UAAU,SAAiB,OAAsB;AACzD,KAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,UAAU,WAC9D,SAAQ,MAAM,SAAS,MAAM;;;;;;;;;AAW/B,SAAgB,aAA+B;CAC9C,MAAM,EAAE,SAAS,WAAW,YAAY;CACxC,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,YAAY,SAAS,MAAM;AAuDjC,QAAO;EACN;EACA,oBAvD0B,YAG1B,OAAO,aAAa;AACnB,OAAI,
|
|
1
|
+
{"version":3,"file":"use-visitor.js","names":[],"sources":["../../src/hooks/use-visitor.ts"],"sourcesContent":["import type {\n\tPublicVisitor,\n\tVisitorMetadata,\n\tVisitorResponse,\n} from \"@cossistant/types\";\nimport { useCallback } from \"react\";\nimport { useSupport } from \"../provider\";\n\nexport type UseVisitorReturn = {\n\tvisitor: PublicVisitor | null;\n\tsetVisitorMetadata: (\n\t\tmetadata: VisitorMetadata\n\t) => Promise<VisitorResponse | null>;\n\tidentify: (params: {\n\t\texternalId?: string;\n\t\temail?: string;\n\t\tname?: string;\n\t\timage?: string;\n\t\tmetadata?: Record<string, unknown>;\n\t}) => Promise<{ contactId: string; visitorId: string } | null>;\n};\n\nfunction safeWarn(message: string): void {\n\tif (typeof console !== \"undefined\" && typeof console.warn === \"function\") {\n\t\tconsole.warn(message);\n\t}\n}\n\nfunction safeError(message: string, error: unknown): void {\n\tif (typeof console !== \"undefined\" && typeof console.error === \"function\") {\n\t\tconsole.error(message, error);\n\t}\n}\n\n/**\n * Exposes the current visitor plus helpers to identify and update metadata.\n *\n * Note: Metadata is stored on contacts, not visitors. When you call\n * setVisitorMetadata, it will update the contact metadata if the visitor\n * has been identified. If not, you must call identify() first.\n */\nexport function useVisitor(): UseVisitorReturn {\n\tconst { website, client } = useSupport();\n\tconst visitor = website?.visitor || null;\n\tconst visitorId = visitor?.id ?? null;\n\n\tconst setVisitorMetadata = useCallback<\n\t\t(metadata: VisitorMetadata) => Promise<VisitorResponse | null>\n\t>(\n\t\tasync (metadata) => {\n\t\t\tif (!(visitorId && client)) {\n\t\t\t\tsafeWarn(\n\t\t\t\t\t\"No visitor is associated with this session; metadata update skipped\"\n\t\t\t\t);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn await client.updateVisitorMetadata(metadata);\n\t\t\t} catch (error) {\n\t\t\t\tsafeError(\"Failed to update visitor metadata\", error);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\t\t[client, visitorId]\n\t);\n\n\tconst identify = useCallback<\n\t\t(params: {\n\t\t\texternalId?: string;\n\t\t\temail?: string;\n\t\t\tname?: string;\n\t\t\timage?: string;\n\t\t\tmetadata?: Record<string, unknown>;\n\t\t}) => Promise<{ contactId: string; visitorId: string } | null>\n\t>(\n\t\tasync (params) => {\n\t\t\tif (!(visitorId && client)) {\n\t\t\t\tsafeWarn(\n\t\t\t\t\t\"No visitor is associated with this session; identify skipped\"\n\t\t\t\t);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst result = await client.identify(params);\n\n\t\t\t\treturn {\n\t\t\t\t\tcontactId: result.contact.id,\n\t\t\t\t\tvisitorId: result.visitorId,\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\tsafeError(\"Failed to identify visitor\", error);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\t\t[client, visitorId]\n\t);\n\n\treturn {\n\t\tvisitor,\n\t\tsetVisitorMetadata,\n\t\tidentify,\n\t};\n}\n"],"mappings":";;;;AAsBA,SAAS,SAAS,SAAuB;AACxC,KAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,SAAS,WAC7D,SAAQ,KAAK,QAAQ;;AAIvB,SAAS,UAAU,SAAiB,OAAsB;AACzD,KAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,UAAU,WAC9D,SAAQ,MAAM,SAAS,MAAM;;;;;;;;;AAW/B,SAAgB,aAA+B;CAC9C,MAAM,EAAE,SAAS,WAAW,YAAY;CACxC,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,YAAY,SAAS,MAAM;AAuDjC,QAAO;EACN;EACA,oBAvD0B,YAG1B,OAAO,aAAa;AACnB,OAAI,EAAE,aAAa,SAAS;AAC3B,aACC,sEACA;AACD,WAAO;;AAGR,OAAI;AACH,WAAO,MAAM,OAAO,sBAAsB,SAAS;YAC3C,OAAO;AACf,cAAU,qCAAqC,MAAM;AACrD,WAAO;;KAGT,CAAC,QAAQ,UAAU,CACnB;EAqCA,UAnCgB,YAShB,OAAO,WAAW;AACjB,OAAI,EAAE,aAAa,SAAS;AAC3B,aACC,+DACA;AACD,WAAO;;AAGR,OAAI;IACH,MAAM,SAAS,MAAM,OAAO,SAAS,OAAO;AAE5C,WAAO;KACN,WAAW,OAAO,QAAQ;KAC1B,WAAW,OAAO;KAClB;YACO,OAAO;AACf,cAAU,8BAA8B,MAAM;AAC9C,WAAO;;KAGT,CAAC,QAAQ,UAAU,CACnB;EAMA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identify-visitor.d.ts","names":[],"sources":["../src/identify-visitor.tsx"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"identify-visitor.d.ts","names":[],"sources":["../src/identify-visitor.tsx"],"sourcesContent":[],"mappings":";;;;KAQY,2BAAA;EAAA,UAAA,CAAA,EAAA,MAAA;EAWC,KAAA,CAAA,EAAA,MAAA;;;aAND;;;;;AAYiC,cANhC,sBAMgC,EAAA;;;;;;;KAA1C,8BAA8B"}
|