diff --git a/package-lock.json b/package-lock.json index 2f10b90..9e4757f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "dompurify": "^3.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-dropzone": "^14.3.5", "sirv-cli": "^2.0.2" }, "devDependencies": { @@ -3174,44 +3175,26 @@ "react-dom": "^17 || ^18" } }, - "node_modules/@patternfly/react-core": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.0.0.tgz", - "integrity": "sha512-UKFj9+YzBY+FfEDsLONgOM4N0e8SPV/27/UzNRiJ0gpgqbw2POuXwLpjGSRTTIUuCaLaGGM5PeTSj7mMB73ykw==", - "license": "MIT", - "dependencies": { - "@patternfly/react-icons": "^6.0.0", - "@patternfly/react-styles": "^6.0.0", - "@patternfly/react-tokens": "^6.0.0", - "focus-trap": "7.6.0", - "react-dropzone": "^14.2.3", - "tslib": "^2.7.0" - }, - "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" - } - }, - "node_modules/@patternfly/react-core/node_modules/file-selector": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", - "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "node_modules/@patternfly/react-code-editor/node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", "license": "MIT", "dependencies": { - "tslib": "^2.7.0" + "tslib": "^2.4.0" }, "engines": { "node": ">= 12" } }, - "node_modules/@patternfly/react-core/node_modules/react-dropzone": { - "version": "14.3.5", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz", - "integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==", + "node_modules/@patternfly/react-code-editor/node_modules/react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", "license": "MIT", "dependencies": { - "attr-accept": "^2.2.4", - "file-selector": "^2.1.0", + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", "prop-types": "^15.8.1" }, "engines": { @@ -3221,6 +3204,24 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/@patternfly/react-core": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.0.0.tgz", + "integrity": "sha512-UKFj9+YzBY+FfEDsLONgOM4N0e8SPV/27/UzNRiJ0gpgqbw2POuXwLpjGSRTTIUuCaLaGGM5PeTSj7mMB73ykw==", + "license": "MIT", + "dependencies": { + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", + "@patternfly/react-tokens": "^6.0.0", + "focus-trap": "7.6.0", + "react-dropzone": "^14.2.3", + "tslib": "^2.7.0" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, "node_modules/@patternfly/react-icons": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.0.0.tgz", @@ -9586,11 +9587,12 @@ } }, "node_modules/file-selector": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", - "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.7.0" }, "engines": { "node": ">= 12" @@ -17701,12 +17703,13 @@ } }, "node_modules/react-dropzone": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", - "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "version": "14.3.5", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz", + "integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==", + "license": "MIT", "dependencies": { - "attr-accept": "^2.2.2", - "file-selector": "^0.6.0", + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "engines": { diff --git a/package.json b/package.json index a0c9ab5..1511293 100644 --- a/package.json +++ b/package.json @@ -75,13 +75,14 @@ "webpack-merge": "^5.10.0" }, "dependencies": { + "@patternfly/chatbot": "^2.1.0-prerelease.26", "@patternfly/react-core": "^6.0.0", "@patternfly/react-icons": "^6.0.0", "@patternfly/react-styles": "^6.0.0", - "@patternfly/chatbot": "^2.1.0-prerelease.26", + "dompurify": "^3.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "sirv-cli": "^2.0.2", - "dompurify": "^3.2.0" + "react-dropzone": "^14.3.5", + "sirv-cli": "^2.0.2" } } diff --git a/src/app/FlyoutList/FlyoutList.tsx b/src/app/FlyoutList/FlyoutList.tsx index 3b1d676..7e1bf6e 100644 --- a/src/app/FlyoutList/FlyoutList.tsx +++ b/src/app/FlyoutList/FlyoutList.tsx @@ -17,6 +17,9 @@ interface FlyoutListProps { hideFlyout: () => void; onFooterButtonClick?: () => void; title: string; + error?: ErrorObject; + isLoading?: boolean; + onRetry?: () => void; } export const FlyoutList: React.FunctionComponent = ({ typeWordPlural, @@ -24,19 +27,33 @@ export const FlyoutList: React.FunctionComponent = ({ hideFlyout, onFooterButtonClick, title, + error: errorProp, + isLoading: isLoadingProp = true, + onRetry, }: FlyoutListProps) => { - const [error, setError] = React.useState(); - const [items, setItems] = React.useState([]); - const [filteredItems, setFilteredItems] = React.useState([]); - const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(errorProp); + const [originalChatbots, setOriginalChatbots] = React.useState([]); + const [filteredChatbots, setFilteredChatbots] = React.useState([]); + // you'll need states for files and filtered files + const [isLoading, setIsLoading] = React.useState(isLoadingProp); const { nextStep, reloadList, setReloadList } = useFlyoutWizard(); const location = useLocation(); const navigate = useNavigate(); const { flyoutMenuSelectedChatbot, updateFlyoutMenuSelectedChatbot, chatbots, setChatbots } = useAppData(); + // used for file case only + React.useEffect(() => { + setIsLoading(isLoadingProp); + }, [isLoadingProp]); + + // used for file case only + React.useEffect(() => { + setError(errorProp); + }, [errorProp]); + const header = (
- {title} + {title}
); @@ -71,8 +88,8 @@ export const FlyoutList: React.FunctionComponent = ({ throw new Error(`${response.status}`); } const data = await response.json(); - setItems(data); - setFilteredItems(data); + setOriginalChatbots(data); + setFilteredChatbots(data); setIsLoading(false); setReloadList(false); setChatbots(data); @@ -89,8 +106,8 @@ export const FlyoutList: React.FunctionComponent = ({ return; } if (chatbots.length > 0) { - setItems(chatbots); - setFilteredItems(chatbots); + setOriginalChatbots(chatbots); + setFilteredChatbots(chatbots); setIsLoading(false); return; } @@ -98,13 +115,18 @@ export const FlyoutList: React.FunctionComponent = ({ }; React.useEffect(() => { - loadAssistants(); + if (typeWordPlural === 'assistants') { + loadAssistants(); + } + if (typeWordPlural === 'files') { + // this is where you'd put your API call for files + } }, []); const buildMenu = () => { return ( - {filteredItems.map((item) => ( + {filteredChatbots.map((item) => ( = ({ }; const onSelect = (_event: React.MouseEvent | undefined, value) => { - if (filteredItems.length > 0) { - const newChatbot = items.filter((item) => item.name === value)[0]; + if (filteredChatbots.length > 0) { + const newChatbot = originalChatbots.filter((item) => item.name === value)[0]; updateFlyoutMenuSelectedChatbot(newChatbot); if (location.pathname !== '/') { navigate('/'); @@ -129,8 +151,8 @@ export const FlyoutList: React.FunctionComponent = ({ } }; - const findMatchingItems = (targetValue: string) => { - const matchingElements = items.filter((item) => { + const findMatchingItems = (targetValue: string, originalItems) => { + const matchingElements = originalItems.filter((item) => { const name = item.displayName ?? item.name; return name.toLowerCase().includes(targetValue.toLowerCase()); }); @@ -138,21 +160,30 @@ export const FlyoutList: React.FunctionComponent = ({ }; const handleTextInputChange = (value: string) => { - if (value === '') { - setFilteredItems(items); - return; + if (typeWordPlural === 'assistants') { + if (value === '') { + setFilteredChatbots(originalChatbots); + return; + } + const newItems = findMatchingItems(value, originalChatbots); + setFilteredChatbots(newItems); + } + if (typeWordPlural === 'files') { + // you'll need to add a matcher here based on state } - const newItems = findMatchingItems(value); - setFilteredItems(newItems); }; - const onClick = () => { + const onAssistantClick = () => { setError(undefined); loadAssistants(); }; return error ? ( - + ) : ( <> @@ -166,7 +197,7 @@ export const FlyoutList: React.FunctionComponent = ({ /> - {filteredItems.length > 0 ? ( + {filteredChatbots.length > 0 ? ( buildMenu() ) : ( diff --git a/src/app/SidebarWithFlyout/SidebarWithFlyout.tsx b/src/app/SidebarWithFlyout/SidebarWithFlyout.tsx index 70365cc..bd83413 100644 --- a/src/app/SidebarWithFlyout/SidebarWithFlyout.tsx +++ b/src/app/SidebarWithFlyout/SidebarWithFlyout.tsx @@ -8,13 +8,74 @@ import { FlyoutWizardProvider } from '@app/FlyoutWizard/FlyoutWizardContext'; import { FlyoutList } from '@app/FlyoutList/FlyoutList'; import { FlyoutWizard } from '@app/FlyoutWizard/FlyoutWizard'; import { FlyoutForm } from '@app/FlyoutForm/FlyoutForm'; +import { useDropzone } from 'react-dropzone'; +import { getId } from '@app/utils/utils'; +import { ErrorObject } from '@app/types/ErrorObject'; +import { UserFacingFile } from '@app/types/UserFacingFile'; export const SidebarWithFlyout: React.FunctionComponent = () => { const [sidebarHeight, setSidebarHeight] = useState(0); const [visibleFlyout, setVisibleFlyout] = useState(null); + const [isLoadingFile, setIsLoadingFile] = useState(false); + const [files, setFiles] = useState([]); + const [error, setError] = useState(); const sidebarRef = useRef(null); const flyoutMenuRef = useRef(null); + // example of how you can read a text file + const readFile = (file: File) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(file); + }); + + const { open, getInputProps } = useDropzone({ + onDropAccepted: (files: File[]) => { + // handle file drop/selection + setIsLoadingFile(true); + // any custom validation you may want + if (files.length > 2) { + setFiles([]); + setError({ title: 'Uploaded more than two files', body: 'Upload fewer files' }); + return; + } + // this is 200MB in bytes; size is in bytes + const anyFileTooBig = files.every((file) => file.size > 200000000); + if (anyFileTooBig) { + setFiles([]); + setError({ title: 'Uploaded a file larger than 200MB.', body: 'Try a uploading a smaller file' }); + return; + } + + const newFiles = files.map((file) => { + return { + name: file.name, + id: getId(), + }; + }); + setFiles(newFiles); + + // example of how to read file - this is where you'd send it to the server and trigger some sort of loading state + files.forEach((file) => { + readFile(file) + .then((data) => { + // eslint-disable-next-line no-console + console.log(data); + setError(undefined); + // this is just for demo purposes, to make the loading state really obvious + setTimeout(() => { + setIsLoadingFile(false); + }, 1000); + }) + .catch((error: DOMException) => { + setError({ title: 'Failed to read file', body: error.message }); + }); + }); + }, + }); + // Capture sidebar height initially and whenever it changes. // We use this to control the flyout height. useEffect(() => { @@ -84,6 +145,33 @@ export const SidebarWithFlyout: React.FunctionComponent = () => { ); } + if (visibleFlyout === 'Files') { + return ( + + {/* this is required for upload function open() to work in Safari and Firefox */} + + setVisibleFlyout(null)} + buttonText="New file" + typeWordPlural="files" + title={visibleFlyout} + onFooterButtonClick={() => { + open(); + }} + error={error} + isLoading={isLoadingFile} + onRetry={() => { + open(); + }} + />, + ]} + /> + + ); + } return; }; @@ -112,6 +200,14 @@ export const SidebarWithFlyout: React.FunctionComponent = () => { > Assistants + + Files + {/* Flyout menu */}