From e3a877e47296d989cd2e9217041d25e6bdc77cb7 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Wed, 16 Oct 2024 11:05:30 -0400 Subject: [PATCH] Add dynamic routing Pull data dynamically from API to allow for interaction with canned assistants. --- README.md | 38 ++++++--- package-lock.json | 103 +++++++---------------- package.json | 2 +- src/app/Ansible/Ansible.tsx | 12 --- src/app/AppLayout/AppLayout.tsx | 59 ++++++++++---- src/app/BaseChatbot/BaseChatbot.tsx | 87 ++++++++++++++++---- src/app/Home/Home.tsx | 11 +++ src/app/NotFound/NotFound.tsx | 32 +++----- src/app/OCP/OCP.tsx | 13 --- src/app/RHEL/RHEL.tsx | 12 --- src/app/RHO/RHO.tsx | 12 --- src/app/RHOAI/RHOAI.tsx | 12 --- src/app/index.tsx | 13 +-- src/app/routes.tsx | 122 ++++++---------------------- src/app/types/CannedChatbot.ts | 7 ++ src/app/utils/useDocumentTitle.ts | 13 --- 16 files changed, 232 insertions(+), 316 deletions(-) delete mode 100644 src/app/Ansible/Ansible.tsx create mode 100644 src/app/Home/Home.tsx delete mode 100644 src/app/OCP/OCP.tsx delete mode 100644 src/app/RHEL/RHEL.tsx delete mode 100644 src/app/RHO/RHO.tsx delete mode 100644 src/app/RHOAI/RHOAI.tsx create mode 100644 src/app/types/CannedChatbot.ts delete mode 100644 src/app/utils/useDocumentTitle.ts diff --git a/README.md b/README.md index 814de94..5cdb45d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ git clone https://github.com/patternfly/patternfly-react-seed cd patternfly-react-seed npm install && npm run start:dev ``` + ## Development scripts + ```sh # Install development/build dependencies npm install @@ -44,37 +46,41 @@ npm run start ``` ## Configurations -* [TypeScript Config](./tsconfig.json) -* [Webpack Config](./webpack.common.js) -* [Jest Config](./jest.config.js) -* [Editor Config](./.editorconfig) + +- [TypeScript Config](./tsconfig.json) +- [Webpack Config](./webpack.common.js) +- [Jest Config](./jest.config.js) +- [Editor Config](./.editorconfig) ## Raster image support To use an image asset that's shipped with PatternFly core, you'll prefix the paths with "@assets". `@assets` is an alias for the PatternFly assets directory in node_modules. For example: + ```js import imgSrc from '@assets/images/g_sizing.png'; -Some image +Some image; ``` You can use a similar technique to import assets from your local app, just prefix the paths with "@app". `@app` is an alias for the main src/app directory. ```js import loader from '@app/assets/images/loader.gif'; -Content loading +Content loading; ``` ## Vector image support + Inlining SVG in the app's markup is also possible. ```js import logo from '@app/assets/images/logo.svg'; - +; ``` You can also use SVG when applying background images with CSS. To do this, your SVG's must live under a `bgimages` directory (this directory name is configurable in [webpack.common.js](./webpack.common.js#L5)). This is necessary because you may need to use SVG's in several other context (inline images, fonts, icons, etc.) and so we need to be able to differentiate between these usages so the appropriate loader is invoked. + ```css body { background: url(./assets/bgimages/img_avatar.svg); @@ -82,16 +88,19 @@ body { ``` ## Adding custom CSS + When importing CSS from a third-party package for the first time, you may encounter the error `Module parse failed: Unexpected token... You may need an appropriate loader to handle this file typ...`. You need to register the path to the stylesheet directory in [stylePaths.js](./stylePaths.js). We specify these explicitly for performance reasons to avoid webpack needing to crawl through the entire node_modules directory when parsing CSS modules. ## Code quality tools -* For accessibility compliance, we use [react-axe](https://github.com/dequelabs/react-axe) -* To keep our bundle size in check, we use [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) -* To keep our code formatting in check, we use [prettier](https://github.com/prettier/prettier) -* To keep our code logic and test coverage in check, we use [jest](https://github.com/facebook/jest) -* To ensure code styles remain consistent, we use [eslint](https://eslint.org/) + +- For accessibility compliance, we use [react-axe](https://github.com/dequelabs/react-axe) +- To keep our bundle size in check, we use [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) +- To keep our code formatting in check, we use [prettier](https://github.com/prettier/prettier) +- To keep our code logic and test coverage in check, we use [jest](https://github.com/facebook/jest) +- To ensure code styles remain consistent, we use [eslint](https://eslint.org/) ## Multi environment configuration + This project uses [dotenv-webpack](https://www.npmjs.com/package/dotenv-webpack) for exposing environment variables to your code. Either export them at the system level like `export MY_ENV_VAR=http://dev.myendpoint.com && npm run start:dev` or simply drop a `.env` file in the root that contains your key-value pairs like below: ```sh @@ -109,4 +118,7 @@ The assistants will need to be dynamic. Eventually we are going to want the abil Add a button to stop a streaming response -(Infra) Set the Registry Secrets at the org level \ No newline at end of file +(Infra) Set the Registry Secrets at the org level + +I will say (and we can sync about this if it does not make sense), we will need to be able to dynamically specify the backend's base url, since this is going to be a demo deployed on openshift where that URL will be auto generated. +My optimal case would be something where we could build an image that would read an Environment variable that has the backend url on startup and that way we could just deploy that image in different environments no problem. But it looks like that may not be possible, seems like that variable needs to be defined at build time. diff --git a/package-lock.json b/package-lock.json index b92142d..5f41219 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "raw-loader": "^4.0.2", "react-axe": "^3.5.4", "react-docgen-typescript-loader": "^3.7.2", - "react-router-dom": "^5.3.4", + "react-router-dom": "^6.27.0", "regenerator-runtime": "^0.13.11", "rimraf": "^5.0.7", "style-loader": "^3.3.4", @@ -1926,6 +1926,16 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -9140,20 +9150,6 @@ "node": "*" } }, - "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -10405,12 +10401,6 @@ "node": ">=8" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -14919,15 +14909,6 @@ "node": "14 || >=16.14" } }, - "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "dependencies": { - "isarray": "0.0.1" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -16304,41 +16285,37 @@ } }, "node_modules/react-router": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", - "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "@remix-run/router": "1.20.0" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", - "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.4", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/react-syntax-highlighter": { @@ -16825,12 +16802,6 @@ "node": ">=8" } }, - "node_modules/resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", - "dev": true - }, "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -18589,12 +18560,6 @@ "node": ">=0.6.0" } }, - "node_modules/tiny-invariant": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", - "dev": true - }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -19663,12 +19628,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", - "dev": true - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index d4af46c..55a8c9b 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "raw-loader": "^4.0.2", "react-axe": "^3.5.4", "react-docgen-typescript-loader": "^3.7.2", - "react-router-dom": "^5.3.4", + "react-router-dom": "^6.27.0", "regenerator-runtime": "^0.13.11", "rimraf": "^5.0.7", "style-loader": "^3.3.4", diff --git a/src/app/Ansible/Ansible.tsx b/src/app/Ansible/Ansible.tsx deleted file mode 100644 index e0cf4c9..0000000 --- a/src/app/Ansible/Ansible.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; -import { BaseChatbot } from '@app/BaseChatbot/BaseChatbot'; -// TODO: This needs to be dynamic and not hard coded for each assistant -const Ansible: React.FunctionComponent = () => ( - -); - -export { Ansible }; diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index 02504c3..2f08048 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { NavLink, useLocation } from 'react-router-dom'; +import { NavLink, Outlet, useLoaderData, useLocation } from 'react-router-dom'; import { Button, Masthead, @@ -9,7 +9,6 @@ import { MastheadToggle, Nav, NavExpandable, - NavGroup, NavItem, NavList, Page, @@ -17,14 +16,48 @@ import { PageSidebarBody, SkipToContent, } from '@patternfly/react-core'; -import { IAppRoute, IAppRouteGroup, routes } from '@app/routes'; +import { IAppRoute, IAppRouteGroup, routes as staticRoutes } from '@app/routes'; import { BarsIcon } from '@patternfly/react-icons'; -interface IAppLayout { - children: React.ReactNode; +import { CannedChatbot } from '../types/CannedChatbot'; + +const getChatbots = () => { + const url = process.env.REACT_APP_INFO_URL ?? ''; + return fetch(url) + .then((res) => res.json()) + .then((data: CannedChatbot[]) => { + return data; + }) + .catch((e) => { + throw new Response(e.message, { status: 404 }); + }); +}; + +export async function loader() { + const chatbots = await getChatbots(); + return { chatbots }; } -const AppLayout: React.FunctionComponent = ({ children }) => { +const AppLayout: React.FunctionComponent = () => { const [sidebarOpen, setSidebarOpen] = React.useState(true); + const [routes, setRoutes] = React.useState(staticRoutes); + const { chatbots } = useLoaderData(); + + React.useEffect(() => { + if (chatbots) { + const newRoutes = structuredClone(routes); + chatbots.forEach((chatbot) => { + const isNotPresent = routes.filter((route) => route.path === `assistants/${chatbot.name}`).length === 0; + if (isNotPresent) { + newRoutes.push({ + path: `assistants/${chatbot.name}`, + label: chatbot.displayName, + title: chatbot.displayName, + }); + } + }); + setRoutes(newRoutes); + } + }, []); const masthead = ( @@ -91,9 +124,7 @@ const AppLayout: React.FunctionComponent = ({ children }) => { const renderNavItem = (route: IAppRoute, index: number) => ( - - {route.label} - + {route.label} ); @@ -111,11 +142,9 @@ const AppLayout: React.FunctionComponent = ({ children }) => { const Navigation = ( ); @@ -149,7 +178,7 @@ const AppLayout: React.FunctionComponent = ({ children }) => { skipToContent={PageSkipToContent} isContentFilled > - {children} + ); }; diff --git a/src/app/BaseChatbot/BaseChatbot.tsx b/src/app/BaseChatbot/BaseChatbot.tsx index 30e79b0..982e7a9 100644 --- a/src/app/BaseChatbot/BaseChatbot.tsx +++ b/src/app/BaseChatbot/BaseChatbot.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Chatbot, + ChatbotAlert, ChatbotContent, ChatbotDisplayMode, ChatbotFooter, @@ -14,23 +15,60 @@ import { MessageBox, MessageProps, } from '@patternfly/virtual-assistant'; - +import { useLoaderData } from 'react-router-dom'; +import { CannedChatbot } from '../types/CannedChatbot'; interface Source { link: string; } -export interface BaseChatbotProps { - title: string; - url: string; - assistantName: string; +const getChatbot = (id: string) => { + const url = process.env.REACT_APP_INFO_URL ?? ''; + return fetch(url) + .then((res) => res.json()) + .then((data: CannedChatbot[]) => { + const filteredChatbots = data.filter((chatbot) => chatbot.name === id); + if (filteredChatbots.length > 0) { + return { + title: filteredChatbots[0].displayName, + assistantName: filteredChatbots[0].name, + id: filteredChatbots[0].id, + llmConnection: filteredChatbots[0].llmConnection, + retrieverConnection: filteredChatbots[0].retrieverConnection, + }; + } else { + throw new Response('Not Found', { status: 404 }); + } + }) + .catch((e) => { + throw new Response(e.message, { status: 404 }); + }); +}; + +export async function loader({ params }) { + const chatbot = await getChatbot(params.chatbotId); + return { chatbot }; } -const BaseChatbot: React.FunctionComponent = ({ title, url, assistantName }) => { +const BaseChatbot: React.FunctionComponent = () => { const [messages, setMessages] = React.useState([]); const [currentMessage, setCurrentMessage] = React.useState([]); const [currentSources, setCurrentSources] = React.useState(); const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false); const scrollToBottomRef = React.useRef(null); + const [error, setError] = React.useState(); + const [stopStream, setStopStream] = React.useState(false); + const { chatbot } = useLoaderData(); + + React.useEffect(() => { + document.title = `PatternFly React Seed | ${chatbot.title}`; + // React Router reuses base components so we need to reset manually whenever the chatbot changes + setMessages([]); + setCurrentMessage([]); + setCurrentSources(undefined); + setIsSendButtonDisabled(false); + setError(undefined); + setStopStream(true); + }, [chatbot]); // Auto-scrolls to the latest message React.useEffect(() => { @@ -40,6 +78,8 @@ const BaseChatbot: React.FunctionComponent = ({ title, url, as } }, [messages, currentMessage, currentSources]); + const url = process.env.REACT_APP_ROUTER_URL ?? ''; + async function fetchData(userMessage: string) { let isSource = false; const response = await fetch(url, { @@ -49,7 +89,7 @@ const BaseChatbot: React.FunctionComponent = ({ title, url, as }, body: JSON.stringify({ message: userMessage, - assistantName, + assistantName: chatbot.assistantName, }), }); @@ -62,7 +102,7 @@ const BaseChatbot: React.FunctionComponent = ({ title, url, as let done; const sources: string[] = []; - while (!done) { + while (!done || !stopStream) { const { done, value } = await reader.read(); if (done) { break; @@ -92,13 +132,17 @@ const BaseChatbot: React.FunctionComponent = ({ title, url, as return undefined; } + const getId = () => { + const date = Date.now() + Math.random(); + return date.toString(); + }; + const handleSend = async (input) => { setIsSendButtonDisabled(true); const newMessages = structuredClone(messages); if (currentMessage.length > 0) { - const id = Date.now() + Math.random(); newMessages.push({ - id: id.toString(), + id: getId(), name: 'Chatbot', role: 'bot', content: currentMessage.join(''), @@ -107,12 +151,15 @@ const BaseChatbot: React.FunctionComponent = ({ title, url, as setCurrentMessage([]); setCurrentSources(undefined); } - newMessages.push({ id: '', name: 'You', role: 'user', content: input }); + newMessages.push({ id: getId(), name: 'You', role: 'user', content: input }); setMessages(newMessages); - await fetchData(input); - const sources = await fetchData(input); - setCurrentSources(sources); + const sources = await fetchData(input).catch((e) => { + setError(e.message); + }); + if (sources) { + setCurrentSources(sources); + } setIsSendButtonDisabled(false); }; @@ -122,10 +169,20 @@ const BaseChatbot: React.FunctionComponent = ({ title, url, as - {title} + {chatbot.title} + {error && ( + { + setError(undefined); + }} + title={error} + /> + )} {messages.map((message) => ( diff --git a/src/app/Home/Home.tsx b/src/app/Home/Home.tsx new file mode 100644 index 0000000..6613d00 --- /dev/null +++ b/src/app/Home/Home.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +const Home: React.FunctionComponent = () => { + React.useEffect(() => { + document.title = `PatternFly React Seed | Home`; + }, []); + + return
Hello world
; +}; + +export { Home }; diff --git a/src/app/NotFound/NotFound.tsx b/src/app/NotFound/NotFound.tsx index b83296e..970ac94 100644 --- a/src/app/NotFound/NotFound.tsx +++ b/src/app/NotFound/NotFound.tsx @@ -1,35 +1,27 @@ import * as React from 'react'; import { ExclamationTriangleIcon } from '@patternfly/react-icons'; -import { - Button, - EmptyState, - EmptyStateBody, - EmptyStateFooter, - PageSection, -} from '@patternfly/react-core'; -import { useHistory } from 'react-router-dom'; +import { Button, EmptyState, EmptyStateBody, EmptyStateFooter, PageSection } from '@patternfly/react-core'; +import { useNavigate } from 'react-router-dom'; const NotFound: React.FunctionComponent = () => { function GoHomeBtn() { - const history = useHistory(); + const navigate = useNavigate(); function handleClick() { - history.push('/'); + navigate('/'); } - return ( - - ); + return ; } return ( - - - We didn't find a page that matches the address you navigated to. - - - + + We didn't find a page that matches the address you navigated to. + + + + - ) + ); }; export { NotFound }; diff --git a/src/app/OCP/OCP.tsx b/src/app/OCP/OCP.tsx deleted file mode 100644 index 73e6ca5..0000000 --- a/src/app/OCP/OCP.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from 'react'; -import { BaseChatbot } from '@app/BaseChatbot/BaseChatbot'; - -const OCP: React.FunctionComponent = () => ( - -); - -export { OCP }; diff --git a/src/app/RHEL/RHEL.tsx b/src/app/RHEL/RHEL.tsx deleted file mode 100644 index 38b1af7..0000000 --- a/src/app/RHEL/RHEL.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; -import { BaseChatbot } from '@app/BaseChatbot/BaseChatbot'; - -const RHEL: React.FunctionComponent = () => ( - -); - -export { RHEL }; diff --git a/src/app/RHO/RHO.tsx b/src/app/RHO/RHO.tsx deleted file mode 100644 index 357c363..0000000 --- a/src/app/RHO/RHO.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; -import { BaseChatbot } from '@app/BaseChatbot/BaseChatbot'; - -const RHO: React.FunctionComponent = () => ( - -); - -export { RHO }; diff --git a/src/app/RHOAI/RHOAI.tsx b/src/app/RHOAI/RHOAI.tsx deleted file mode 100644 index f3f07c7..0000000 --- a/src/app/RHOAI/RHOAI.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; -import { BaseChatbot } from '@app/BaseChatbot/BaseChatbot'; - -const RHOAI: React.FunctionComponent = () => ( - -); - -export { RHOAI }; diff --git a/src/app/index.tsx b/src/app/index.tsx index 7ab9758..bd8ccd4 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -1,17 +1,10 @@ import * as React from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { AppLayout } from '@app/AppLayout/AppLayout'; -import { AppRoutes } from '@app/routes'; +import { RouterProvider } from 'react-router-dom'; +import { router } from '@app/routes'; import '@app/app.css'; import '@patternfly/react-core/dist/styles/base.css'; import '@patternfly/virtual-assistant/dist/css/main.css'; -const App: React.FunctionComponent = () => ( - - - - - -); +const App: React.FunctionComponent = () => ; export default App; diff --git a/src/app/routes.tsx b/src/app/routes.tsx index f71c2c5..f151648 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -1,20 +1,12 @@ import * as React from 'react'; -import { Route, RouteComponentProps, Switch, useLocation } from 'react-router-dom'; -import { RHOAI } from '@app/RHOAI/RHOAI'; +import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import { NotFound } from '@app/NotFound/NotFound'; -import { useDocumentTitle } from '@app/utils/useDocumentTitle'; -import { OCP } from './OCP/OCP'; -import { RHO } from './RHO/RHO'; -import { Ansible } from './Ansible/Ansible'; -import { RHEL } from './RHEL/RHEL'; +import { BaseChatbot, loader as chatbotLoader } from './BaseChatbot/BaseChatbot'; +import { AppLayout, loader as layoutLoader } from './AppLayout/AppLayout'; +import { Home } from './Home/Home'; -let routeFocusTimer: number; export interface IAppRoute { label?: string; // Excluding the label will exclude the route from the nav sidebar in AppLayout - /* eslint-disable @typescript-eslint/no-explicit-any */ - component: React.ComponentType> | React.ComponentType; - /* eslint-enable @typescript-eslint/no-explicit-any */ - exact?: boolean; path: string; title: string; routes?: undefined; @@ -27,92 +19,30 @@ export interface IAppRouteGroup { export type AppRouteConfig = IAppRoute | IAppRouteGroup; -const routes: AppRouteConfig[] = [ - { - component: Ansible, - exact: true, - label: 'Ansible', - path: '/', - title: 'PatternFly Seed | Ansible', - }, - { - component: OCP, - exact: true, - label: 'OpenShift Container Platform', - path: '/ocp', - title: 'PatternFly Seed | OpenShift Container Platform', - }, - { - component: RHEL, - exact: true, - label: 'Red Hat Enterprise Linux', - path: '/rhel', - title: 'PatternFly Seed | Red Hat Enterprise Linux', - }, - { - component: RHOAI, - exact: true, - label: 'Red Hat OpenShift AI', - path: '/rhoai', - title: 'PatternFly Seed | Red Hat OpenShift AI', - }, +// used for navigation panel +const routes: AppRouteConfig[] = [{ path: '/', label: 'Home', title: 'PatternFly React Seed | Home' }]; +// used for actual routing +const router = createBrowserRouter([ { - component: RHO, - exact: true, - label: 'RHO 2025 FAQ', - path: '/rho', - title: 'PatternFly Seed | RHO 2025 FAQ', + element: , + loader: layoutLoader, + errorElement: , + children: [ + { + path: '/', + element: , + }, + { + path: 'assistants/:chatbotId', + element: , + loader: chatbotLoader, + errorElement: , + }, + ], }, -]; - -// a custom hook for sending focus to the primary content container -// after a view has loaded so that subsequent press of tab key -// sends focus directly to relevant content -// may not be necessary if https://github.com/ReactTraining/react-router/issues/5210 is resolved -const useA11yRouteChange = () => { - const { pathname } = useLocation(); - React.useEffect(() => { - routeFocusTimer = window.setTimeout(() => { - const mainContainer = document.getElementById('primary-app-container'); - if (mainContainer) { - mainContainer.focus(); - } - }, 50); - return () => { - window.clearTimeout(routeFocusTimer); - }; - }, [pathname]); -}; - -const RouteWithTitleUpdates = ({ component: Component, title, ...rest }: IAppRoute) => { - useA11yRouteChange(); - useDocumentTitle(title); - - function routeWithTitle(routeProps: RouteComponentProps) { - return ; - } - - return ; -}; - -const PageNotFound = ({ title }: { title: string }) => { - useDocumentTitle(title); - return ; -}; - -const flattenedRoutes: IAppRoute[] = routes.reduce( - (flattened, route) => [...flattened, ...(route.routes ? route.routes : [route])], - [] as IAppRoute[], -); +]); -const AppRoutes = (): React.ReactElement => ( - - {flattenedRoutes.map(({ path, exact, component, title }, idx) => ( - - ))} - - -); +const AppRoutes = (): React.ReactElement => ; -export { AppRoutes, routes }; +export { AppRoutes, routes, router }; diff --git a/src/app/types/CannedChatbot.ts b/src/app/types/CannedChatbot.ts new file mode 100644 index 0000000..4b431e5 --- /dev/null +++ b/src/app/types/CannedChatbot.ts @@ -0,0 +1,7 @@ +export interface CannedChatbot { + displayName: string; + id: string; + llmConnection: { description: string; id: string; name: string }; + name: string; + retrieverConnection: { id: string; name: string; description: string; index: string; metadataFields: string[] }; +} diff --git a/src/app/utils/useDocumentTitle.ts b/src/app/utils/useDocumentTitle.ts deleted file mode 100644 index 0442ab4..0000000 --- a/src/app/utils/useDocumentTitle.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from 'react'; - -// a custom hook for setting the page title -export function useDocumentTitle(title: string) { - React.useEffect(() => { - const originalTitle = document.title; - document.title = title; - - return () => { - document.title = originalTitle; - }; - }, [title]); -}