diff --git a/package-lock.json b/package-lock.json index ee07f01..2622a26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "0.0.2", "license": "MIT", "dependencies": { - "@patternfly/react-core": "^6.0.0-prerelease.21", - "@patternfly/react-icons": "^6.0.0-prerelease.7", - "@patternfly/react-styles": "^6.0.0-prerelease.6", - "@patternfly/virtual-assistant": "^2.0.0-alpha.61", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", + "@patternfly/virtual-assistant": "^2.1.0-prerelease.8", "react": "^18.3.1", "react-dom": "^18.3.1", "sirv-cli": "^2.0.2" @@ -3133,15 +3133,15 @@ } }, "node_modules/@patternfly/react-code-editor": { - "version": "6.0.0-prerelease.21", - "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.0.0-prerelease.21.tgz", - "integrity": "sha512-t9/8Uk3sbPaXasZXHaIxvcAGRWAlep9L0Gsy1vA+vzmpU8Igk1GO2JNMVr9ux4ScLEuMnzp0Rbq++VbxtDNdwA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.0.0.tgz", + "integrity": "sha512-TnI/NNkizzWTzdVZWmpyEPKXgsOoUeklk8Xlgtl7II/+5juLjlt0wXTMhL35F59Rzd0YohGs251zXAwJbn6vIQ==", "license": "MIT", "dependencies": { "@monaco-editor/react": "^4.6.0", - "@patternfly/react-core": "^6.0.0-prerelease.21", - "@patternfly/react-icons": "^6.0.0-prerelease.7", - "@patternfly/react-styles": "^6.0.0-prerelease.6", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", "react-dropzone": "14.2.3", "tslib": "^2.7.0" }, @@ -3151,14 +3151,14 @@ } }, "node_modules/@patternfly/react-core": { - "version": "6.0.0-prerelease.21", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.0.0-prerelease.21.tgz", - "integrity": "sha512-EaGcKUPeeR253vY4N0Ahm9oOVtltoI6JycfclwmzjevOzpYvuLj1jcsVwL8wqgWYQVpURoBm1yxIdx34fo5UHA==", + "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-prerelease.7", - "@patternfly/react-styles": "^6.0.0-prerelease.6", - "@patternfly/react-tokens": "^6.0.0-prerelease.7", + "@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" @@ -3168,16 +3168,10 @@ "react-dom": "^17 || ^18" } }, - "node_modules/@patternfly/react-core/node_modules/@patternfly/react-tokens": { - "version": "6.0.0-prerelease.7", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.0.0-prerelease.7.tgz", - "integrity": "sha512-SLgVwbIgVx26LCjaXkpNlPIZYqWpHJkw3QX/n3URLmIcRlCw536/rKO1PzXaeuCCqhuSq66J6R125zM2eJjM6A==", - "license": "MIT" - }, "node_modules/@patternfly/react-icons": { - "version": "6.0.0-prerelease.7", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.0.0-prerelease.7.tgz", - "integrity": "sha512-DQmecVgXRIiD3ww4KUuJ0qO76TmYMDEJ1ao1+DYuTSP+FzeJLJKuE9QxvL8qn3anyKtuORBuHdTIjM52mVq5Vg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.0.0.tgz", + "integrity": "sha512-ZFrsBVKrAp0DZrPOss98OA/EVUL4F0frXhR1uBId9+3ZrRArdKTgYgmQUCeSzMbxnSlxpmm3a2L05XQ36VUVbw==", "license": "MIT", "peerDependencies": { "react": "^17 || ^18", @@ -3185,20 +3179,26 @@ } }, "node_modules/@patternfly/react-styles": { - "version": "6.0.0-prerelease.6", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.0.0-prerelease.6.tgz", - "integrity": "sha512-tI28gIJFgbgVQs7Xj705csfl6T92dr5Bh7ynR5gN4+QdTWCUWmSctp46G2ZewXdrIN+C+2zUPE86o77aFp4CWw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.0.0.tgz", + "integrity": "sha512-fJFMB89sTRGlZTzTLmpRmthgOXqcN078scHMFJ3ttfi2D2btnem5oZrxmQ/gPZkZOxR+9MqwKDB6l3F5x1SqLQ==", + "license": "MIT" + }, + "node_modules/@patternfly/react-tokens": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.0.0.tgz", + "integrity": "sha512-xd0ynDkiIW2rp8jz4TNvR4Dyaw9kSMkZdsuYcLlFXCVmvX//Mnl4rhBnid/2j2TaqK0NbkyTTPnPY/BU7SfLVQ==", "license": "MIT" }, "node_modules/@patternfly/virtual-assistant": { - "version": "2.0.0-alpha.61", - "resolved": "https://registry.npmjs.org/@patternfly/virtual-assistant/-/virtual-assistant-2.0.0-alpha.61.tgz", - "integrity": "sha512-5hWy5Yq6udT9VZmyAPZbQ63/nAm5K6QOvcmCoVSd6xl6eLPG5X3wiW0NuJQi9rJGSMPuGRufsqCEvkzW8yFEMQ==", + "version": "2.1.0-prerelease.8", + "resolved": "https://registry.npmjs.org/@patternfly/virtual-assistant/-/virtual-assistant-2.1.0-prerelease.8.tgz", + "integrity": "sha512-VbU/BluxZfn4aY58dBi0tQQZK1upGrj5JtfrqgRi64K4JCAT4NGaRghEtRdPgI1HHE6OPQtXam/e+2EeaU8vOQ==", "license": "MIT", "dependencies": { - "@patternfly/react-code-editor": "6.0.0-prerelease.21", - "@patternfly/react-core": "6.0.0-prerelease.21", - "@patternfly/react-icons": "6.0.0-prerelease.7", + "@patternfly/react-code-editor": "^6.0.0", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", "clsx": "^2.1.0", "framer-motion": "^11.3.28", "path-browserify": "^1.0.1", diff --git a/package.json b/package.json index c292433..f234796 100644 --- a/package.json +++ b/package.json @@ -75,10 +75,10 @@ "webpack-merge": "^5.10.0" }, "dependencies": { - "@patternfly/react-core": "^6.0.0-prerelease.21", - "@patternfly/react-icons": "^6.0.0-prerelease.7", - "@patternfly/react-styles": "^6.0.0-prerelease.6", - "@patternfly/virtual-assistant": "^2.0.0-alpha.61", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", + "@patternfly/virtual-assistant": "^2.1.0-prerelease.8", "react": "^18.3.1", "react-dom": "^18.3.1", "sirv-cli": "^2.0.2" diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index c7cef3c..04a85b6 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -26,7 +26,7 @@ const AppLayout: React.FunctionComponent = () => { const [sidebarOpen, setSidebarOpen] = React.useState(true); const masthead = ( - + + )} @@ -327,6 +254,7 @@ const BaseChatbot: React.FunctionComponent = () => { role="bot" content={currentMessage.join('')} {...(currentSources && { sources: { sources: currentSources } })} + timestamp={`${currentDate?.toLocaleDateString()} ${currentDate?.toLocaleTimeString()}`} /> )}
diff --git a/src/app/Compare/Compare.tsx b/src/app/Compare/Compare.tsx new file mode 100644 index 0000000..e1328c8 --- /dev/null +++ b/src/app/Compare/Compare.tsx @@ -0,0 +1,145 @@ +import { CompareChatbot } from '@app/Compare/CompareChatbot'; +import { CannedChatbot } from '@app/types/CannedChatbot'; +import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; +import { ChatbotFooter, ChatbotFootnote, MessageBar } from '@patternfly/virtual-assistant'; +import * as React from 'react'; +import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'; + +export const Compare: React.FunctionComponent = () => { + const { chatbots } = useLoaderData() as { chatbots: CannedChatbot[] }; + const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false); + const [input, setInput] = React.useState(); + const [hasNewInput, setHasNewInput] = React.useState(false); + const [firstController, setFirstController] = React.useState(); + const [secondController, setSecondController] = React.useState(); + const [firstChatbot, setFirstChatbot] = React.useState(); + const [secondChatbot, setSecondChatbot] = React.useState(); + const [isSelected, setIsSelected] = React.useState('toggle-group-assistant-1'); + const [showFirstChatbot, setShowFirstChatbot] = React.useState(true); + const [showSecondChatbot, setShowSecondChatbot] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + const assistants = searchParams.get('assistants')?.split(','); + const navigate = useNavigate(); + + const handleToggleClick = (event) => { + const id = event.currentTarget.id; + setIsSelected(id); + setShowSecondChatbot(!showSecondChatbot); + setShowFirstChatbot(!showFirstChatbot); + }; + + const handleSend = (value: string) => { + setInput(value); + setHasNewInput(!hasNewInput); + }; + + React.useEffect(() => { + document.title = `Red Hat Composer AI Studio | Compare`; + if (assistants && assistants.length === 2) { + const actualChatbots = chatbots.filter( + (chatbot) => chatbot.name === assistants[0] || chatbot.name === assistants[1], + ); + if (actualChatbots.length === 2) { + setFirstChatbot(actualChatbots[0]); + setSecondChatbot(actualChatbots[1]); + } else { + // assistants are not real + navigate('/'); + } + } else { + // not enough assistants, or duplicate assistants + navigate('/'); + } + + const updateChatbotVisibility = () => { + if (window.innerWidth >= 901) { + setShowFirstChatbot(true); + setShowSecondChatbot(true); + } else { + setShowFirstChatbot(true); + setShowSecondChatbot(false); + setIsSelected('toggle-group-assistant-1'); + } + }; + window.addEventListener('resize', updateChatbotVisibility); + + return () => { + window.removeEventListener('resize', updateChatbotVisibility); + }; + }, []); + + const changeSearchParams = (_event, value: string, order: string) => { + if (order === 'first' && secondChatbot) { + setSearchParams(`assistants=${value},${secondChatbot.name}`); + } + if (order === 'second' && firstChatbot) { + setSearchParams(`assistants=${firstChatbot.name},${value}`); + } + }; + + return ( + firstChatbot && + secondChatbot && ( +
+
+ + + + +
+
+
+ +
+
+ +
+
+ + + + +
+ ) + ); +}; diff --git a/src/app/Compare/CompareChatbot.tsx b/src/app/Compare/CompareChatbot.tsx new file mode 100644 index 0000000..0d046f0 --- /dev/null +++ b/src/app/Compare/CompareChatbot.tsx @@ -0,0 +1,271 @@ +import * as React from 'react'; +import { + Chatbot, + ChatbotAlert, + ChatbotContent, + ChatbotDisplayMode, + ChatbotHeader, + ChatbotHeaderMain, + ChatbotWelcomePrompt, + Message, + MessageBox, + MessageProps, +} from '@patternfly/virtual-assistant'; +import { CannedChatbot } from '../types/CannedChatbot'; +import { HeaderDropdown } from '@app/HeaderDropdown/HeaderDropdown'; +import { ERROR_TITLE, getId } from '@app/utils/utils'; +interface Source { + link: string; +} + +interface CompareChatbotProps { + chatbot: CannedChatbot; + allChatbots: CannedChatbot[]; + setIsSendButtonDisabled: (bool: boolean) => void; + controller?: AbortController; + setController: (controller: AbortController | undefined) => void; + input?: string; + hasNewInput: boolean; + setChatbot: (value: CannedChatbot) => void; + setSearchParams: (_event, value: string, order: string) => void; + order: string; +} + +const CompareChatbot: React.FunctionComponent = ({ + chatbot, + allChatbots, + setIsSendButtonDisabled, + controller, + setController, + input, + hasNewInput, + setChatbot, + setSearchParams, + order, +}: CompareChatbotProps) => { + const [messages, setMessages] = React.useState([]); + const [currentMessage, setCurrentMessage] = React.useState([]); + const [currentSources, setCurrentSources] = React.useState(); + const scrollToBottomRef = React.useRef(null); + const [error, setError] = React.useState<{ title: string; body: string }>(); + const [announcement, setAnnouncement] = React.useState(); + const [currentChatbot, setCurrentChatbot] = React.useState(chatbot); + const [currentDate, setCurrentDate] = React.useState(); + + const handleSend = async (input: string) => { + setIsSendButtonDisabled(true); + const date = new Date(); + const newMessages = structuredClone(messages); + if (currentMessage.length > 0) { + newMessages.push({ + id: getId(), + name: currentChatbot?.displayName, + role: 'bot', + content: currentMessage.join(''), + ...(currentSources && { sources: { sources: currentSources } }), + timestamp: currentDate + ? `${currentDate.toLocaleDateString()} ${currentDate.toLocaleTimeString()}` + : `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`, + }); + setCurrentMessage([]); + setCurrentSources(undefined); + } + newMessages.push({ + id: getId(), + name: 'You', + role: 'user', + content: input, + timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`, + }); + setMessages(newMessages); + setCurrentDate(date); + // make announcement to assistive devices that new messages have been added + setAnnouncement(`Message from You: ${input}. Message from Chatbot is loading.`); + + const sources = await fetchData(input); + if (sources) { + setCurrentSources(sources); + } + // make announcement to assistive devices that new message has been added + currentMessage.length > 0 && setAnnouncement(`Message from Chatbot: ${currentMessage.join('')}`); + setIsSendButtonDisabled(false); + }; + + React.useEffect(() => { + if (input) { + handleSend(input); + } + }, [hasNewInput]); + + // Auto-scrolls to the latest message + React.useEffect(() => { + // don't scroll the first load, but scroll if there's a current stream or a new source has popped up + if (messages.length > 0 || currentMessage || currentSources) { + scrollToBottomRef.current?.scrollIntoView(); + } + }, [messages, currentMessage, currentSources]); + + const url = process.env.REACT_APP_ROUTER_URL ?? ''; + + const ERROR_BODY = { + 'Error: 404': `${currentChatbot?.displayName} is currently unavailable. Use a different assistant or try again later.`, + 'Error: 500': `${currentChatbot?.displayName} has encountered an error and is unable to answer your question. Use a different assistant or try again later.`, + 'Error: Other': `${currentChatbot?.displayName} has encountered an error and is unable to answer your question. Use a different assistant or try again later.`, + }; + + const handleError = (e) => { + const title = ERROR_TITLE; + const body = ERROR_BODY; + let newError; + if (title && body) { + newError = { title: ERROR_TITLE[e], body: ERROR_BODY[e] }; + } else { + newError = { title: 'Error', body: e.message }; + } + setError(newError); + // make announcement to assistive devices that there was an error + setAnnouncement(`Error: ${newError.title} ${newError.body}`); + }; + + // fixme this is getting too large; we should refactor + async function fetchData(userMessage: string) { + if (controller) { + controller.abort(); + } + + const newController = new AbortController(); + setController(newController); + + try { + let isSource = false; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: userMessage, + assistantName: currentChatbot?.name, + }), + signal: newController?.signal, + }); + + if (!response.ok || !response.body) { + switch (response.status) { + case 500: + throw new Error('500'); + case 404: + throw new Error('404'); + default: + throw new Error('Other'); + } + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder('utf-8'); + let done; + const sources: string[] = []; + + while (!done) { + const { done, value } = await reader.read(); + if (done) { + break; + } + + const chunk = decoder.decode(value, { stream: true }); + if (chunk.includes('Sources used to generate this content')) { + sources.push(chunk); + isSource = true; + } else { + if (isSource) { + sources.push(chunk); + } else { + setCurrentMessage((prevData) => [...prevData, chunk]); + } + } + } + + if (sources) { + const sourceLinks = sources.join('').split('Sources used to generate this content:\n')[1]; + const sourceLinksArr = sourceLinks.split('\n').filter((source) => source !== ''); + const formattedSources: Source[] = []; + sourceLinksArr.forEach((source) => formattedSources.push({ link: source })); + setController(newController); + return formattedSources; + } + + return undefined; + } catch (error) { + if (error instanceof Error) { + if (error.name !== 'AbortError') { + handleError(error); + } + } + return undefined; + } finally { + setController(undefined); + } + } + + const displayMode = ChatbotDisplayMode.embedded; + + const onSelect = (_event: React.MouseEvent | undefined, value: CannedChatbot) => { + if (controller) { + controller.abort(); + } + setController(undefined); + setCurrentChatbot(value); + setMessages([]); + setCurrentMessage([]); + setCurrentSources(undefined); + setError(undefined); + setAnnouncement(undefined); + setIsSendButtonDisabled(false); + setChatbot(value); + setSearchParams(_event, value.name, order); + }; + + return ( + + + + + + + + + {error && ( + { + setError(undefined); + }} + title={error.title} + > + {error.body} + + )} + + {messages.map((message) => ( + + ))} + {currentMessage.length > 0 && ( + + )} +
+
+
+
+ ); +}; + +export { CompareChatbot }; diff --git a/src/app/HeaderDropdown/HeaderDropdown.tsx b/src/app/HeaderDropdown/HeaderDropdown.tsx new file mode 100644 index 0000000..f5dc13b --- /dev/null +++ b/src/app/HeaderDropdown/HeaderDropdown.tsx @@ -0,0 +1,101 @@ +import { CannedChatbot } from '@app/types/CannedChatbot'; +import { + Dropdown, + DropdownItem, + DropdownList, + MenuSearch, + MenuSearchInput, + MenuToggle, + MenuToggleElement, + SearchInput, +} from '@patternfly/react-core'; +import * as React from 'react'; + +interface HeaderDropdownProps { + chatbots: CannedChatbot[]; + selectedChatbot?: CannedChatbot; + onSelect: (_event: React.MouseEvent | undefined, value: CannedChatbot) => void; +} +export const HeaderDropdown: React.FunctionComponent = ({ + chatbots, + selectedChatbot, + onSelect, +}) => { + const [isOpen, setIsOpen] = React.useState(false); + const [visibleAssistants, setVisibleAssistants] = React.useState(chatbots); + + const onToggleClick = () => { + setIsOpen(!isOpen); + setVisibleAssistants(chatbots); + }; + + const findMatchingElements = (chatbots: CannedChatbot[], targetValue: string) => { + const matchingElements = chatbots.filter((chatbot) => + chatbot.displayName.toLowerCase().includes(targetValue.toLowerCase()), + ); + return matchingElements; + }; + + const onTextInputChange = (value: string) => { + if (value === '') { + setVisibleAssistants(chatbots); + return; + } + const newVisibleAssistants = findMatchingElements(chatbots, value); + setVisibleAssistants(newVisibleAssistants); + }; + + const handleSelect = (_event: React.MouseEvent | undefined, value: CannedChatbot) => { + // don't do a select if they choose "no results found" + if (value) { + setIsOpen(false); + onSelect(_event, value); + } + }; + + return ( + setIsOpen(isOpen)} + ouiaId="BasicDropdown" + shouldFocusToggleOnSelect + onOpenChangeKeys={['Escape']} + toggle={(toggleRef: React.Ref) => { + return ( + + Red Hat AI Assistant + + ); + }} + popperProps={{ appendTo: 'inline' }} + > + + + onTextInputChange(value)} + placeholder="Search assistants..." + /> + + + + {visibleAssistants && visibleAssistants?.length > 0 ? ( + visibleAssistants?.map((chatbot) => ( + + {chatbot.displayName} + + )) + ) : ( + No results found + )} + + + ); +}; diff --git a/src/app/app.css b/src/app/app.css index 4093413..89ad26f 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -4,6 +4,12 @@ body, height: 100%; } +@media screen and (min-width: 64rem) { + .pf-chatbot--embedded .pf-chatbot__messagebox { + width: 100%; + } +} + .pf-v6-c-page__main, pf-v6-c-page__main-container.pf-m-fill { overflow-y: hidden; @@ -18,3 +24,106 @@ pf-v6-c-page__main-container.pf-m-fill { max-height: 80vh; overflow-y: auto; } + +.compare { + display: flex; + height: 100%; + width: 100%; + + @media screen and (max-width: 900px) { + overflow-y: auto; + } + + .compare-item:first-of-type { + border-right: 1px solid var(--pf-t--global--border--color--default); + + @media screen and (max-width: 900px) { + border-right: 0px; + } + } + + .compare-item { + flex: 1; + + .pf-chatbot--embedded .pf-chatbot__messagebox { + width: 100%; + } + + .pf-chatbot__content { + padding: 0; + } + } +} + +.compare-mobile-controls { + padding: var(--pf-t--global--spacer--md) var(--pf-t--global--spacer--lg) 0 var(--pf-t--global--spacer--lg); + display: none; + background-color: var(--pf-t--global--background--color--secondary--default); + position: sticky; + top: 0; + z-index: 9999; /*fixme*/ + + @media screen and (max-width: 900px) { + display: flex; + flex-direction: column; + gap: var(--pf-t--global--spacer--md); + } +} + +.compare-item { + .pf-chatbot.pf-chatbot--embedded { + @media screen and (max-width: 900px) { + height: 100%; + } + } +} +.compare-item-hidden { + display: block; + + @media screen and (max-width: 900px) { + display: none; + } +} + +.compare-container { + display: flex; + flex-direction: column; + position: relative; + height: 100%; + + .pf-chatbot__footer { + position: sticky; + bottom: 0; + } +} + +.compare-toggle { + width: 100%; + + .pf-v6-c-toggle-group__button { + width: 100%; + display: flex; + justify-content: center; + } +} + +.chatbot-ui-page { + .pf-v6-c-page__main { + overflow: hidden; + } + @media screen and (max-width: 900px) { + --pf-v6-c-page__main-container--MaxHeight: 100%; + } + .pf-v6-c-page__main-container { + @media screen and (min-width: 1024px) { + border-top: 0px; + } + @media screen and (max-width: 900px) { + --pf-v6-c-page__main-container--MarginInlineStart: 0; + --pf-v6-c-page__main-container--MarginInlineEnd: 0; + --pf-v6-c-page__main-container--BorderWidth: 0; + --pf-v6-c-page__main-container--BorderColor: initial; + --pf-v6-c-page__main-container--BorderRadius: none; + } + } +} diff --git a/src/app/routes.tsx b/src/app/routes.tsx index 8ec734c..cd6b30d 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import { NotFound } from '@app/NotFound/NotFound'; -import { BaseChatbot, loader as chatbotLoader } from './BaseChatbot/BaseChatbot'; -import { AppLayout } from './AppLayout/AppLayout'; -import { Home } from './Home/Home'; +import { BaseChatbot } from '@app/BaseChatbot/BaseChatbot'; +import { AppLayout } from '@app/AppLayout/AppLayout'; +import { Compare } from '@app/Compare/Compare'; +import { chatbotLoader } from '@app/utils/utils'; export interface IAppRoute { label?: string; // Excluding the label will exclude the route from the nav sidebar in AppLayout @@ -20,10 +21,7 @@ export interface IAppRouteGroup { export type AppRouteConfig = IAppRoute | IAppRouteGroup; // used for navigation panel -const routes: AppRouteConfig[] = [ - { path: '/', label: 'Home', title: 'Red Hat Composer AI Studio | Home' }, - { path: '/chats', label: 'Chats', title: 'Red Hat Composer AI Studio | Chats' }, -]; +const routes: AppRouteConfig[] = [{ path: '/', label: 'Home', title: 'Red Hat Composer AI Studio | Home' }]; // used for actual routing const router = createBrowserRouter([ @@ -33,11 +31,13 @@ const router = createBrowserRouter([ children: [ { path: '/', - element: , + element: , + loader: chatbotLoader, + errorElement: , }, { - path: 'Chats', - element: , + path: 'compare', + element: , loader: chatbotLoader, errorElement: , }, diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts new file mode 100644 index 0000000..9b6cec2 --- /dev/null +++ b/src/app/utils/utils.ts @@ -0,0 +1,29 @@ +import { CannedChatbot } from '@app/types/CannedChatbot'; + +export const ERROR_TITLE = { + 'Error: 404': '404: Network error', + 'Error: 500': 'Server error', + 'Error: Other': 'Error', +}; + +export const getId = () => { + const date = Date.now() + Math.random(); + return date.toString(); +}; + +export 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 chatbotLoader() { + const chatbots = await getChatbots(); + return { chatbots }; +} diff --git a/tsconfig.json b/tsconfig.json index 9c2485f..1cda565 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "outDir": "dist", "module": "esnext", "target": "es5", - "lib": ["es6", "dom"], + "lib": ["es6", "dom", "dom.iterable", "es2019"], "sourceMap": true, "jsx": "react", "moduleResolution": "node", @@ -24,11 +24,6 @@ "importHelpers": true, "skipLibCheck": true }, - "include": [ - "**/*.ts", - "**/*.tsx", - "**/*.jsx", - "**/*.js" - ], + "include": ["**/*.ts", "**/*.tsx", "**/*.jsx", "**/*.js"], "exclude": ["node_modules"] }