Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validation #2

Merged
merged 7 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 53 additions & 28 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,61 @@ 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()];
// Initialize the 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",
}
}
// Example flow for testing
const flow: Flow = {
start: {
message: "Hey there! Please enter your age.",
path: "age_validation",
validateTextInput: (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,

return (
<ChatBot
id="chatbot-id"
plugins={plugins}
flow={flow}
></ChatBot>
);
}
age_validation: {
message:
"Great! Now please upload a profile picture (JPEG or PNG) or provide a URL.",
path: "file_upload_validation",
chatDisabled: true, // Text input is disabled
validateFileInput: (file?: File) => {
return validateFile(file); // Validation is handled here
},
file: async ({ files }) => {
console.log("Files received:", files);

if (files && files[0]) {
console.log("File uploaded successfully:", files[0]);
} else {
console.error("No file provided.");
}
},
} as InputValidatorBlock,


export default App;
file_upload_validation: {
message:
"Thank you! Your input has been received. You passed the validation!",
Comment on lines +51 to +52
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can go in a single line.

path: "start",
},
};

return (
<ChatBot id="chatbot-id" plugins={plugins} flow={flow}></ChatBot>
);
};

export default App;
93 changes: 72 additions & 21 deletions src/core/useRcbPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
import {
useBotId,
RcbUserSubmitTextEvent,
RcbUserUploadFileEvent,
useToasts,
useFlow,
useStyles,
Expand Down Expand Up @@ -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<string>(
rcbEvent,
getBotId(),
getFlow(),
"validateTextInput"
);
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);
};

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;
}

const validator = getValidator<File>(
rcbEvent,
getBotId(),
getFlow(),
"validateFileInput"
);

if (!validator) {
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.log("Validation successful:", validationResult);
};

/**
* Handles the dismiss toast event.
*
* @param event event emitted when toast is dismissed
* @param event Event emitted when toast is dismissed.
*/
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);
};
}, [
Expand All @@ -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<Plugin> = {
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,
},
};
Expand Down
7 changes: 4 additions & 3 deletions src/types/InputValidatorBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ 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 = Block & {
validateInput: (userInput?: string) => ValidationResult;
};
validateTextInput?: (userInput?: string) => ValidationResult;
validateFileInput?: (files?: FileList) => ValidationResult; // Accepts multiple files
};
40 changes: 23 additions & 17 deletions src/utils/getValidator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { Flow, RcbUserSubmitTextEvent } from "react-chatbotify";
import { Flow, RcbUserSubmitTextEvent, RcbUserUploadFileEvent } from "react-chatbotify";
import { InputValidatorBlock } from "../types/InputValidatorBlock";
import { ValidationResult } from "../types/ValidationResult";

/**
* 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) {
return;
}

if (!event.detail.currPath) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm any reason this was removed in favour of splitting into 2 separate checks?

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 = <T = string | File>(
event: RcbUserEvent,
currBotId: string | null,
currFlow: Flow,
validatorType: "validateTextInput" | "validateFileInput" = "validateTextInput"
): ((input: T) => ValidationResult) | undefined => {
if (!event.detail?.currPath || currBotId !== event.detail.botId) {
return;
}

Expand All @@ -18,12 +30,6 @@ export const getValidator = (event: RcbUserSubmitTextEvent, currBotId: string |
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;
};
Loading