UNPKG

leumas-private-shared

Version:

Private React JSX Package For Leumas Shared Components, Headers, Footers, Asides, Login Pages, API Key Manager and much more. Styles and everything reusable to avoid DRY code across all of our subdomains

440 lines (347 loc) 15.7 kB
import { useState , useEffect } from 'react'; import { useTrail, animated } from 'react-spring'; import { classes } from "../styles/tailwindStyles"; import { createItem } from '../LMS-WildCard-Helpers/UniversalCrud/UniversalCrudHelpers'; // Adjust the path import {getAllItems , editItem} from "../LMS-WildCard-Helpers/UniversalCrud/UniversalCrudHelpers" import Message from './Message'; import useAuthUser from 'react-auth-kit/hooks/useAuthUser'; import renderInputField from "./renderInputField" // Importing a debounce utility import { debounce } from 'lodash'; import {LeumasBaseButtonStyle} from "../styles/baseStyles" import {useNavigate} from "react-router-dom" import React from 'react'; const MultiPageForm = ({ model, endpoint, token, pages = [] , extraData , setMode , AIMode , onFormDataChange, itemToEdit = null , isEditMode =false , selectedItemId , IDMode=false , passToParent , hideCreateButton = false , navigationEffect , navigateLink }) => { const [aiModels, setAiModels] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [messages, setMessages] = useState([]); const [currentRole, setCurrentRole] = useState('user'); const navigate = useNavigate() const [generatedText, setGeneratedText] = useState(null); // console.log("selected item id ", selectedItemId) // console.log("Is this IDMode? ", IDMode) useEffect(() => { const fetchAiModels = async () => { try { const allAiModels = await getAllItems("AIModel", endpoint, token); const publishedAiModels = allAiModels?.filter(model => model?.isPublished); setAiModels(publishedAiModels); } catch (error) { console.error("Error fetching AI Models:", error); } }; fetchAiModels(); }, [token, endpoint]); const [currentPage, setCurrentPage] = useState(0); const initialFormValues = pages.reduce((accum, pageFields) => { // Check if pageFields is an array before proceeding if (Array.isArray(pageFields)) { pageFields?.forEach(field => { if (field?.type === 'hidden' && (field?.name === 'premium' || field?.name === 'public')) { accum[field?.name] = field?.value === 'true' ? true : field?.value === 'false' ? false : field?.value; } else { accum[field?.name] = field?.value || ""; } }); } // console.log("accum " , accum) return accum; }, {}); const [formValues, setFormValues] = useState(initialFormValues); const [messageInfo, setMessageInfo] = useState({ message: "", type: "success" }); const auth = useAuthUser(); const nextPage = () => setCurrentPage(currentPage + 1); const prevPage = () => setCurrentPage(currentPage - 1); useEffect(() => { if (isEditMode && itemToEdit) { setFormValues(itemToEdit); // Set form values to the item's data if in edit mode } else { setFormValues(initialFormValues); // Otherwise use initial values // console.log("Form Values" , initialFormValues) } }, [isEditMode, itemToEdit]); function handleInstantInputChange(event) { const { name, value } = event.target; // First, update the local state setFormValues(prevValues => { const updatedValues = { ...prevValues, [name]: value }; // After state update, call the function to pass data to parent passDataToParent(updatedValues); // Handle AI message update with new values handleAiMessageUpdate(name, value); // Return the updated values to update the state return updatedValues; }); } const handleImageUpload = (event) => { const file = event.target.files[0]; // Get the uploaded file if (file) { const blobUrl = URL.createObjectURL(file); // Create a blob URL for the file updateFormWithBlobUrl(event.target.name, blobUrl); // Update form values with the blob URL } }; const handleCheckboxChange = (event) => { const { name, checked } = event.target; // Use `checked` for checkboxes, not `value` // First, update the local state setFormValues(prevValues => { const updatedValues = { ...prevValues, [name]: checked }; // Update using `checked` // After state update, call the function to pass data to parent passDataToParent(updatedValues); // Handle AI message update with new values handleAiMessageUpdate(name, checked); // Note the use of `checked` here // Return the updated values to update the state return updatedValues; }); }; // Assuming this is your file upload handler const handleFileUpload = (event) => { const file = event.target.files[0]; if (file) { // Generate a blob URL for the uploaded file const blobUrl = URL.createObjectURL(file); console.log("Blob URL " , blobUrl) // Update the form state with the blob URL updateFormWithBlobUrl(event.target.name, blobUrl); // Update form values with the blob URL } }; const handleFolderUpload = (event) => { const files = event.target.files; // Get the uploaded files const folderFilesDetails = []; for (let i = 0; i < files.length; i++) { const file = files[i]; const blobUrl = URL.createObjectURL(file); // Create a blob URL for each file const fileDetails = { name: file.name, size: file.size, type: file.type, lastModified: file.lastModified, blobUrl: blobUrl, }; folderFilesDetails.push(fileDetails); // Add the file details to the array } // Add the number of files to the details const folderDetails = { filesCount: files.length, files: folderFilesDetails, }; // Update form values with the folder details, including the files count updateFormWithBlobUrl(event.target.name, folderDetails); }; // Helper function to update the form state with the blob URL const updateFormWithBlobUrl = (inputName, blobUrl) => { setFormValues(prevValues => { const updatedValues = { ...prevValues, [inputName]: blobUrl }; // After state update, call the function to pass data to parent passDataToParent(updatedValues); // Optional: If you have a specific handler for AI message updates based on file uploads // handleAiMessageUpdate(inputName, blobUrl); return updatedValues; }); }; const passDataToParent = (dataToPass) => { // console.log("Here is our Data to Pass to parent of MultiPageForm ", dataToPass) if(passToParent){ passToParent(dataToPass) } } const [userMessages, setUserMessages] = useState(pages.map(page => ({ role: 'user', content: page[0]?.placeholder }))); const [assistantMessages, setAssistantMessages] = useState(pages.map(() => ({ role: 'assistant', content: '' }))); const handleAiMessageUpdate = debounce((name, value) => { if (AIMode) { // Update the assistant's message corresponding to the current page let updatedAssistantMessages = [...assistantMessages]; updatedAssistantMessages[currentPage] = { role: 'assistant', content: value }; setAssistantMessages(updatedAssistantMessages); } }, 300); const getUserId = () => { const user = auth?.user || auth?.authStateUser; return user?._id || user?.id; }; const getUserToken = () => { const user = auth; // console.log("Here is the user that we found " , user) const token = user?.token || user?.authUserState?.token; // console.log("Here is the token that we found " , token) return token; }; const handleSubmit = async () => { const userId = getUserId(); const token = getUserToken(); if(IDMode){ // console.log("We are in ID Mode so we need to fetch to make sure that this user does not have any existing ID, if they do then we must return the results for their ID and cancel the creation") // const userIds = await getVirtualIdsByOwner(userId, "LeumasAPI", token) // console.log("Here are the user ids that we have returned" , userIds) // if(userIds){ // console.log("Okay we are returning a value for the user id" , userIds) // console.log("Since you have Ids already we cannot let you create another one , we need to alert the user, ") // alert("Sorry you already have a Leumas Id") // setMessageInfo({ message: "Leumas Id already registered for this account, Each user may only have 1 LeumasID.", type: "error" }); // return; // } } if (AIMode) { const formattedMessages = []; for(let i = 0; i < userMessages.length; i++) { if(userMessages[i].content.trim() !== "" && assistantMessages[i].content.trim() !== "") { formattedMessages.push({ messages: [userMessages[i], assistantMessages[i]] }); } } // Convert the formatted messages into the desired single line string format const generatedStrings = formattedMessages.map(fm => JSON.stringify(fm)); const allGeneratedText = generatedStrings.join("\n"); setGeneratedText(allGeneratedText); // console.log("Formatted AI data:", allGeneratedText); } else { if (isEditMode) { // If in edit mode, call editItem const editData = { model: model, endpoint: endpoint || "LeumasAPI", data:formValues, owner: auth?.id || auth?.user?._id } try { // console.log("MultiPage Form says : \n We are in edit mode, and we are about to edit the item with these parameters") // console.log("model" , model); // console.log("item to edit" , itemToEdit?._id, "\n form Values : ", editData, "\n",token) const result = await editItem(model, itemToEdit?._id, editData, endpoint, token); console.log("Item edited:", result); setMessageInfo({ message: "Item successfully edited!", type: "success" }); } catch (err) { console.error("Error editing the item:", err); setMessageInfo({ message: "Error editing the item. Please try again.", type: "error" }); } } else { // console.log("We should be creating a normal schema " , dataToSubmit) let data = { ...dataToSubmit }; data.data.owner = userId; console.log(auth) console.log("data" , data); try { const result = await createItem(model, endpoint, dataToSubmit, token); console.log("Item created:", result); setMessageInfo({ message: "Item successfully created!", type: "success" }); } catch (err) { console.error("Error creating the item:", err); setMessageInfo({ message: "Error creating the item. Please try again.", type: "error" }); } } if(navigationEffect){ console.log("There is a navigation effect, we must navigate the user after create / edit") navigate() } } }; const dataToSubmit = { model, data: { ...formValues, models:model, model, owner: getUserId(), participants: [`${getUserId()}`], reaction: 'Happy', type:"ai", } }; // console.log("extra data " , extraData ); const styledInput = ` bg-white border-2 border-gray-200 rounded-md shadow-sm w-full p-3 text-gray-900 transition duration-150 ease-in-out hover:border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 `; const currentFields = pages[currentPage] || []; const trail = useTrail(currentFields.length, { from: { transform: 'translate3d(-100%,0,0)', opacity: 0 }, to: { transform: 'translate3d(0%,0,0)', opacity: 1 }, }); useEffect(() => { if(onFormDataChange) { onFormDataChange(formValues); } }, [formValues, onFormDataChange]); return ( <div className={`w-full flex flex-col max-w-[100%] resize-none border rounded-lg shadow-xl bg-blue-400 px-4 `}> <div className={`flex-grow overflow-y-auto overflow-x-hidden rounded-lg rounded-lg w-full px-2 gap-2 flex flex-col max-w-full`}> <div className="flex justify-between w-full gap-4 items-center p-2"> <p className={classes?.title}>{model} - {isEditMode ? "Edit" : "Create"}</p> <div className='flex flex-col justify-end gap-2 '> <p className={`text-[12px] font-bold ${AIMode ? "text-green-400" : "text-red-400"} flex justify-end `}> {AIMode ? "AI Training Mode" : "Form Mode"} </p> <div className="flex space-x-2 justify-center mt-4"> {pages.map((_, idx) => ( <button key={idx} onClick={() => setCurrentPage(idx)} aria-label={`Go to section ${idx + 1}`} className={`h-3 w-3 rounded-full ${currentPage === idx ? 'bg-blue-500' : 'bg-gray-300'} transition duration-300 ease-in-out hover:bg-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-500`} ></button> ))} </div> </div> </div> {trail.map((props, index) => ( <animated.div style={props} key={index}> {renderInputField({ inputField: currentFields[index], formValues, handleImageUpload: handleImageUpload, handleFileUpload : handleFileUpload, handleInputChange: handleInstantInputChange, handleFolderUpload: handleFolderUpload, styledInput, setSearchTerm, aiModels, selectedItemId, handleCheckboxChange: handleCheckboxChange, })} {AIMode && ( <button onClick={() => setCurrentRole(currentRole === 'user' ? 'assistant' : 'user')}> Switch to {currentRole === 'user' ? 'Assistant' : 'User'} </button> )} </animated.div> ))} <div className="mt-4 flex space-x-4 justify-center items-center"> {currentPage > 0 && ( <button onClick={prevPage} className="bg-gray-200 text-gray-800 p-3 hover:bg-gray-300 rounded-lg transition duration-150 ease-in-out"> Back </button> )} {currentPage < pages.length - 1 ? ( <button onClick={nextPage} className="bg-blue-500 text-white p-3 hover:bg-blue-600 rounded-lg transition duration-150 ease-in-out"> Next </button> ) : !hideCreateButton && ( // Note the use of !hideCreateButton and removal of extra braces <button onClick={handleSubmit} className={`${LeumasBaseButtonStyle} w-full`}> {isEditMode ? "Save Changes" : "Create"} </button> )} </div> </div> {messageInfo.message && ( <Message message={messageInfo.message} type={messageInfo.type} /> )} {AIMode && generatedText && ( <div className="mt-4 p-4 border rounded"> <h3 className="mb-2 font-bold">Generated Text:</h3> <pre className=" p-4 rounded mb-4 overflow-x-auto max-w-full whitespace-pre-wrap">{generatedText}</pre> <button onClick={() => navigator.clipboard.writeText(generatedText)} className="mr-2 bg-blue-500 text-white rounded px-4 py-2" > Copy to Clipboard </button> {/* Depending on your save mechanism, you can also provide a save button */} {/* <button className="bg-green-500 text-white rounded px-4 py-2">Save</button> */} </div> )} </div> ); }; export default MultiPageForm;