UNPKG

captivate-chat-api

Version:

This is a wrapper for captivate chat api socket custom channel

451 lines (403 loc) 16 kB
/** * File manager for handling file uploads, storage, and presigned URL generation. * Supports direct file uploads with automatic text extraction and storage management. */ /** Allowed TTL values for S3 object expiration (days). Only these values are applied; any other value is ignored. */ export type TtlDays = 1 | 7 | 30 | 90; const ALLOWED_TTL_DAYS: TtlDays[] = [1, 7, 30, 90]; export class CaptivateChatFileManager { private static readonly FILE_TO_TEXT_API_URL = 'https://file-to-text.prod.captivat.io/api/file-to-text'; private static readonly PRESIGNED_URL_API_URL = 'https://file-to-text.prod.captivat.io/api/presigned-url'; public readonly type: 'files' = 'files'; public readonly files: Array<{ filename: string; type: string; file?: File | Blob; // For direct file uploads url?: string; // For referencing files when storage is false storage?: { fileKey: string; presignedUrl: string; expiresIn: number; fileSize: number; processingTime: number; }; // Storage information when storage is true textContent: { type: 'file_content'; text: string; metadata: { source: 'file_attachment'; originalFileName: string; storageType?: 'direct'; }; }; }>; constructor( file: File | Blob, textContent: { type: 'file_content'; text: string; metadata: { source: 'file_attachment'; originalFileName: string; storageType?: 'direct'; }; }, type: string ) { // Set up files array for direct file upload this.files = [{ filename: textContent.metadata.originalFileName, type: type, file: file, textContent: textContent }]; } /** * Factory method to create a CaptivateChatFileManager with automatic file-to-text conversion for direct file uploads. * @param options - Configuration options for the file input. * @param options.file - The file to convert. * @param options.fileName - Optional custom file name. * @param options.fileType - Optional custom file type. * @param options.storage - Whether to store the file for future reference (default: true). * @param options.url - URL to reference the file when storage is false (required when storage is false). * @param options.apiKey - Optional API key for constructing the path parameter. * @param options.conversationId - Optional conversation ID for constructing the path parameter. * @param options.ttlDays - Optional TTL for the stored S3 object: 1, 7, 30, or 90 days. Omit for no expiration. * @returns A promise that resolves to a CaptivateChatFileManager instance with converted text. */ static async create( options: { file: File | Blob; fileName?: string; fileType?: string; storage?: boolean; url?: string; apiKey?: string; conversationId?: string; ttlDays?: TtlDays; } ): Promise<CaptivateChatFileManager> { // Direct file upload const file = options.file; const storage = options.storage !== undefined ? options.storage : true; // Default to true // If storage is false, URL is required for sendMessage compatibility if (storage === false && !options.url) { throw new Error('URL is required when storage is false for sendMessage compatibility'); } const finalFileName = options.fileName || (file instanceof File ? file.name : `attachment_${Date.now()}`); const finalType = options.fileType || (file instanceof File ? file.type : file.type || 'application/octet-stream'); // Convert file to text const conversionResult = await CaptivateChatFileManager.convertFileToText( file, finalFileName, true, // Always include metadata storage, options.apiKey, options.conversationId, options.ttlDays ); // Create and return the instance with proxy for array-like behavior const instance = new CaptivateChatFileManager(file, { type: 'file_content', text: conversionResult.text, metadata: { source: 'file_attachment', originalFileName: finalFileName, storageType: 'direct' } }, finalType); // Add storage information if available if (storage && conversionResult.storageInfo) { instance.files[0].storage = conversionResult.storageInfo; } // If storage is false and URL is provided, add URL to the file object if (storage === false && options.url) { instance.files[0].url = options.url; } return CaptivateChatFileManager.createProxy(instance); } /** * Gets the first file from the files array for convenience. * @returns The first file object, or undefined if no files exist. */ getFirstFile() { return this.files[0]; } /** * Gets the filename of the first file. * @returns The filename, or undefined if no files exist. */ getFilename() { return this.files[0]?.filename; } /** * Gets the text content of the first file. * @returns The text content, or empty string if no files exist. */ getTextContent() { return this.files[0]?.textContent?.text || ''; } /** * Gets the file type of the first file. * @returns The file type, or undefined if no files exist. */ getFileType() { return this.files[0]?.type; } /** * Returns the files array directly for compatibility with the files property. * This allows using fileInputObj directly as the files value. * @returns The files array. */ toFilesArray() { return this.files; } /** * Makes the class iterable so it can be used directly as an array. */ *[Symbol.iterator]() { yield* this.files; } /** * Getter that returns the files array, allowing the class to be used directly as files. * This enables: files: fileInputObj instead of files: fileInputObj.files */ get [Symbol.toStringTag]() { return 'Array'; } /** * Returns the length of the files array for array-like behavior. */ get length() { return this.files.length; } /** * Refreshes the secure URL for the first file (if it has storage information). * @param expiresIn - Expiration time in seconds (default: 7200 = 2 hours). * @returns A promise that resolves to the refreshed secure URL, or undefined if no storage info. */ async refreshSecureUrl(expiresIn: number = 7200): Promise<string | undefined> { const firstFile = this.files[0]; if (!firstFile || !(firstFile as any).storage?.fileKey) { return undefined; } return await CaptivateChatFileManager.getSecureFileUrl( (firstFile as any).storage.fileKey, expiresIn ); } /** * Proxy handler to make the class behave like the files array. * This allows using fileInputObj directly as files: fileInputObj */ static createProxy(instance: CaptivateChatFileManager) { return new Proxy(instance, { get(target, prop) { // If accessing the files array directly, return the files if (prop === Symbol.iterator || prop === 'length' || typeof prop === 'number') { return target.files[prop as any]; } // Otherwise, return the property from the instance return (target as any)[prop]; } }); } /** * Factory method to create a single file object with automatic file-to-text conversion. * Returns just the file object instead of the full CaptivateChatFileManager wrapper. * @param options - Configuration options for the file input. * @param options.file - The file to convert. * @param options.fileName - Optional custom file name. * @param options.fileType - Optional custom file type. * @param options.storage - Whether to store the file for future reference (default: true). * @param options.url - URL to reference the file when storage is false (required when storage is false). * @param options.ttlDays - Optional TTL for the stored S3 object: 1, 7, 30, or 90 days. Omit for no expiration. * @returns A promise that resolves to a single file object. */ static async createFile( options: { file: File | Blob; fileName?: string; fileType?: string; storage?: boolean; url?: string; apiKey?: string; conversationId?: string; ttlDays?: TtlDays; } ): Promise<{ filename: string; type: string; file?: File | Blob; textContent: { type: 'file_content'; text: string; metadata: { source: 'file_attachment'; originalFileName: string; storageType?: 'direct'; }; }; }> { const fileInput = await CaptivateChatFileManager.create({ file: options.file, fileName: options.fileName, fileType: options.fileType, storage: options.storage, url: options.url, apiKey: options.apiKey, conversationId: options.conversationId, ttlDays: options.ttlDays }); return fileInput.getFirstFile()!; } /** * Factory method to create multiple file inputs from an array of files. * Processes all files in parallel and returns a single CaptivateChatFileManager with all files. * @param options - Configuration options for all files. * @param options.files - Array of files to process. * @param options.storage - Whether to store the files for future reference (default: true). * @param options.urls - Array of URLs to reference files when storage is false (required when storage is false). * @param options.ttlDays - Optional TTL for stored S3 objects: 1, 7, 30, or 90 days. Omit for no expiration. * @returns A promise that resolves to a CaptivateChatFileManager instance with all processed files. */ static async createMultiple( options: { files: (File | Blob)[]; storage?: boolean; urls?: string[]; apiKey?: string; conversationId?: string; ttlDays?: TtlDays; } ): Promise<CaptivateChatFileManager> { const storage = options.storage !== undefined ? options.storage : true; // Default to true // If storage is false, URLs are required for sendMessage compatibility if (storage === false && (!options.urls || options.urls.length !== options.files.length)) { throw new Error('URLs array is required when storage is false and must match the number of files'); } // Process multiple files const files = options.files; const fileInputs = await Promise.all( files.map((file, index) => CaptivateChatFileManager.create({ file: file, storage: storage, url: storage === false ? options.urls![index] : undefined, apiKey: options.apiKey, conversationId: options.conversationId, ttlDays: options.ttlDays })) ); // Combine all files into a single array const allFiles = fileInputs.flatMap(input => input.files); // Create a new instance with all files const combinedInstance = new CaptivateChatFileManager( files[0], // Use first file as placeholder { type: 'file_content', text: '', // Will be ignored since we're using the files array metadata: { source: 'file_attachment', originalFileName: 'multiple_files', storageType: 'direct' } }, 'application/octet-stream' ); // Replace the files array with the combined files (combinedInstance as any).files = allFiles; return CaptivateChatFileManager.createProxy(combinedInstance); } /** * Generates a secure URL for accessing a stored file. * @param fileKey - The file key from the storage response. * @param expiresIn - Expiration time in seconds (default: 7200 = 2 hours). * @returns A promise that resolves to the secure URL. */ static async getSecureFileUrl(fileKey: string, expiresIn: number = 7200): Promise<string> { const url = CaptivateChatFileManager.PRESIGNED_URL_API_URL; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileKey: fileKey, expiresIn: expiresIn }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Secure URL generation failed: ${response.status} ${response.statusText}. ${errorData.error || ''}`); } const data = await response.json(); if (!data.success) { throw new Error(`Secure URL generation failed: ${data.error || 'Unknown error'}`); } return data.presignedUrl || ''; } catch (error: any) { if (error.message.includes('Secure URL generation failed')) { throw error; } throw new Error(`Failed to generate secure URL: ${error.message}`); } } /** * Converts a file to text using the file-to-text API endpoint. * @param file - The file to convert (File or Blob). * @param fileName - The name of the file. * @param includeMetadata - Whether to include additional metadata in the response. * @param storage - Whether to store the file for future reference. * @param apiKey - Optional API key for constructing the path parameter. * @param conversationId - Optional conversation ID for constructing the path parameter. * @param ttlDays - Optional TTL for the stored S3 object: 1, 7, 30, or 90 days. Only these values are sent; any other is ignored. * @returns A promise that resolves to the extracted text. */ private static async convertFileToText(file: File | Blob, fileName: string, includeMetadata: boolean, storage: boolean, apiKey?: string, conversationId?: string, ttlDays?: TtlDays): Promise<{text: string, storageInfo?: any}> { const url = CaptivateChatFileManager.FILE_TO_TEXT_API_URL; // Create FormData for multipart/form-data request const formData = new FormData(); formData.append('file', file, fileName); formData.append('includeMetadata', includeMetadata.toString()); formData.append('storage', storage.toString()); // Add path parameter if both apiKey and conversationId are provided if (apiKey && conversationId) { const path = `${apiKey}/${conversationId}`; formData.append('path', path); } // Add ttlDays only if it's one of the allowed values (1, 7, 30, 90) if (ttlDays !== undefined && ALLOWED_TTL_DAYS.includes(ttlDays)) { formData.append('ttlDays', String(ttlDays)); } try { const response = await fetch(url, { method: 'POST', body: formData }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`File conversion failed: ${response.status} ${response.statusText}. ${errorData.error || ''}`); } const data = await response.json(); if (!data.success) { throw new Error(`File conversion failed: ${data.error || 'Unknown error'}`); } return { text: data.text || '', storageInfo: storage ? { fileKey: data.fileKey, presignedUrl: data.presignedUrl, expiresIn: data.expiresIn, fileSize: data.fileSize, processingTime: data.processingTime } : undefined }; } catch (error: any) { if (error.message.includes('File conversion failed')) { throw error; } throw new Error(`Failed to convert file to text: ${error.message}`); } } }