From 8659c06589c8b526a1d158ef16d8cb176fb94913 Mon Sep 17 00:00:00 2001 From: Tasbi Tasbi Date: Sat, 16 Nov 2024 23:36:12 +0000 Subject: [PATCH 1/7] made some changes but image upload pending --- package-lock.json | 5 +- src/App.tsx | 104 ++++++++++++++++++++++--------- src/core/useRcbPlugin.ts | 97 +++++++++++++++++++++------- src/types/InputValidatorBlock.ts | 8 ++- src/utils/getValidator.ts | 41 +++++++++--- 5 files changed, 186 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77ea639..ae970c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { - "name": "rcb-plugin", + "name": "@rcb-plugins/input-validator", "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "rcb-plugin", + "name": "@rcb-plugins/input-validator", "version": "0.1.1", + "license": "MIT", "devDependencies": { "@eslint/js": "^9.11.1", "@types/react": "^18.3.10", diff --git a/src/App.tsx b/src/App.tsx index 760f700..f76de88 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,37 +1,79 @@ +// src/App.tsx + +import React from "react"; import ChatBot, { Flow } from "react-chatbotify"; import RcbPlugin from "./factory/RcbPluginFactory"; import { InputValidatorBlock } from "./types/InputValidatorBlock"; +import { ValidationResult } from "./types/ValidationResult"; const App = () => { - // initialize example plugin - const plugins = [RcbPlugin()]; - - // example flow for testing - const flow: Flow = { - start: { - message: "Hey there, please enter your age!", - path: "try_again", - validateInput: (userInput: string) => { - if (typeof userInput === "string" && !Number.isNaN(Number(userInput))) { - return {success: true}; - } - return {success: false, promptContent: "Age must be a number!", promptDuration: 3000, promptType: "error", highlightTextArea: true}; - } - } as InputValidatorBlock, - try_again : { - message: "Nice, you passed the input validation!", - path: "start", - } - } - - return ( - - ); -} - -export default App; \ No newline at end of file + // Initialize the plugin + const plugins = [RcbPlugin()]; + + // Define the validation function for file uploads + const validateFile = (userInput?: File): ValidationResult => { + if (!userInput) { + return { success: false, promptContent: "No file selected.", promptType: "error" }; + } + + const allowedTypes = ["image/jpeg", "image/png"]; + const maxSizeInBytes = 5 * 1024 * 1024; // 5MB + + if (!allowedTypes.includes(userInput.type)) { + return { + success: false, + promptContent: "Only JPEG and PNG images are allowed.", + promptType: "error", + }; + } + + if (userInput.size > maxSizeInBytes) { + return { + success: false, + promptContent: "File size must be less than 5MB.", + promptType: "error", + }; + } + + return { success: true }; + }; + + // Define the flow + const flow: Flow = { + start: { + message: "Hey there! Please enter your age.", + path: "age_validation", + validateInput: (userInput?: string) => { + if (userInput && !Number.isNaN(Number(userInput))) { + return { success: true }; + } + return { + success: false, + promptContent: "Age must be a number!", + promptDuration: 3000, + promptType: "error", + highlightTextArea: true, + }; + }, + } as InputValidatorBlock, + age_validation: { + message: "Great! Now please upload a profile picture (JPEG or PNG).", + path: "file_upload_validation", + validateInput: validateFile, + } as InputValidatorBlock, + file_upload_validation: { + message: "Thank you! Your profile picture has been uploaded successfully.", + path: "end", + }, + end: { + message: "This is the end of the flow. Thank you!", + }, + }; + + return ( + + ); +}; + +export default App; diff --git a/src/core/useRcbPlugin.ts b/src/core/useRcbPlugin.ts index b8798ac..7b77c93 100644 --- a/src/core/useRcbPlugin.ts +++ b/src/core/useRcbPlugin.ts @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useBotId, RcbUserSubmitTextEvent, + RcbUserUploadFileEvent, useToasts, useFlow, useStyles, @@ -17,7 +18,7 @@ import { getValidator } from "../utils/getValidator"; /** * Plugin hook that handles all the core logic. * - * @param pluginConfig configurations for the plugin + * @param pluginConfig Configurations for the plugin. */ const useRcbPlugin = (pluginConfig?: PluginConfig) => { const { showToast } = useToasts(); @@ -31,67 +32,116 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => { useEffect(() => { /** - * Handles the user submitting input event. + * Handles the user submitting text input event. * - * @param event event emitted when user submits input + * @param event Event emitted when user submits text input. */ - const handleUserSubmitText = (event: RcbUserSubmitTextEvent): void => { - // gets validator and if no validator, return - const validator = getValidator(event, getBotId(), getFlow()); + const handleUserSubmitText = (event: Event): void => { + const rcbEvent = event as RcbUserSubmitTextEvent; + + // Get validator and if no validator, return + const validator = getValidator(rcbEvent, getBotId(), getFlow()); if (!validator) { return; } - // gets and checks validation result + // Get and check validation result const validationResult = validator( - event.data.inputText + rcbEvent.data.inputText ) as ValidationResult; if (!validationResult?.success) { event.preventDefault(); } - // if nothing to prompt, return + // If nothing to prompt, return if (!validationResult.promptContent) { return; } - // if this is the first plugin toast, preserve original styles for restoration later + // Preserve original styles if this is the first plugin toast if (numPluginToasts === 0) { - originalStyles.current = structuredClone(styles) + originalStyles.current = structuredClone(styles); } const promptStyles = getPromptStyles( validationResult, mergedPluginConfig ); - // update toast with prompt styles + // Update styles with prompt styles updateStyles(promptStyles); - // shows prompt toast to user + // Show prompt toast to user showToast( validationResult.promptContent, validationResult.promptDuration ?? 3000 ); - // increases number of plugin toasts by 1 + // Increase number of plugin toasts by 1 setNumPluginToasts((prev) => prev + 1); }; /** - * Handles the dismiss toast event. + * Handles the user uploading a file event. * - * @param event event emitted when toast is dismissed + * @param event Event emitted when user uploads a file. + */ + const handleUserUploadFile = (event: Event): void => { + const rcbEvent = event as RcbUserUploadFileEvent; + const file: File = rcbEvent.data.files[0]; + + // Get validator and if no validator, return + const validator = getValidator(rcbEvent, getBotId(), getFlow()); + if (!validator) { + return; + } + + // Perform validation + const validationResult = validator(file) as ValidationResult; + if (!validationResult?.success) { + event.preventDefault(); + } + + // Show prompt if necessary + if (validationResult.promptContent) { + // Preserve original styles if this is the first plugin toast + if (numPluginToasts === 0) { + originalStyles.current = structuredClone(styles); + } + const promptStyles = getPromptStyles( + validationResult, + mergedPluginConfig + ); + + // Update styles with prompt styles + updateStyles(promptStyles); + + // Show prompt toast to user + showToast( + validationResult.promptContent, + validationResult.promptDuration ?? 3000 + ); + + // Increase number of plugin toasts by 1 + setNumPluginToasts((prev) => prev + 1); + } + }; + + /** + * Handles the dismiss toast event. */ const handleDismissToast = (): void => { setNumPluginToasts((prev) => prev - 1); }; - // adds required events + // Add required event listeners window.addEventListener("rcb-user-submit-text", handleUserSubmitText); + window.addEventListener("rcb-user-upload-file", handleUserUploadFile); window.addEventListener("rcb-dismiss-toast", handleDismissToast); return () => { + // Remove event listeners window.removeEventListener("rcb-user-submit-text", handleUserSubmitText); + window.removeEventListener("rcb-user-upload-file", handleUserUploadFile); window.removeEventListener("rcb-dismiss-toast", handleDismissToast); }; }, [ @@ -101,28 +151,29 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => { updateStyles, styles, mergedPluginConfig, - numPluginToasts + numPluginToasts, ]); - // restores original styles when plugin toasts are all dismissed + // Restore original styles when all plugin toasts are dismissed useEffect(() => { if (numPluginToasts === 0) { setTimeout(() => { replaceStyles(originalStyles.current); }); } - }, [numPluginToasts, replaceStyles, originalStyles]); + }, [numPluginToasts, replaceStyles]); - // initializes plugin metadata with plugin name + // Initialize plugin metadata with plugin name const pluginMetaData: ReturnType = { - name: "@rcb-plugins/input-validator" + name: "@rcb-plugins/input-validator", }; - // adds required events in settings if auto config is true + // Add required events in settings if autoConfig is true if (mergedPluginConfig.autoConfig) { pluginMetaData.settings = { event: { rcbUserSubmitText: true, + rcbUserUploadFile: true, rcbDismissToast: true, }, }; diff --git a/src/types/InputValidatorBlock.ts b/src/types/InputValidatorBlock.ts index ffb923d..4997dc1 100644 --- a/src/types/InputValidatorBlock.ts +++ b/src/types/InputValidatorBlock.ts @@ -1,9 +1,11 @@ +// src/types/InputValidatorBlock.ts + import { Block } from "react-chatbotify"; import { ValidationResult } from "./ValidationResult"; /** - * Extends the Block from React ChatBotify to support inputValidator attribute. + * Extends the Block from React ChatBotify to support validateInput attribute. */ export type InputValidatorBlock = Block & { - validateInput: (userInput?: string) => ValidationResult; -}; \ No newline at end of file + validateInput: (userInput?: string | File) => ValidationResult; +}; diff --git a/src/utils/getValidator.ts b/src/utils/getValidator.ts index e61f27d..09de1bf 100644 --- a/src/utils/getValidator.ts +++ b/src/utils/getValidator.ts @@ -1,29 +1,50 @@ -import { Flow, RcbUserSubmitTextEvent } from "react-chatbotify"; +// src/utils/getValidator.ts + +import { Flow, RcbUserSubmitTextEvent, RcbUserUploadFileEvent } from "react-chatbotify"; import { InputValidatorBlock } from "../types/InputValidatorBlock"; /** - * Retrieves the validator function and returns null if not applicable. + * Union type for user events that can be validated. */ -export const getValidator = (event: RcbUserSubmitTextEvent, currBotId: string | null, currFlow: Flow) => { - if (currBotId !== event.detail.botId) { +type RcbUserEvent = RcbUserSubmitTextEvent | RcbUserUploadFileEvent; + +/** + * Retrieves the validator function from the current flow block. + * + * @param event The event emitted by the user action (text submission or file upload). + * @param currBotId The current bot ID. + * @param currFlow The current flow object. + * @returns The validator function if it exists, otherwise undefined. + */ +export const getValidator = ( + event: RcbUserEvent, + currBotId: string | null, + currFlow: Flow +) => { + if (!event.detail) { return; } - - if (!event.detail.currPath) { + + const { botId, currPath } = event.detail; + + if (currBotId !== botId) { + return; + } + + if (!currPath) { return; } - const currBlock = currFlow[event.detail.currPath] as InputValidatorBlock; + const currBlock = currFlow[currPath] as InputValidatorBlock; if (!currBlock) { return; } const validator = currBlock.validateInput; - const isValidatorFunction = - validator && typeof validator === "function"; + const isValidatorFunction = validator && typeof validator === "function"; if (!isValidatorFunction) { return; } return validator; -} \ No newline at end of file +}; From 6b7c798bea4eeca7286615a2db66dc64a61f2619 Mon Sep 17 00:00:00 2001 From: Tasbi Tasbi Date: Wed, 20 Nov 2024 06:26:33 +0000 Subject: [PATCH 2/7] file upload working, still some more validation to do --- src/App.tsx | 129 +++++++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 63 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f76de88..816be1b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,79 +1,82 @@ -// src/App.tsx - import React from "react"; import ChatBot, { Flow } from "react-chatbotify"; - import RcbPlugin from "./factory/RcbPluginFactory"; import { InputValidatorBlock } from "./types/InputValidatorBlock"; -import { ValidationResult } from "./types/ValidationResult"; const App = () => { - // Initialize the plugin - const plugins = [RcbPlugin()]; + // Initialize the plugin + const plugins = [RcbPlugin()]; - // Define the validation function for file uploads - const validateFile = (userInput?: File): ValidationResult => { - if (!userInput) { - return { success: false, promptContent: "No file selected.", promptType: "error" }; - } + const handleUpload = (params: { files?: FileList }) => { + const files = params.files; - const allowedTypes = ["image/jpeg", "image/png"]; - const maxSizeInBytes = 5 * 1024 * 1024; // 5MB + if (!files || files.length === 0) { + return { success: false, promptContent: "No file selected." }; + } - if (!allowedTypes.includes(userInput.type)) { - return { - success: false, - promptContent: "Only JPEG and PNG images are allowed.", - promptType: "error", - }; - } + const file = files[0]; + const maxSize = 5 * 1024 * 1024; // 5MB - if (userInput.size > maxSizeInBytes) { - return { - success: false, - promptContent: "File size must be less than 5MB.", - promptType: "error", - }; - } + // Debugging log for file details + console.log("Uploaded file details:", { + name: file.name, + type: file.type, + size: file.size, + }); + + // Adjusted MIME type checking + if (!file.type.match(/^image\/(jpeg|jpg|png)$/)) { + return { + success: false, + promptContent: "Only JPEG and PNG files are allowed.", + }; + } - return { success: true }; - }; + if (file.size > maxSize) { + return { + success: false, + promptContent: "File size must be less than 5MB.", + }; + } - // Define the flow - const flow: Flow = { - start: { - message: "Hey there! Please enter your age.", - path: "age_validation", - validateInput: (userInput?: string) => { - if (userInput && !Number.isNaN(Number(userInput))) { - return { success: true }; - } - return { - success: false, - promptContent: "Age must be a number!", - promptDuration: 3000, - promptType: "error", - highlightTextArea: true, - }; - }, - } as InputValidatorBlock, - age_validation: { - message: "Great! Now please upload a profile picture (JPEG or PNG).", - path: "file_upload_validation", - validateInput: validateFile, - } as InputValidatorBlock, - file_upload_validation: { - message: "Thank you! Your profile picture has been uploaded successfully.", - path: "end", - }, - end: { - message: "This is the end of the flow. Thank you!", - }, - }; + // If all checks pass + console.log("File validation passed:", file.name); + return { success: true }; + }; + + const flow: Flow = { + start: { + message: "Hey there! Please enter your age.", + path: "age_validation", + validateInput: (userInput?: string) => { + if (userInput && !Number.isNaN(Number(userInput))) { + return { success: true }; + } + return { + success: false, + promptContent: "Age must be a number!", + promptDuration: 3000, + promptType: "error", + highlightTextArea: true, + }; + }, + } as InputValidatorBlock, + age_validation: { + message: "Great! Now please upload a profile picture (JPEG or PNG).", + file: (params) => handleUpload(params), + path: "file_upload_validation", + // Removed validateInput + }, + file_upload_validation: { + message: "Thank you! Your profile picture has been uploaded successfully.", + path: "end", + }, + end: { + message: "This is the end of the flow. Thank you!", + }, + }; - return ( - - ); + return ; }; export default App; From 00f183b665a3cabb799d7f7b57d744e023f8fcc0 Mon Sep 17 00:00:00 2001 From: Tasbi Tasbi Date: Fri, 22 Nov 2024 03:18:46 +0000 Subject: [PATCH 3/7] upload file done --- src/App.tsx | 87 +++++++++++++++++--------------- src/core/useRcbPlugin.ts | 63 +++++++++++------------ src/types/InputValidatorBlock.ts | 11 ++-- src/utils/getValidator.ts | 37 +++++++------- src/utils/validateFile.ts | 50 ++++++++++++++++++ 5 files changed, 147 insertions(+), 101 deletions(-) create mode 100644 src/utils/validateFile.ts diff --git a/src/App.tsx b/src/App.tsx index 816be1b..d52b9ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,48 +2,11 @@ import React from "react"; import ChatBot, { Flow } from "react-chatbotify"; import RcbPlugin from "./factory/RcbPluginFactory"; import { InputValidatorBlock } from "./types/InputValidatorBlock"; +import { validateFile } from "./utils/validateFile"; const App = () => { - // Initialize the plugin const plugins = [RcbPlugin()]; - const handleUpload = (params: { files?: FileList }) => { - const files = params.files; - - if (!files || files.length === 0) { - return { success: false, promptContent: "No file selected." }; - } - - const file = files[0]; - const maxSize = 5 * 1024 * 1024; // 5MB - - // Debugging log for file details - console.log("Uploaded file details:", { - name: file.name, - type: file.type, - size: file.size, - }); - - // Adjusted MIME type checking - if (!file.type.match(/^image\/(jpeg|jpg|png)$/)) { - return { - success: false, - promptContent: "Only JPEG and PNG files are allowed.", - }; - } - - if (file.size > maxSize) { - return { - success: false, - promptContent: "File size must be less than 5MB.", - }; - } - - // If all checks pass - console.log("File validation passed:", file.name); - return { success: true }; - }; - const flow: Flow = { start: { message: "Hey there! Please enter your age.", @@ -61,21 +24,61 @@ const App = () => { }; }, } as InputValidatorBlock, + age_validation: { message: "Great! Now please upload a profile picture (JPEG or PNG).", - file: (params) => handleUpload(params), path: "file_upload_validation", - // Removed validateInput - }, + validateInput: (userInput?: string) => { + console.log("validateInput called with userInput:", userInput); + + // Allow empty input or file names with allowed extensions + if ( + !userInput || + /\.(jpg|jpeg|png)$/i.test(userInput.trim()) + ) { + return { success: true }; + } + + // Disallow other text inputs + return { + success: false, + promptContent: "Please upload a file.", + promptDuration: 3000, + promptType: "error", + }; + }, + validateFile: (file?: File) => { + return validateFile(file); // Validate file input + }, + file: async ({ files }) => { + console.log("Files received:", files); + + if (files && files[0]) { + const validationResult = validateFile(files[0]); + if (!validationResult.success) { + console.error(validationResult.promptContent); + return; + } + console.log("File uploaded successfully:", files[0]); + } else { + console.error("No file provided."); + } + }, + } as InputValidatorBlock, + file_upload_validation: { - message: "Thank you! Your profile picture has been uploaded successfully.", + message: + "Thank you! Your profile picture has been uploaded successfully.", path: "end", }, + end: { message: "This is the end of the flow. Thank you!", }, }; + + return ; }; diff --git a/src/core/useRcbPlugin.ts b/src/core/useRcbPlugin.ts index 7b77c93..9094884 100644 --- a/src/core/useRcbPlugin.ts +++ b/src/core/useRcbPlugin.ts @@ -85,45 +85,42 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => { * * @param event Event emitted when user uploads a file. */ + // useRcbPlugin.ts + const handleUserUploadFile = (event: Event): void => { const rcbEvent = event as RcbUserUploadFileEvent; - const file: File = rcbEvent.data.files[0]; - - // Get validator and if no validator, return - const validator = getValidator(rcbEvent, getBotId(), getFlow()); - if (!validator) { - return; + const file: File | undefined = rcbEvent.data?.files?.[0]; + + if (!file) { + console.error("No file uploaded."); + event.preventDefault(); + return; } - - // Perform validation - const validationResult = validator(file) as ValidationResult; - if (!validationResult?.success) { - event.preventDefault(); + + const validator = getValidator( + rcbEvent, + getBotId(), + getFlow(), + "validateFileInput" + ); + + if (!validator) { + console.error("Validator not found for file input."); + return; } - - // Show prompt if necessary + + const validationResult = validator(file); + + if (!validationResult.success) { + console.error("Validation failed:", validationResult); if (validationResult.promptContent) { - // Preserve original styles if this is the first plugin toast - if (numPluginToasts === 0) { - originalStyles.current = structuredClone(styles); - } - const promptStyles = getPromptStyles( - validationResult, - mergedPluginConfig - ); - - // Update styles with prompt styles - updateStyles(promptStyles); - - // Show prompt toast to user - showToast( - validationResult.promptContent, - validationResult.promptDuration ?? 3000 - ); - - // Increase number of plugin toasts by 1 - setNumPluginToasts((prev) => prev + 1); + showToast(validationResult.promptContent, validationResult.promptDuration ?? 3000); + } + event.preventDefault(); + return; } + + console.log("Validation successful:", validationResult); }; /** diff --git a/src/types/InputValidatorBlock.ts b/src/types/InputValidatorBlock.ts index 4997dc1..0bc120e 100644 --- a/src/types/InputValidatorBlock.ts +++ b/src/types/InputValidatorBlock.ts @@ -3,9 +3,8 @@ import { Block } from "react-chatbotify"; import { ValidationResult } from "./ValidationResult"; -/** - * Extends the Block from React ChatBotify to support validateInput attribute. - */ -export type InputValidatorBlock = Block & { - validateInput: (userInput?: string | File) => ValidationResult; -}; +export type InputValidatorBlock = Omit & { + file?: (params: { files?: FileList }) => void | Promise; // Updated + validateInput?: (userInput?: string) => ValidationResult; + validateFileInput?: (file?: File) => ValidationResult; +}; \ No newline at end of file diff --git a/src/utils/getValidator.ts b/src/utils/getValidator.ts index 09de1bf..da7d734 100644 --- a/src/utils/getValidator.ts +++ b/src/utils/getValidator.ts @@ -2,6 +2,7 @@ import { Flow, RcbUserSubmitTextEvent, RcbUserUploadFileEvent } from "react-chatbotify"; import { InputValidatorBlock } from "../types/InputValidatorBlock"; +import { ValidationResult } from "../types/ValidationResult"; /** * Union type for user events that can be validated. @@ -16,35 +17,31 @@ type RcbUserEvent = RcbUserSubmitTextEvent | RcbUserUploadFileEvent; * @param currFlow The current flow object. * @returns The validator function if it exists, otherwise undefined. */ -export const getValidator = ( +export const getValidator = ( event: RcbUserEvent, currBotId: string | null, - currFlow: Flow -) => { + currFlow: Flow, + validatorType: "validateInput" | "validateFileInput" = "validateInput" + ): ((input: T) => ValidationResult) | undefined => { if (!event.detail) { - return; + return; } - + const { botId, currPath } = event.detail; - + if (currBotId !== botId) { - return; + return; } - + if (!currPath) { - return; + return; } - + const currBlock = currFlow[currPath] as InputValidatorBlock; if (!currBlock) { - return; + return; } - - const validator = currBlock.validateInput; - const isValidatorFunction = validator && typeof validator === "function"; - if (!isValidatorFunction) { - return; - } - - return validator; -}; + + const validator = currBlock[validatorType] as ((input: T) => ValidationResult) | undefined; + return typeof validator === "function" ? validator : undefined; + }; \ No newline at end of file diff --git a/src/utils/validateFile.ts b/src/utils/validateFile.ts new file mode 100644 index 0000000..492552c --- /dev/null +++ b/src/utils/validateFile.ts @@ -0,0 +1,50 @@ +import { ValidationResult } from "../types/ValidationResult"; + +/** + * Validates the uploaded file. + * Ensures the file is of allowed type and size, and rejects non-file inputs. + */ +export const validateFile = (file?: File): ValidationResult => { + const allowedTypes = ["image/jpeg", "image/png"]; + const maxSizeInBytes = 5 * 1024 * 1024; // 5MB + + // Check if no file is provided + if (!file) { + return { + success: false, + promptContent: "No file uploaded.", + promptDuration: 3000, + promptType: "error", + }; + } + + // Check if the input is not a File object (e.g., text input or invalid type) + if (!(file instanceof File)) { + return { + success: false, + promptContent: "Invalid input. Please upload a valid file.", + promptDuration: 3000, + promptType: "error", + }; + } + + // Validate file type + if (!allowedTypes.includes(file.type)) { + return { + success: false, + promptContent: "Only JPEG or PNG files are allowed.", + promptType: "error", + }; + } + + // Validate file size + if (file.size > maxSizeInBytes) { + return { + success: false, + promptContent: "File size must be less than 5MB.", + promptType: "error", + }; + } + + return { success: true }; +}; From d42d6e973dc6d6bb90ade4a235962d51cc1c89e0 Mon Sep 17 00:00:00 2001 From: Tasbi Tasbi Date: Fri, 22 Nov 2024 04:29:22 +0000 Subject: [PATCH 4/7] made some changes --- src/App.tsx | 40 +++++++------ src/types/InputValidatorBlock.ts | 4 ++ src/utils/validateFile.ts | 97 ++++++++++++++++++-------------- 3 files changed, 80 insertions(+), 61 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d52b9ca..7eec38c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,14 @@ -import React from "react"; import ChatBot, { Flow } from "react-chatbotify"; + import RcbPlugin from "./factory/RcbPluginFactory"; import { InputValidatorBlock } from "./types/InputValidatorBlock"; import { validateFile } from "./utils/validateFile"; const App = () => { + // initialize example plugin const plugins = [RcbPlugin()]; + // example flow for testing const flow: Flow = { start: { message: "Hey there! Please enter your age.", @@ -31,9 +33,9 @@ const App = () => { validateInput: (userInput?: string) => { console.log("validateInput called with userInput:", userInput); - // Allow empty input or file names with allowed extensions + if ( - !userInput || + userInput && /\.(jpg|jpeg|png)$/i.test(userInput.trim()) ) { return { success: true }; @@ -42,44 +44,46 @@ const App = () => { // Disallow other text inputs return { success: false, - promptContent: "Please upload a file.", + promptContent: "Please upload a valid file (JPEG or PNG). Empty inputs are not allowed.", promptDuration: 3000, promptType: "error", }; }, - validateFile: (file?: File) => { + validateFileInput: (file?: File) => { return validateFile(file); // Validate file input }, file: async ({ files }) => { console.log("Files received:", files); - + if (files && files[0]) { const validationResult = validateFile(files[0]); if (!validationResult.success) { console.error(validationResult.promptContent); - return; + // Return early to prevent success + return { success: false }; } console.log("File uploaded successfully:", files[0]); } else { console.error("No file provided."); } }, + } as InputValidatorBlock, file_upload_validation: { message: - "Thank you! Your profile picture has been uploaded successfully.", - path: "end", + "Thank you! Your picture has been uploaded successfully. You passed the file upload validation!", + path: "start", }, + } - end: { - message: "This is the end of the flow. Thank you!", - }, - }; - - - - return ; -}; + return ( + + ); +} export default App; diff --git a/src/types/InputValidatorBlock.ts b/src/types/InputValidatorBlock.ts index 0bc120e..beb2792 100644 --- a/src/types/InputValidatorBlock.ts +++ b/src/types/InputValidatorBlock.ts @@ -3,6 +3,10 @@ import { Block } from "react-chatbotify"; import { ValidationResult } from "./ValidationResult"; +/** + * Extends the Block from React ChatBotify to support inputValidator attribute. + */ + export type InputValidatorBlock = Omit & { file?: (params: { files?: FileList }) => void | Promise; // Updated validateInput?: (userInput?: string) => ValidationResult; diff --git a/src/utils/validateFile.ts b/src/utils/validateFile.ts index 492552c..319b902 100644 --- a/src/utils/validateFile.ts +++ b/src/utils/validateFile.ts @@ -5,46 +5,57 @@ import { ValidationResult } from "../types/ValidationResult"; * Ensures the file is of allowed type and size, and rejects non-file inputs. */ export const validateFile = (file?: File): ValidationResult => { - const allowedTypes = ["image/jpeg", "image/png"]; - const maxSizeInBytes = 5 * 1024 * 1024; // 5MB - - // Check if no file is provided - if (!file) { - return { - success: false, - promptContent: "No file uploaded.", - promptDuration: 3000, - promptType: "error", - }; - } - - // Check if the input is not a File object (e.g., text input or invalid type) - if (!(file instanceof File)) { - return { - success: false, - promptContent: "Invalid input. Please upload a valid file.", - promptDuration: 3000, - promptType: "error", - }; - } - - // Validate file type - if (!allowedTypes.includes(file.type)) { - return { - success: false, - promptContent: "Only JPEG or PNG files are allowed.", - promptType: "error", - }; - } - - // Validate file size - if (file.size > maxSizeInBytes) { - return { - success: false, - promptContent: "File size must be less than 5MB.", - promptType: "error", - }; - } - - return { success: true }; -}; + const allowedTypes = ["image/jpeg", "image/png"]; + const maxSizeInBytes = 5 * 1024 * 1024; // 5MB + + // Check if no file is provided + if (!file) { + return { + success: false, + promptContent: "No file uploaded.", + promptDuration: 3000, + promptType: "error", + }; + } + + // Check if the input is not a File object (e.g., text input or invalid type) + if (!(file instanceof File)) { + return { + success: false, + promptContent: "Invalid input. Please upload a valid file.", + promptDuration: 3000, + promptType: "error", + }; + } + + // Check if the file is empty + if (file.size === 0) { + return { + success: false, + promptContent: "The uploaded file is empty. Please upload a valid file.", + promptDuration: 3000, + promptType: "error", + }; + } + + // Validate file type + if (!allowedTypes.includes(file.type)) { + return { + success: false, + promptContent: "Only JPEG or PNG files are allowed.", + promptType: "error", + }; + } + + // Validate file size + if (file.size > maxSizeInBytes) { + return { + success: false, + promptContent: "File size must be less than 5MB.", + promptType: "error", + }; + } + + return { success: true }; + }; + \ No newline at end of file From 7dc6b27830614063066b673ce3e15e3adea9142c Mon Sep 17 00:00:00 2001 From: Tasbi Tasbi Date: Fri, 22 Nov 2024 06:45:14 +0000 Subject: [PATCH 5/7] made changes as requested --- src/App.tsx | 43 ++++++++++++++------------------ src/core/useRcbPlugin.ts | 7 +++++- src/types/InputValidatorBlock.ts | 4 +-- src/utils/getValidator.ts | 10 ++++---- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7eec38c..f5300a3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,15 +5,15 @@ import { InputValidatorBlock } from "./types/InputValidatorBlock"; import { validateFile } from "./utils/validateFile"; const App = () => { - // initialize example plugin + // Initialize the plugin const plugins = [RcbPlugin()]; - // example flow for testing + // Example flow for testing const flow: Flow = { start: { message: "Hey there! Please enter your age.", path: "age_validation", - validateInput: (userInput?: string) => { + validateTextInput: (userInput?: string) => { if (userInput && !Number.isNaN(Number(userInput))) { return { success: true }; } @@ -28,23 +28,23 @@ const App = () => { } as InputValidatorBlock, age_validation: { - message: "Great! Now please upload a profile picture (JPEG or PNG).", + message: + "Great! Now please upload a profile picture (JPEG or PNG) or provide a URL.", path: "file_upload_validation", - validateInput: (userInput?: string) => { - console.log("validateInput called with userInput:", userInput); + chatDisabled: true, // Set to true if you want to disable text input + validateTextInput: (userInput?: string) => { + console.log("validateTextInput called with userInput:", userInput); - - if ( - userInput && - /\.(jpg|jpeg|png)$/i.test(userInput.trim()) - ) { + if (userInput && userInput.trim().length > 0) { + // Optionally, validate if the input is a valid URL + // For simplicity, we'll accept any non-empty text return { success: true }; } - // Disallow other text inputs return { success: false, - promptContent: "Please upload a valid file (JPEG or PNG). Empty inputs are not allowed.", + promptContent: + "Please provide a valid URL or upload a file.", promptDuration: 3000, promptType: "error", }; @@ -54,7 +54,7 @@ const App = () => { }, file: async ({ files }) => { console.log("Files received:", files); - + if (files && files[0]) { const validationResult = validateFile(files[0]); if (!validationResult.success) { @@ -67,23 +67,18 @@ const App = () => { console.error("No file provided."); } }, - } as InputValidatorBlock, file_upload_validation: { message: - "Thank you! Your picture has been uploaded successfully. You passed the file upload validation!", + "Thank you! Your input has been received. You passed the validation!", path: "start", }, - } + }; return ( - - ); -} + + ); +}; export default App; diff --git a/src/core/useRcbPlugin.ts b/src/core/useRcbPlugin.ts index 9094884..d5c50bb 100644 --- a/src/core/useRcbPlugin.ts +++ b/src/core/useRcbPlugin.ts @@ -40,7 +40,12 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => { const rcbEvent = event as RcbUserSubmitTextEvent; // Get validator and if no validator, return - const validator = getValidator(rcbEvent, getBotId(), getFlow()); + const validator = getValidator( + rcbEvent, + getBotId(), + getFlow(), + "validateTextInput" + ); if (!validator) { return; } diff --git a/src/types/InputValidatorBlock.ts b/src/types/InputValidatorBlock.ts index beb2792..ef3c1fb 100644 --- a/src/types/InputValidatorBlock.ts +++ b/src/types/InputValidatorBlock.ts @@ -8,7 +8,7 @@ import { ValidationResult } from "./ValidationResult"; */ export type InputValidatorBlock = Omit & { - file?: (params: { files?: FileList }) => void | Promise; // Updated - validateInput?: (userInput?: string) => ValidationResult; + file?: (params: { files?: FileList }) => void | Promise; + validateTextInput?: (userInput?: string) => ValidationResult; validateFileInput?: (file?: File) => ValidationResult; }; \ No newline at end of file diff --git a/src/utils/getValidator.ts b/src/utils/getValidator.ts index da7d734..256db53 100644 --- a/src/utils/getValidator.ts +++ b/src/utils/getValidator.ts @@ -18,11 +18,11 @@ type RcbUserEvent = RcbUserSubmitTextEvent | RcbUserUploadFileEvent; * @returns The validator function if it exists, otherwise undefined. */ export const getValidator = ( - event: RcbUserEvent, - currBotId: string | null, - currFlow: Flow, - validatorType: "validateInput" | "validateFileInput" = "validateInput" - ): ((input: T) => ValidationResult) | undefined => { + event: RcbUserEvent, + currBotId: string | null, + currFlow: Flow, + validatorType: "validateTextInput" | "validateFileInput" = "validateTextInput" +): ((input: T) => ValidationResult) | undefined => { if (!event.detail) { return; } From 5ef1c5c59bbd817550f2a7ee74778f72771ce18b Mon Sep 17 00:00:00 2001 From: Tasbi Tasbi Date: Fri, 22 Nov 2024 13:55:38 +0000 Subject: [PATCH 6/7] final changes --- src/utils/validateFile.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/utils/validateFile.ts b/src/utils/validateFile.ts index 319b902..f690f10 100644 --- a/src/utils/validateFile.ts +++ b/src/utils/validateFile.ts @@ -18,16 +18,6 @@ export const validateFile = (file?: File): ValidationResult => { }; } - // Check if the input is not a File object (e.g., text input or invalid type) - if (!(file instanceof File)) { - return { - success: false, - promptContent: "Invalid input. Please upload a valid file.", - promptDuration: 3000, - promptType: "error", - }; - } - // Check if the file is empty if (file.size === 0) { return { From 7773d34d7e12f511b48040f2cef6178de62fd4d1 Mon Sep 17 00:00:00 2001 From: Tasbi Tasbi Date: Sat, 23 Nov 2024 01:45:05 +0000 Subject: [PATCH 7/7] Requested changes done and cleaned up the code --- src/App.tsx | 30 ++--------- src/core/useRcbPlugin.ts | 58 ++++++++++----------- src/types/InputValidatorBlock.ts | 12 ++--- src/utils/getValidator.ts | 26 +++------- src/utils/validateFile.ts | 87 +++++++++++++++++--------------- 5 files changed, 88 insertions(+), 125 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f5300a3..202451d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,43 +31,21 @@ const App = () => { message: "Great! Now please upload a profile picture (JPEG or PNG) or provide a URL.", path: "file_upload_validation", - chatDisabled: true, // Set to true if you want to disable text input - validateTextInput: (userInput?: string) => { - console.log("validateTextInput called with userInput:", userInput); - - if (userInput && userInput.trim().length > 0) { - // Optionally, validate if the input is a valid URL - // For simplicity, we'll accept any non-empty text - return { success: true }; - } - - return { - success: false, - promptContent: - "Please provide a valid URL or upload a file.", - promptDuration: 3000, - promptType: "error", - }; - }, + chatDisabled: true, // Text input is disabled validateFileInput: (file?: File) => { - return validateFile(file); // Validate file input + return validateFile(file); // Validation is handled here }, file: async ({ files }) => { console.log("Files received:", files); - + if (files && files[0]) { - const validationResult = validateFile(files[0]); - if (!validationResult.success) { - console.error(validationResult.promptContent); - // Return early to prevent success - return { success: false }; - } console.log("File uploaded successfully:", files[0]); } else { console.error("No file provided."); } }, } as InputValidatorBlock, + file_upload_validation: { message: diff --git a/src/core/useRcbPlugin.ts b/src/core/useRcbPlugin.ts index d5c50bb..16a3daf 100644 --- a/src/core/useRcbPlugin.ts +++ b/src/core/useRcbPlugin.ts @@ -18,7 +18,7 @@ import { getValidator } from "../utils/getValidator"; /** * Plugin hook that handles all the core logic. * - * @param pluginConfig Configurations for the plugin. + * @param pluginConfig configurations for the plugin */ const useRcbPlugin = (pluginConfig?: PluginConfig) => { const { showToast } = useToasts(); @@ -45,7 +45,7 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => { getBotId(), getFlow(), "validateTextInput" - ); + ); if (!validator) { return; } @@ -85,51 +85,49 @@ const useRcbPlugin = (pluginConfig?: PluginConfig) => { setNumPluginToasts((prev) => prev + 1); }; - /** - * Handles the user uploading a file event. - * - * @param event Event emitted when user uploads a file. - */ - // useRcbPlugin.ts - const handleUserUploadFile = (event: Event): void => { const rcbEvent = event as RcbUserUploadFileEvent; const file: File | undefined = rcbEvent.data?.files?.[0]; - + if (!file) { - console.error("No file uploaded."); - event.preventDefault(); - return; + console.error("No file uploaded."); + event.preventDefault(); + return; } - + const validator = getValidator( - rcbEvent, - getBotId(), - getFlow(), - "validateFileInput" + rcbEvent, + getBotId(), + getFlow(), + "validateFileInput" ); - + if (!validator) { - console.error("Validator not found for file input."); - return; + console.error("Validator not found for file input."); + return; } - + const validationResult = validator(file); - + if (!validationResult.success) { - console.error("Validation failed:", validationResult); - if (validationResult.promptContent) { - showToast(validationResult.promptContent, validationResult.promptDuration ?? 3000); - } - event.preventDefault(); - return; + console.error("Validation failed:", validationResult); + if (validationResult.promptContent) { + showToast( + validationResult.promptContent, + validationResult.promptDuration ?? 3000 + ); + } + event.preventDefault(); + return; } - + console.log("Validation successful:", validationResult); }; /** * Handles the dismiss toast event. + * + * @param event Event emitted when toast is dismissed. */ const handleDismissToast = (): void => { setNumPluginToasts((prev) => prev - 1); diff --git a/src/types/InputValidatorBlock.ts b/src/types/InputValidatorBlock.ts index ef3c1fb..f93fbc7 100644 --- a/src/types/InputValidatorBlock.ts +++ b/src/types/InputValidatorBlock.ts @@ -1,14 +1,10 @@ -// src/types/InputValidatorBlock.ts - import { Block } from "react-chatbotify"; import { ValidationResult } from "./ValidationResult"; /** - * Extends the Block from React ChatBotify to support inputValidator attribute. + * Extends the Block from React ChatBotify to support inputValidator attributes. */ - -export type InputValidatorBlock = Omit & { - file?: (params: { files?: FileList }) => void | Promise; +export type InputValidatorBlock = Block & { validateTextInput?: (userInput?: string) => ValidationResult; - validateFileInput?: (file?: File) => ValidationResult; -}; \ No newline at end of file + validateFileInput?: (files?: FileList) => ValidationResult; // Accepts multiple files +}; diff --git a/src/utils/getValidator.ts b/src/utils/getValidator.ts index 256db53..b217158 100644 --- a/src/utils/getValidator.ts +++ b/src/utils/getValidator.ts @@ -1,5 +1,3 @@ -// src/utils/getValidator.ts - import { Flow, RcbUserSubmitTextEvent, RcbUserUploadFileEvent } from "react-chatbotify"; import { InputValidatorBlock } from "../types/InputValidatorBlock"; import { ValidationResult } from "../types/ValidationResult"; @@ -23,25 +21,15 @@ export const getValidator = ( currFlow: Flow, validatorType: "validateTextInput" | "validateFileInput" = "validateTextInput" ): ((input: T) => ValidationResult) | undefined => { - if (!event.detail) { - return; - } - - const { botId, currPath } = event.detail; - - if (currBotId !== botId) { - return; - } - - if (!currPath) { - return; + if (!event.detail?.currPath || currBotId !== event.detail.botId) { + return; } - - const currBlock = currFlow[currPath] as InputValidatorBlock; + + const currBlock = currFlow[event.detail.currPath] as InputValidatorBlock; if (!currBlock) { - return; + return; } - + const validator = currBlock[validatorType] as ((input: T) => ValidationResult) | undefined; return typeof validator === "function" ? validator : undefined; - }; \ No newline at end of file +}; diff --git a/src/utils/validateFile.ts b/src/utils/validateFile.ts index f690f10..001d801 100644 --- a/src/utils/validateFile.ts +++ b/src/utils/validateFile.ts @@ -1,51 +1,54 @@ import { ValidationResult } from "../types/ValidationResult"; /** - * Validates the uploaded file. - * Ensures the file is of allowed type and size, and rejects non-file inputs. + * Validates uploaded files. + * Ensures each file is of an allowed type and size, and rejects invalid inputs. */ -export const validateFile = (file?: File): ValidationResult => { +export const validateFile = (input?: File | FileList): ValidationResult => { const allowedTypes = ["image/jpeg", "image/png"]; const maxSizeInBytes = 5 * 1024 * 1024; // 5MB - - // Check if no file is provided - if (!file) { - return { - success: false, - promptContent: "No file uploaded.", - promptDuration: 3000, - promptType: "error", - }; - } - - // Check if the file is empty - if (file.size === 0) { - return { - success: false, - promptContent: "The uploaded file is empty. Please upload a valid file.", - promptDuration: 3000, - promptType: "error", - }; - } - - // Validate file type - if (!allowedTypes.includes(file.type)) { - return { - success: false, - promptContent: "Only JPEG or PNG files are allowed.", - promptType: "error", - }; + const files: File[] = input instanceof FileList ? Array.from(input) : input ? [input] : []; + + // Check if no files are provided + if (files.length === 0) { + return { + success: false, + promptContent: "No files uploaded.", + promptDuration: 3000, + promptType: "error", + }; } - - // Validate file size - if (file.size > maxSizeInBytes) { - return { - success: false, - promptContent: "File size must be less than 5MB.", - promptType: "error", - }; + + // Validate each file + for (const file of files) { + // Check if the file is empty + if (file.size === 0) { + return { + success: false, + promptContent: `The file "${file.name}" is empty. Please upload a valid file.`, + promptDuration: 3000, + promptType: "error", + }; + } + + // Validate file type + if (!allowedTypes.includes(file.type)) { + return { + success: false, + promptContent: `The file "${file.name}" is not a valid type. Only JPEG or PNG files are allowed.`, + promptType: "error", + }; + } + + // Validate file size + if (file.size > maxSizeInBytes) { + return { + success: false, + promptContent: `The file "${file.name}" exceeds the 5MB size limit.`, + promptType: "error", + }; + } } - + return { success: true }; - }; - \ No newline at end of file +};