@cossistant/react 0.0.29 → 0.0.31
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 +3 -1
- package/_virtual/rolldown_runtime.js +9 -23
- package/hooks/index.d.ts +2 -2
- package/hooks/private/store/use-conversations-store.d.ts +2 -0
- package/hooks/private/store/use-conversations-store.d.ts.map +1 -1
- package/hooks/private/store/use-conversations-store.js +15 -8
- package/hooks/private/store/use-conversations-store.js.map +1 -1
- package/hooks/private/store/use-store-selector.d.ts +3 -0
- package/hooks/private/store/use-store-selector.d.ts.map +1 -1
- package/hooks/private/store/use-store-selector.js +4 -8
- package/hooks/private/store/use-store-selector.js.map +1 -1
- package/hooks/private/store/use-website-store.d.ts +3 -1
- package/hooks/private/store/use-website-store.d.ts.map +1 -1
- package/hooks/private/store/use-website-store.js +14 -6
- package/hooks/private/store/use-website-store.js.map +1 -1
- package/hooks/private/use-client-query.d.ts +1 -1
- package/hooks/private/use-client-query.d.ts.map +1 -1
- package/hooks/private/use-client-query.js +1 -0
- package/hooks/private/use-client-query.js.map +1 -1
- package/hooks/private/use-default-messages.d.ts +1 -1
- package/hooks/private/use-grouped-messages.d.ts +2 -2
- package/hooks/private/use-grouped-messages.d.ts.map +1 -1
- package/hooks/private/use-grouped-messages.js +34 -10
- 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 +13 -4
- 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 +8 -4
- package/hooks/use-conversation-preview.js.map +1 -1
- package/hooks/use-conversation-seen.d.ts +1 -1
- package/hooks/use-conversation-timeline-items.d.ts +1 -1
- package/hooks/use-conversation-timeline-items.js +2 -3
- package/hooks/use-conversation-timeline-items.js.map +1 -1
- package/hooks/use-conversation-timeline.d.ts +1 -1
- package/hooks/use-conversation.d.ts +1 -1
- package/hooks/use-conversation.js +2 -3
- package/hooks/use-conversation.js.map +1 -1
- package/hooks/use-conversations.d.ts +1 -1
- package/hooks/use-conversations.js +5 -3
- package/hooks/use-conversations.js.map +1 -1
- package/hooks/use-create-conversation.d.ts +3 -3
- package/hooks/use-create-conversation.js +1 -0
- package/hooks/use-create-conversation.js.map +1 -1
- package/hooks/use-file-upload.d.ts +1 -1
- package/hooks/use-file-upload.js +3 -3
- package/hooks/use-file-upload.js.map +1 -1
- package/hooks/use-home-page.js +3 -3
- package/hooks/use-home-page.js.map +1 -1
- package/hooks/use-message-composer.d.ts +10 -3
- package/hooks/use-message-composer.d.ts.map +1 -1
- package/hooks/use-message-composer.js +5 -2
- package/hooks/use-message-composer.js.map +1 -1
- package/hooks/use-realtime-support.d.ts +1 -1
- package/hooks/use-send-message.d.ts +8 -2
- package/hooks/use-send-message.d.ts.map +1 -1
- package/hooks/use-send-message.js +5 -3
- package/hooks/use-send-message.js.map +1 -1
- package/hooks/use-visitor.js +2 -2
- package/hooks/use-visitor.js.map +1 -1
- package/identify-visitor.d.ts.map +1 -1
- package/identify-visitor.js +15 -1
- package/identify-visitor.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/package.json +6 -3
- package/{conversation.d.ts → packages/types/src/api/conversation.d.ts} +374 -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/{realtime-events.d.ts → packages/types/src/realtime-events.d.ts} +449 -47
- package/packages/types/src/realtime-events.d.ts.map +1 -0
- package/{schemas3.d.ts → packages/types/src/schemas.d.ts} +97 -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.js +1 -1
- package/primitives/avatar/fallback.js.map +1 -1
- package/primitives/avatar/image.js +1 -1
- package/primitives/avatar/image.js.map +1 -1
- package/primitives/button.js +1 -1
- package/primitives/button.js.map +1 -1
- package/primitives/conversation-timeline.d.ts +1 -1
- package/primitives/conversation-timeline.js +4 -4
- package/primitives/conversation-timeline.js.map +1 -1
- package/primitives/day-separator.js +3 -3
- package/primitives/day-separator.js.map +1 -1
- package/primitives/multimodal-input.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.js +7 -7
- package/primitives/timeline-item-group.js.map +1 -1
- package/primitives/timeline-item.d.ts +1 -1
- package/primitives/timeline-item.d.ts.map +1 -1
- package/primitives/timeline-item.js +54 -14
- package/primitives/timeline-item.js.map +1 -1
- package/primitives/trigger.js +1 -1
- package/primitives/trigger.js.map +1 -1
- package/primitives/window.js +1 -1
- package/primitives/window.js.map +1 -1
- package/provider.d.ts +4 -2
- package/provider.d.ts.map +1 -1
- package/provider.js +56 -8
- package/provider.js.map +1 -1
- package/realtime/event-filter.d.ts +4 -1
- package/realtime/event-filter.d.ts.map +1 -1
- package/realtime/event-filter.js +14 -0
- package/realtime/event-filter.js.map +1 -1
- package/realtime/provider.d.ts +1 -1
- package/realtime/provider.d.ts.map +1 -1
- package/realtime/provider.js +1 -2
- package/realtime/provider.js.map +1 -1
- package/realtime/seen-store.d.ts +2 -2
- package/realtime/support-provider.js +3 -2
- package/realtime/support-provider.js.map +1 -1
- package/realtime/typing-store.d.ts +1 -1
- package/realtime/use-realtime.d.ts +1 -1
- package/support/components/avatar-stack.d.ts.map +1 -1
- package/support/components/avatar-stack.js +32 -12
- package/support/components/avatar-stack.js.map +1 -1
- package/support/components/avatar.d.ts +34 -3
- package/support/components/avatar.d.ts.map +1 -1
- package/support/components/avatar.js +61 -8
- package/support/components/avatar.js.map +1 -1
- package/support/components/button.d.ts +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-resolved-feedback.d.ts +21 -0
- package/support/components/conversation-resolved-feedback.d.ts.map +1 -0
- package/support/components/conversation-resolved-feedback.js +59 -0
- package/support/components/conversation-resolved-feedback.js.map +1 -0
- package/support/components/conversation-timeline-utils.d.ts +5 -0
- package/support/components/conversation-timeline-utils.d.ts.map +1 -0
- package/support/components/conversation-timeline-utils.js +10 -0
- package/support/components/conversation-timeline-utils.js.map +1 -0
- package/support/components/conversation-timeline.d.ts +1 -1
- package/support/components/conversation-timeline.d.ts.map +1 -1
- package/support/components/conversation-timeline.js +5 -4
- package/support/components/conversation-timeline.js.map +1 -1
- package/support/components/header.js +1 -1
- package/support/components/icons.d.ts +1 -1
- package/support/components/icons.d.ts.map +1 -1
- package/support/components/icons.js +6 -2
- package/support/components/icons.js.map +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.d.ts +2 -1
- package/support/components/index.js +3 -2
- 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.d.ts.map +1 -1
- package/support/pages/conversation.js +34 -8
- package/support/pages/conversation.js.map +1 -1
- package/support/pages/home.js +5 -3
- package/support/pages/home.js.map +1 -1
- package/support/router.d.ts.map +1 -1
- package/support/router.js +4 -0
- package/support/router.js.map +1 -1
- package/support/store/support-store.js +0 -1
- package/support/store/support-store.js.map +1 -1
- package/support/{support-C7Xaw-N6.css → support-DmViRaga.css} +2 -2
- package/support/{support-C7Xaw-N6.css.map → support-DmViRaga.css.map} +1 -1
- package/support/text/index.js +1 -1
- package/support/text/index.js.map +1 -1
- package/support/text/locales/en.js +10 -1
- package/support/text/locales/en.js.map +1 -1
- package/support/text/locales/es.js +10 -1
- package/support/text/locales/es.js.map +1 -1
- package/support/text/locales/fr.js +10 -1
- package/support/text/locales/fr.js.map +1 -1
- package/support/text/locales/keys.d.ts +11 -0
- package/support/text/locales/keys.d.ts.map +1 -1
- package/support/text/locales/keys.js +3 -0
- package/support/text/locales/keys.js.map +1 -1
- package/support/utils/index.d.ts +1 -1
- package/support-config.js +0 -1
- package/support-config.js.map +1 -1
- package/support.css +1 -1
- package/tailwind.css +1 -1
- package/utils/conversation.d.ts.map +1 -1
- package/utils/conversation.js +1 -3
- package/utils/conversation.js.map +1 -1
- package/utils/use-render-element.js +2 -2
- package/utils/use-render-element.js.map +1 -1
- package/api.d.ts +0 -71
- package/api.d.ts.map +0 -1
- package/checks.d.ts +0 -189
- package/checks.d.ts.map +0 -1
- package/clsx.d.ts +0 -7
- package/clsx.d.ts.map +0 -1
- package/coerce.d.ts +0 -9
- package/coerce.d.ts.map +0 -1
- package/conversation.d.ts.map +0 -1
- package/core.d.ts +0 -35
- package/core.d.ts.map +0 -1
- package/errors.d.ts +0 -130
- package/errors.d.ts.map +0 -1
- package/errors2.d.ts +0 -24
- package/errors2.d.ts.map +0 -1
- package/index2.d.ts +0 -4
- package/index3.d.ts +0 -1
- package/json-schema.d.ts +0 -70
- package/json-schema.d.ts.map +0 -1
- package/metadata.d.ts +0 -1
- package/openapi-generator.d.ts +0 -1
- package/openapi-generator2.d.ts +0 -1
- package/openapi-generator3.d.ts +0 -1
- package/openapi30.d.ts +0 -125
- package/openapi30.d.ts.map +0 -1
- package/openapi31.d.ts +0 -131
- package/openapi31.d.ts.map +0 -1
- package/parse.d.ts +0 -17
- package/parse.d.ts.map +0 -1
- package/realtime-events.d.ts.map +0 -1
- package/registries.d.ts +0 -32
- package/registries.d.ts.map +0 -1
- package/schemas.d.ts +0 -971
- package/schemas.d.ts.map +0 -1
- package/schemas2.d.ts +0 -345
- package/schemas2.d.ts.map +0 -1
- package/schemas3.d.ts.map +0 -1
- package/specification-extension.d.ts +0 -9
- package/specification-extension.d.ts.map +0 -1
- package/standard-schema.d.ts +0 -121
- package/standard-schema.d.ts.map +0 -1
- package/timeline-item.d.ts +0 -227
- package/timeline-item.d.ts.map +0 -1
- package/to-json-schema.d.ts +0 -96
- package/to-json-schema.d.ts.map +0 -1
- package/util.d.ts +0 -45
- package/util.d.ts.map +0 -1
- package/versions.d.ts +0 -9
- package/versions.d.ts.map +0 -1
- package/zod-extensions.d.ts +0 -39
- package/zod-extensions.d.ts.map +0 -1
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"}
|
package/identify-visitor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useIdentificationState } from "./support/context/identification.js";
|
|
1
2
|
import { useVisitor } from "./hooks/use-visitor.js";
|
|
2
3
|
import { computeMetadataHash } from "./utils/metadata-hash.js";
|
|
3
4
|
import { useEffect, useState } from "react";
|
|
@@ -8,8 +9,19 @@ import { useEffect, useState } from "react";
|
|
|
8
9
|
*/
|
|
9
10
|
const IdentifySupportVisitor = ({ externalId, email, name, image, metadata: _newMetadata }) => {
|
|
10
11
|
const { visitor, identify, setVisitorMetadata } = useVisitor();
|
|
12
|
+
const identificationState = useIdentificationState();
|
|
11
13
|
const [hasIdentified, setHasIdentified] = useState(false);
|
|
12
14
|
const [lastMetadataHash, setLastMetadataHash] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const needsIdentification = Boolean(externalId || email) && !visitor?.contact;
|
|
17
|
+
if (identificationState?.setIsIdentifying) identificationState.setIsIdentifying(needsIdentification && !hasIdentified);
|
|
18
|
+
}, [
|
|
19
|
+
externalId,
|
|
20
|
+
email,
|
|
21
|
+
visitor?.contact,
|
|
22
|
+
hasIdentified,
|
|
23
|
+
identificationState
|
|
24
|
+
]);
|
|
13
25
|
useEffect(() => {
|
|
14
26
|
const shouldIdentify = async () => {
|
|
15
27
|
if (!Boolean(externalId || email)) return;
|
|
@@ -22,6 +34,7 @@ const IdentifySupportVisitor = ({ externalId, email, name, image, metadata: _new
|
|
|
22
34
|
image: image ?? void 0
|
|
23
35
|
});
|
|
24
36
|
setHasIdentified(true);
|
|
37
|
+
if (identificationState?.setIsIdentifying) identificationState.setIsIdentifying(false);
|
|
25
38
|
}
|
|
26
39
|
return;
|
|
27
40
|
}
|
|
@@ -44,7 +57,8 @@ const IdentifySupportVisitor = ({ externalId, email, name, image, metadata: _new
|
|
|
44
57
|
name,
|
|
45
58
|
image,
|
|
46
59
|
identify,
|
|
47
|
-
hasIdentified
|
|
60
|
+
hasIdentified,
|
|
61
|
+
identificationState
|
|
48
62
|
]);
|
|
49
63
|
useEffect(() => {
|
|
50
64
|
const updateMetadata = async () => {
|
package/identify-visitor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identify-visitor.js","names":[],"sources":["../src/identify-visitor.tsx"],"sourcesContent":["/** biome-ignore-all lint/correctness/useExhaustiveDependencies: wanted here */\n\nimport type { VisitorMetadata } from \"@cossistant/types\";\nimport { type ReactElement, useEffect, useState } from \"react\";\nimport { useVisitor } from \"./hooks\";\nimport { computeMetadataHash } from \"./utils/metadata-hash\";\n\nexport type IdentifySupportVisitorProps = {\n\texternalId?: string;\n\temail?: string;\n\tname?: string | null;\n\timage?: string | null;\n\tmetadata?: VisitorMetadata | null;\n};\n\n/**\n * Component exposed by Cossistant allowing you to identify a visitor whenever rendered with either an `externalId` or `email`. Once identified, the visitor will be associated with a contact and any subsequent metadata updates will be attached to this contact.\n */\nexport const IdentifySupportVisitor = ({\n\texternalId,\n\temail,\n\tname,\n\timage,\n\tmetadata: _newMetadata,\n}: IdentifySupportVisitorProps): ReactElement | null => {\n\tconst { visitor, identify, setVisitorMetadata } = useVisitor();\n\tconst [hasIdentified, setHasIdentified] = useState(false);\n\tconst [lastMetadataHash, setLastMetadataHash] = useState<string | null>(null);\n\n\t// Only call identify if:\n\t// 1. Visitor hasn't been identified yet (no contact)\n\t// 2. Name, email, or image changed compared to current contact\n\tuseEffect(() => {\n\t\tconst shouldIdentify = async () => {\n\t\t\tconst hasIdentificationData = Boolean(externalId || email);\n\n\t\t\t// Need at least externalId or email to identify\n\t\t\tif (!hasIdentificationData) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Case 1: No contact exists yet\n\t\t\tif (!visitor?.contact) {\n\t\t\t\tif (!hasIdentified) {\n\t\t\t\t\tawait identify({\n\t\t\t\t\t\texternalId,\n\t\t\t\t\t\temail,\n\t\t\t\t\t\tname: name ?? undefined,\n\t\t\t\t\t\timage: image ?? undefined,\n\t\t\t\t\t});\n\t\t\t\t\tsetHasIdentified(true);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Case 2: Contact exists but name/email/image changed\n\t\t\tconst contact = visitor.contact;\n\t\t\tconst nameChanged = name !== undefined && name !== contact.name;\n\t\t\tconst emailChanged = email !== undefined && email !== contact.email;\n\t\t\tconst imageChanged = image !== undefined && image !== contact.image;\n\t\t\tconst hasChanges = nameChanged || emailChanged || imageChanged;\n\n\t\t\tif (hasChanges) {\n\t\t\t\tawait identify({\n\t\t\t\t\texternalId,\n\t\t\t\t\temail,\n\t\t\t\t\tname: name ?? undefined,\n\t\t\t\t\timage: image ?? undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\n\t\tshouldIdentify();\n\t}, [\n\t\tvisitor?.contact,\n\t\texternalId,\n\t\temail,\n\t\tname,\n\t\timage,\n\t\tidentify,\n\t\thasIdentified,\n\t]);\n\n\t// Compute metadata hash, compare to previous hash and only update if it has changed\n\tuseEffect(() => {\n\t\tconst updateMetadata = async () => {\n\t\t\t// Skip if no metadata provided or visitor doesn't have a contact\n\t\t\tif (!_newMetadata) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!visitor?.contact) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Compute new metadata hash\n\t\t\tconst newMetadataHash = await computeMetadataHash(_newMetadata);\n\n\t\t\t// Get the existing hash from the contact\n\t\t\tconst existingHash = visitor.contact.metadataHash || \"\";\n\n\t\t\t// Only update if hashes don't match and we haven't already updated with this hash\n\t\t\tconst hashChanged = newMetadataHash !== existingHash;\n\t\t\tconst notAlreadyUpdated = newMetadataHash !== lastMetadataHash;\n\t\t\tconst shouldUpdate = Boolean(\n\t\t\t\tnewMetadataHash && hashChanged && notAlreadyUpdated\n\t\t\t);\n\n\t\t\tif (shouldUpdate) {\n\t\t\t\tawait setVisitorMetadata(_newMetadata);\n\t\t\t\tsetLastMetadataHash(newMetadataHash);\n\t\t\t}\n\t\t};\n\n\t\tupdateMetadata();\n\t}, [\n\t\t_newMetadata,\n\t\tvisitor?.contact?.metadataHash,\n\t\tvisitor?.contact,\n\t\tsetVisitorMetadata,\n\t\tlastMetadataHash,\n\t]);\n\n\treturn null;\n};\n\nIdentifySupportVisitor.displayName = \"IdentifySupportVisitor\";\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"identify-visitor.js","names":[],"sources":["../src/identify-visitor.tsx"],"sourcesContent":["/** biome-ignore-all lint/correctness/useExhaustiveDependencies: wanted here */\n\nimport type { VisitorMetadata } from \"@cossistant/types\";\nimport { type ReactElement, useEffect, useState } from \"react\";\nimport { useVisitor } from \"./hooks\";\nimport { useIdentificationState } from \"./support/context/identification\";\nimport { computeMetadataHash } from \"./utils/metadata-hash\";\n\nexport type IdentifySupportVisitorProps = {\n\texternalId?: string;\n\temail?: string;\n\tname?: string | null;\n\timage?: string | null;\n\tmetadata?: VisitorMetadata | null;\n};\n\n/**\n * Component exposed by Cossistant allowing you to identify a visitor whenever rendered with either an `externalId` or `email`. Once identified, the visitor will be associated with a contact and any subsequent metadata updates will be attached to this contact.\n */\nexport const IdentifySupportVisitor = ({\n\texternalId,\n\temail,\n\tname,\n\timage,\n\tmetadata: _newMetadata,\n}: IdentifySupportVisitorProps): ReactElement | null => {\n\tconst { visitor, identify, setVisitorMetadata } = useVisitor();\n\tconst identificationState = useIdentificationState();\n\tconst [hasIdentified, setHasIdentified] = useState(false);\n\tconst [lastMetadataHash, setLastMetadataHash] = useState<string | null>(null);\n\n\t// Signal that identification is pending when we have identification data but no contact yet\n\tuseEffect(() => {\n\t\tconst hasIdentificationData = Boolean(externalId || email);\n\t\tconst needsIdentification = hasIdentificationData && !visitor?.contact;\n\n\t\tif (identificationState?.setIsIdentifying) {\n\t\t\tidentificationState.setIsIdentifying(\n\t\t\t\tneedsIdentification && !hasIdentified\n\t\t\t);\n\t\t}\n\t}, [externalId, email, visitor?.contact, hasIdentified, identificationState]);\n\n\t// Only call identify if:\n\t// 1. Visitor hasn't been identified yet (no contact)\n\t// 2. Name, email, or image changed compared to current contact\n\tuseEffect(() => {\n\t\tconst shouldIdentify = async () => {\n\t\t\tconst hasIdentificationData = Boolean(externalId || email);\n\n\t\t\t// Need at least externalId or email to identify\n\t\t\tif (!hasIdentificationData) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Case 1: No contact exists yet\n\t\t\tif (!visitor?.contact) {\n\t\t\t\tif (!hasIdentified) {\n\t\t\t\t\tawait identify({\n\t\t\t\t\t\texternalId,\n\t\t\t\t\t\temail,\n\t\t\t\t\t\tname: name ?? undefined,\n\t\t\t\t\t\timage: image ?? undefined,\n\t\t\t\t\t});\n\t\t\t\t\tsetHasIdentified(true);\n\t\t\t\t\t// Clear identifying state after identification completes\n\t\t\t\t\tif (identificationState?.setIsIdentifying) {\n\t\t\t\t\t\tidentificationState.setIsIdentifying(false);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Case 2: Contact exists but name/email/image changed\n\t\t\tconst contact = visitor.contact;\n\t\t\tconst nameChanged = name !== undefined && name !== contact.name;\n\t\t\tconst emailChanged = email !== undefined && email !== contact.email;\n\t\t\tconst imageChanged = image !== undefined && image !== contact.image;\n\t\t\tconst hasChanges = nameChanged || emailChanged || imageChanged;\n\n\t\t\tif (hasChanges) {\n\t\t\t\tawait identify({\n\t\t\t\t\texternalId,\n\t\t\t\t\temail,\n\t\t\t\t\tname: name ?? undefined,\n\t\t\t\t\timage: image ?? undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\n\t\tshouldIdentify();\n\t}, [\n\t\tvisitor?.contact,\n\t\texternalId,\n\t\temail,\n\t\tname,\n\t\timage,\n\t\tidentify,\n\t\thasIdentified,\n\t\tidentificationState,\n\t]);\n\n\t// Compute metadata hash, compare to previous hash and only update if it has changed\n\tuseEffect(() => {\n\t\tconst updateMetadata = async () => {\n\t\t\t// Skip if no metadata provided or visitor doesn't have a contact\n\t\t\tif (!_newMetadata) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!visitor?.contact) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Compute new metadata hash\n\t\t\tconst newMetadataHash = await computeMetadataHash(_newMetadata);\n\n\t\t\t// Get the existing hash from the contact\n\t\t\tconst existingHash = visitor.contact.metadataHash || \"\";\n\n\t\t\t// Only update if hashes don't match and we haven't already updated with this hash\n\t\t\tconst hashChanged = newMetadataHash !== existingHash;\n\t\t\tconst notAlreadyUpdated = newMetadataHash !== lastMetadataHash;\n\t\t\tconst shouldUpdate = Boolean(\n\t\t\t\tnewMetadataHash && hashChanged && notAlreadyUpdated\n\t\t\t);\n\n\t\t\tif (shouldUpdate) {\n\t\t\t\tawait setVisitorMetadata(_newMetadata);\n\t\t\t\tsetLastMetadataHash(newMetadataHash);\n\t\t\t}\n\t\t};\n\n\t\tupdateMetadata();\n\t}, [\n\t\t_newMetadata,\n\t\tvisitor?.contact?.metadataHash,\n\t\tvisitor?.contact,\n\t\tsetVisitorMetadata,\n\t\tlastMetadataHash,\n\t]);\n\n\treturn null;\n};\n\nIdentifySupportVisitor.displayName = \"IdentifySupportVisitor\";\n"],"mappings":";;;;;;;;;AAmBA,MAAa,0BAA0B,EACtC,YACA,OACA,MACA,OACA,UAAU,mBAC6C;CACvD,MAAM,EAAE,SAAS,UAAU,uBAAuB,YAAY;CAC9D,MAAM,sBAAsB,wBAAwB;CACpD,MAAM,CAAC,eAAe,oBAAoB,SAAS,MAAM;CACzD,MAAM,CAAC,kBAAkB,uBAAuB,SAAwB,KAAK;AAG7E,iBAAgB;EAEf,MAAM,sBADwB,QAAQ,cAAc,MAAM,IACL,CAAC,SAAS;AAE/D,MAAI,qBAAqB,iBACxB,qBAAoB,iBACnB,uBAAuB,CAAC,cACxB;IAEA;EAAC;EAAY;EAAO,SAAS;EAAS;EAAe;EAAoB,CAAC;AAK7E,iBAAgB;EACf,MAAM,iBAAiB,YAAY;AAIlC,OAAI,CAH0B,QAAQ,cAAc,MAAM,CAIzD;AAID,OAAI,CAAC,SAAS,SAAS;AACtB,QAAI,CAAC,eAAe;AACnB,WAAM,SAAS;MACd;MACA;MACA,MAAM,QAAQ;MACd,OAAO,SAAS;MAChB,CAAC;AACF,sBAAiB,KAAK;AAEtB,SAAI,qBAAqB,iBACxB,qBAAoB,iBAAiB,MAAM;;AAG7C;;GAID,MAAM,UAAU,QAAQ;GACxB,MAAM,cAAc,SAAS,UAAa,SAAS,QAAQ;GAC3D,MAAM,eAAe,UAAU,UAAa,UAAU,QAAQ;GAC9D,MAAM,eAAe,UAAU,UAAa,UAAU,QAAQ;AAG9D,OAFmB,eAAe,gBAAgB,aAGjD,OAAM,SAAS;IACd;IACA;IACA,MAAM,QAAQ;IACd,OAAO,SAAS;IAChB,CAAC;;AAIJ,kBAAgB;IACd;EACF,SAAS;EACT;EACA;EACA;EACA;EACA;EACA;EACA;EACA,CAAC;AAGF,iBAAgB;EACf,MAAM,iBAAiB,YAAY;AAElC,OAAI,CAAC,aACJ;AAGD,OAAI,CAAC,SAAS,QACb;GAID,MAAM,kBAAkB,MAAM,oBAAoB,aAAa;GAM/D,MAAM,cAAc,qBAHC,QAAQ,QAAQ,gBAAgB;GAIrD,MAAM,oBAAoB,oBAAoB;AAK9C,OAJqB,QACpB,mBAAmB,eAAe,kBAClC,EAEiB;AACjB,UAAM,mBAAmB,aAAa;AACtC,wBAAoB,gBAAgB;;;AAItC,kBAAgB;IACd;EACF;EACA,SAAS,SAAS;EAClB,SAAS;EACT;EACA;EACA,CAAC;AAEF,QAAO;;AAGR,uBAAuB,cAAc"}
|
package/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { useClientQuery } from "./hooks/private/use-client-query.js";
|
|
|
2
2
|
import { useDefaultMessages } from "./hooks/private/use-default-messages.js";
|
|
3
3
|
import { ConversationItem, DaySeparatorItem, GroupedMessage, TimelineEventItem, TimelineToolItem, UseGroupedMessagesOptions, UseGroupedMessagesProps, useGroupedMessages } from "./hooks/private/use-grouped-messages.js";
|
|
4
4
|
import { UseMultimodalInputOptions, UseMultimodalInputReturn, useMultimodalInput } from "./hooks/private/use-multimodal-input.js";
|
|
5
|
-
import { UseClientResult, useClient } from "./hooks/private/use-rest-client.js";
|
|
5
|
+
import { ConfigurationError, UseClientResult, useClient } from "./hooks/private/use-rest-client.js";
|
|
6
6
|
import { UseComposerRefocusOptions, UseComposerRefocusReturn, useComposerRefocus } from "./hooks/use-composer-refocus.js";
|
|
7
7
|
import { UseConversationOptions, UseConversationResult, useConversation } from "./hooks/use-conversation.js";
|
|
8
8
|
import { CONVERSATION_AUTO_SEEN_DELAY_MS, UseConversationAutoSeenOptions, useConversationAutoSeen } from "./hooks/use-conversation-auto-seen.js";
|
|
@@ -49,4 +49,4 @@ import { Header } from "./support/components/header.js";
|
|
|
49
49
|
import { WebSocketContextValue, WebSocketProvider, useWebSocket } from "./support/context/websocket.js";
|
|
50
50
|
import { useSupportConfig, useSupportNavigation, useSupportStore } from "./support/store/support-store.js";
|
|
51
51
|
import { DefaultRoutes, NavigationState, RouteRegistry, Support, SupportContentProps, SupportPageProps, SupportPageType, SupportProps, SupportRootProps, SupportRouterProps, SupportTriggerProps } from "./support/index.js";
|
|
52
|
-
export { Align, CoButton as Button, CONVERSATION_AUTO_SEEN_DELAY_MS, CollisionPadding, ContentProps, ConversationEndEvent, ConversationItem, ConversationLifecycleState, ConversationPreviewAssignedAgent, ConversationPreviewLastMessage, ConversationPreviewTypingParticipant, ConversationPreviewTypingState, ConversationStartEvent, ConversationTimelineTypingParticipant, ConversationTypingParticipant, CossistantContextValue, CossistantProviderProps, CreateConversationVariables, CustomPage, DaySeparatorItem, DefaultRoutes, ErrorEvent, FileUploadPart, GroupedMessage, Header, IdentifySupportVisitor, IdentifySupportVisitorProps, MessageReceivedEvent, MessageSentEvent, NavigationState, index_d_exports as Primitives, RealtimeAuthConfig, RealtimeContextValue, RealtimeEventHandler, RealtimeEventHandlerEntry, RealtimeEventHandlersMap, RealtimeEventMeta, RealtimeProvider, RealtimeProviderProps, RootProps, RouteRegistry, SendMessageOptions, SendMessageResult, Side, Support, SupportConfig, SupportConfigProps, SupportContentProps, SupportContext, SupportEvent, SupportEventCallbacks, SupportEventType, SupportHandle, SupportLocale, SupportPageProps, SupportPageType, SupportProps, SupportProvider, SupportProviderProps, SupportRealtimeProvider, SupportRootProps, SupportRouterProps, SupportTextContentOverrides, SupportTriggerProps, Text, TimelineEventItem, TimelineToolItem, TriggerRenderProps, UseClientResult, UseComposerRefocusOptions, UseComposerRefocusReturn, UseConversationAutoSeenOptions, UseConversationHistoryPageOptions, UseConversationHistoryPageReturn, UseConversationLifecycleOptions, UseConversationLifecycleReturn, UseConversationOptions, UseConversationPageOptions, UseConversationPageReturn, UseConversationPreviewOptions, UseConversationPreviewReturn, UseConversationResult, UseConversationTimelineItemsOptions, UseConversationTimelineItemsResult, UseConversationTimelineOptions, UseConversationTimelineReturn, UseConversationsOptions, UseConversationsResult, UseCreateConversationOptions, UseCreateConversationResult, UseFileUploadOptions, UseFileUploadReturn, UseGroupedMessagesOptions, UseGroupedMessagesProps, UseHomePageOptions, UseHomePageReturn, UseMessageComposerOptions, UseMessageComposerReturn, UseMultimodalInputOptions, UseMultimodalInputReturn, UseRealtimeSupportOptions, UseRealtimeSupportResult, UseScrollMaskOptions, UseScrollMaskReturn, UseSendMessageOptions, UseSendMessageResult, UseSoundEffectOptions, UseSoundEffectReturn, UseSupportValue, UseVisitorReturn, WebSocketContextValue, WebSocketProvider, WindowVisibilityFocusState, applyConversationSeenEvent, applyConversationTypingEvent, clearTypingFromTimelineItem, clearTypingState, hydrateConversationSeen, setTypingState, upsertConversationSeen, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useFileUpload, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useNewMessageSound, useRealtime, useRealtimeConnection, useRealtimeSupport, useScrollMask, useSendMessage, useSoundEffect, useSupport, useSupportConfig, useSupportEventEmitter, useSupportEvents, useSupportHandle, useSupportNavigation, useSupportStore, useSupportText, useTypingSound, useVisitor, useWebSocket, useWindowVisibilityFocus };
|
|
52
|
+
export { Align, CoButton as Button, CONVERSATION_AUTO_SEEN_DELAY_MS, CollisionPadding, ConfigurationError, ContentProps, ConversationEndEvent, ConversationItem, ConversationLifecycleState, ConversationPreviewAssignedAgent, ConversationPreviewLastMessage, ConversationPreviewTypingParticipant, ConversationPreviewTypingState, ConversationStartEvent, ConversationTimelineTypingParticipant, ConversationTypingParticipant, CossistantContextValue, CossistantProviderProps, CreateConversationVariables, CustomPage, DaySeparatorItem, DefaultRoutes, ErrorEvent, FileUploadPart, GroupedMessage, Header, IdentifySupportVisitor, IdentifySupportVisitorProps, MessageReceivedEvent, MessageSentEvent, NavigationState, index_d_exports as Primitives, RealtimeAuthConfig, RealtimeContextValue, RealtimeEventHandler, RealtimeEventHandlerEntry, RealtimeEventHandlersMap, RealtimeEventMeta, RealtimeProvider, RealtimeProviderProps, RootProps, RouteRegistry, SendMessageOptions, SendMessageResult, Side, Support, SupportConfig, SupportConfigProps, SupportContentProps, SupportContext, SupportEvent, SupportEventCallbacks, SupportEventType, SupportHandle, SupportLocale, SupportPageProps, SupportPageType, SupportProps, SupportProvider, SupportProviderProps, SupportRealtimeProvider, SupportRootProps, SupportRouterProps, SupportTextContentOverrides, SupportTriggerProps, Text, TimelineEventItem, TimelineToolItem, TriggerRenderProps, UseClientResult, UseComposerRefocusOptions, UseComposerRefocusReturn, UseConversationAutoSeenOptions, UseConversationHistoryPageOptions, UseConversationHistoryPageReturn, UseConversationLifecycleOptions, UseConversationLifecycleReturn, UseConversationOptions, UseConversationPageOptions, UseConversationPageReturn, UseConversationPreviewOptions, UseConversationPreviewReturn, UseConversationResult, UseConversationTimelineItemsOptions, UseConversationTimelineItemsResult, UseConversationTimelineOptions, UseConversationTimelineReturn, UseConversationsOptions, UseConversationsResult, UseCreateConversationOptions, UseCreateConversationResult, UseFileUploadOptions, UseFileUploadReturn, UseGroupedMessagesOptions, UseGroupedMessagesProps, UseHomePageOptions, UseHomePageReturn, UseMessageComposerOptions, UseMessageComposerReturn, UseMultimodalInputOptions, UseMultimodalInputReturn, UseRealtimeSupportOptions, UseRealtimeSupportResult, UseScrollMaskOptions, UseScrollMaskReturn, UseSendMessageOptions, UseSendMessageResult, UseSoundEffectOptions, UseSoundEffectReturn, UseSupportValue, UseVisitorReturn, WebSocketContextValue, WebSocketProvider, WindowVisibilityFocusState, applyConversationSeenEvent, applyConversationTypingEvent, clearTypingFromTimelineItem, clearTypingState, hydrateConversationSeen, setTypingState, upsertConversationSeen, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useFileUpload, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useNewMessageSound, useRealtime, useRealtimeConnection, useRealtimeSupport, useScrollMask, useSendMessage, useSoundEffect, useSupport, useSupportConfig, useSupportEventEmitter, useSupportEvents, useSupportHandle, useSupportNavigation, useSupportStore, useSupportText, useTypingSound, useVisitor, useWebSocket, useWindowVisibilityFocus };
|
package/index.js
CHANGED
|
@@ -9,12 +9,12 @@ import { primitives_exports } from "./primitives/index.js";
|
|
|
9
9
|
import { RealtimeProvider, useRealtimeConnection } from "./realtime/provider.js";
|
|
10
10
|
import { useRealtime } from "./realtime/use-realtime.js";
|
|
11
11
|
import { SupportRealtimeProvider } from "./realtime/support-provider.js";
|
|
12
|
+
import { CoButton } from "./support/components/button.js";
|
|
12
13
|
import { useSoundEffect } from "./hooks/use-sound-effect.js";
|
|
13
14
|
import { useNewMessageSound } from "./hooks/use-new-message-sound.js";
|
|
14
15
|
import { useTypingSound } from "./hooks/use-typing-sound.js";
|
|
15
16
|
import { useSupportEventEmitter, useSupportEvents } from "./support/context/events.js";
|
|
16
17
|
import { useSupportHandle } from "./support/context/handle.js";
|
|
17
|
-
import { CoButton } from "./support/components/button.js";
|
|
18
18
|
import { Header } from "./support/components/header.js";
|
|
19
19
|
import { Text, useSupportText } from "./support/text/index.js";
|
|
20
20
|
import { WebSocketProvider, useWebSocket } from "./support/context/websocket.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cossistant/react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.31",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Cossistant team",
|
|
7
7
|
"description": "Headless React SDK for building AI-powered support/chat widgets. Hooks + primitives, WS-driven, TypeScript-first. Next.js-ready, Tailwind optional.",
|
|
@@ -88,14 +88,17 @@
|
|
|
88
88
|
"*.css"
|
|
89
89
|
],
|
|
90
90
|
"dependencies": {
|
|
91
|
-
"@cossistant/core": "0.0.
|
|
92
|
-
"@cossistant/
|
|
91
|
+
"@cossistant/core": "0.0.31",
|
|
92
|
+
"@cossistant/tiny-markdown": "0.0.1",
|
|
93
|
+
"@cossistant/types": "0.0.31",
|
|
94
|
+
"facehash": "0.0.6",
|
|
93
95
|
"@floating-ui/react": "^0.27.16",
|
|
94
96
|
"class-variance-authority": "^0.7.1",
|
|
95
97
|
"clsx": "^2.1.1",
|
|
96
98
|
"nanoid": "^5.1.5",
|
|
97
99
|
"react-markdown": "^10.1.0",
|
|
98
100
|
"react-use-websocket": "^4.13.0",
|
|
101
|
+
"remark-breaks": "^4.0.0",
|
|
99
102
|
"tailwind-merge": "^3.3.1",
|
|
100
103
|
"ulid": "^3.0.1"
|
|
101
104
|
},
|