Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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.

109 changes: 78 additions & 31 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,83 @@ 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!",
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 (
<ChatBot
id="chatbot-id"
plugins={plugins}
flow={flow}
></ChatBot>
);
}

export default App;
// Initialize the plugin
const plugins = [RcbPlugin()];

// 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,

age_validation: {
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",
};
},
Copy link
Member

Choose a reason for hiding this comment

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

No need for validateTextInput here, since chatDisabled is already set to true.

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 early to prevent success
return { success: false };
}
console.log("File uploaded successfully:", files[0]);
} else {
console.error("No file provided.");
}
Copy link
Member

Choose a reason for hiding this comment

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

The validation for files were already performed in validateFileInput, so we don't have to repeat any checks here. Sufficient to just log the files received.

},
} as InputValidatorBlock,

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;
99 changes: 76 additions & 23 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 All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

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

Unnecessary comment changes, there're some other ones in the rest of the code, can revert these changes :3

*/
const useRcbPlugin = (pluginConfig?: PluginConfig) => {
const { showToast } = useToasts();
Expand All @@ -31,67 +32,118 @@ 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);
};

/**
* 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.
*/
// 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;
Copy link
Member

Choose a reason for hiding this comment

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

Missing indentations, same for the code below.

}

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.
*/
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 +153,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
9 changes: 7 additions & 2 deletions src/types/InputValidatorBlock.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// src/types/InputValidatorBlock.ts

import { Block } from "react-chatbotify";
import { ValidationResult } from "./ValidationResult";

/**
* Extends the Block from React ChatBotify to support inputValidator attribute.
*/
export type InputValidatorBlock = Block & {
validateInput: (userInput?: string) => ValidationResult;

export type InputValidatorBlock = Omit<Block, "file"> & {
file?: (params: { files?: FileList }) => void | Promise<void>;
Copy link
Member

Choose a reason for hiding this comment

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

The file attribute is already present in the original Block type, not needed here.

validateTextInput?: (userInput?: string) => ValidationResult;
validateFileInput?: (file?: File) => ValidationResult;
Copy link
Member

Choose a reason for hiding this comment

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

It's actually possible that users may submit multiple files, so we have to cater to that possibility.

};
Loading