diff --git a/workspaces/lightspeed/.changeset/migrate-mui-v5.md b/workspaces/lightspeed/.changeset/migrate-mui-v5.md new file mode 100644 index 0000000000..d8404239d7 --- /dev/null +++ b/workspaces/lightspeed/.changeset/migrate-mui-v5.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed': patch +--- + +Migrated from Material UI v4 (`@material-ui/*`) to MUI v5 (`@mui/material`, `@mui/styles`). Replaced all `makeStyles`/`createStyles` usage with `styled()` and `sx` prop. Added `StylesProvider` with seeded `createGenerateClassName` for JSS collision prevention. Added ESLint restrictions to prevent `@material-ui/*` imports from being reintroduced. diff --git a/workspaces/lightspeed/eslint.frontend-shared.cjs b/workspaces/lightspeed/eslint.frontend-shared.cjs new file mode 100644 index 0000000000..02cb00ae23 --- /dev/null +++ b/workspaces/lightspeed/eslint.frontend-shared.cjs @@ -0,0 +1,43 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const materialUiMigrationEslintConfig = { + restrictedImports: [ + { + name: '@material-ui/core', + message: 'Use @mui/material instead of Material UI v4.', + }, + { + name: '@material-ui/lab', + message: 'Use @mui/material instead of Material UI v4.', + }, + { + name: '@material-ui/styles', + message: + 'Use @mui/styles, @mui/material (sx/styled), or Backstage UI instead of Material UI v4.', + }, + ], + restrictedImportPatterns: ['@material-ui/*'], +}; + +/** + * ESLint config for frontend packages in this workspace (MUI v4 migration guards). + */ +module.exports = packageDir => + require('@backstage/cli/config/eslint-factory')( + packageDir, + materialUiMigrationEslintConfig, + ); diff --git a/workspaces/lightspeed/packages/app/.eslintrc.js b/workspaces/lightspeed/packages/app/.eslintrc.js index 9184408ae4..493ce7565d 100644 --- a/workspaces/lightspeed/packages/app/.eslintrc.js +++ b/workspaces/lightspeed/packages/app/.eslintrc.js @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports -- workspace ESLint shared config +module.exports = require('../../eslint.frontend-shared.cjs')(__dirname); diff --git a/workspaces/lightspeed/packages/app/package.json b/workspaces/lightspeed/packages/app/package.json index 9599f7c802..c60a197d16 100644 --- a/workspaces/lightspeed/packages/app/package.json +++ b/workspaces/lightspeed/packages/app/package.json @@ -32,8 +32,8 @@ "@backstage/plugin-search": "^1.7.0", "@backstage/plugin-user-settings": "^0.9.1", "@backstage/ui": "^0.13.1", - "@material-ui/core": "^4.12.2", - "@material-ui/icons": "^4.9.1", + "@mui/icons-material": "^6.1.8", + "@mui/material": "^5.12.2", "@red-hat-developer-hub/backstage-plugin-app-react": "^0.0.5", "@red-hat-developer-hub/backstage-plugin-lightspeed": "workspace:^", "react": "^18.0.2", diff --git a/workspaces/lightspeed/packages/app/src/modules/nav/LogoFull.tsx b/workspaces/lightspeed/packages/app/src/modules/nav/LogoFull.tsx index 6b6a1bd8c1..9e835665ae 100644 --- a/workspaces/lightspeed/packages/app/src/modules/nav/LogoFull.tsx +++ b/workspaces/lightspeed/packages/app/src/modules/nav/LogoFull.tsx @@ -13,31 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { makeStyles } from '@material-ui/core'; +import { styled } from '@mui/material/styles'; -const useStyles = makeStyles({ - svg: { - width: 'auto', - height: 30, - }, - path: { - fill: '#7df3e1', - }, +const StyledSvg = styled('svg')({ + width: 'auto', + height: 30, }); -export const LogoFull = () => { - const classes = useStyles(); +const StyledPath = styled('path')({ + fill: '#7df3e1', +}); +export const LogoFull = () => { return ( - - - + + + ); }; diff --git a/workspaces/lightspeed/packages/app/src/modules/nav/LogoIcon.tsx b/workspaces/lightspeed/packages/app/src/modules/nav/LogoIcon.tsx index 87024b08c2..fc7c64f81a 100644 --- a/workspaces/lightspeed/packages/app/src/modules/nav/LogoIcon.tsx +++ b/workspaces/lightspeed/packages/app/src/modules/nav/LogoIcon.tsx @@ -13,31 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { makeStyles } from '@material-ui/core'; +import { styled } from '@mui/material/styles'; -const useStyles = makeStyles({ - svg: { - width: 'auto', - height: 28, - }, - path: { - fill: '#7df3e1', - }, +const StyledSvg = styled('svg')({ + width: 'auto', + height: 28, }); -export const LogoIcon = () => { - const classes = useStyles(); +const StyledPath = styled('path')({ + fill: '#7df3e1', +}); +export const LogoIcon = () => { return ( - - - + + + ); }; diff --git a/workspaces/lightspeed/packages/app/src/modules/nav/Sidebar.tsx b/workspaces/lightspeed/packages/app/src/modules/nav/Sidebar.tsx index 606c5f2147..7e9f9cd30c 100644 --- a/workspaces/lightspeed/packages/app/src/modules/nav/Sidebar.tsx +++ b/workspaces/lightspeed/packages/app/src/modules/nav/Sidebar.tsx @@ -24,7 +24,7 @@ import { } from '@backstage/core-components'; import { NavContentBlueprint } from '@backstage/plugin-app-react'; import { SidebarLogo } from './SidebarLogo'; -import MenuIcon from '@material-ui/icons/Menu'; +import MenuIcon from '@mui/icons-material/Menu'; export const SidebarContent = NavContentBlueprint.make({ params: { diff --git a/workspaces/lightspeed/packages/app/src/modules/nav/SidebarLogo.tsx b/workspaces/lightspeed/packages/app/src/modules/nav/SidebarLogo.tsx index 0085b75fce..04da2b85a6 100644 --- a/workspaces/lightspeed/packages/app/src/modules/nav/SidebarLogo.tsx +++ b/workspaces/lightspeed/packages/app/src/modules/nav/SidebarLogo.tsx @@ -18,34 +18,37 @@ import { sidebarConfig, useSidebarOpenState, } from '@backstage/core-components'; -import { makeStyles } from '@material-ui/core'; + +import Box from '@mui/material/Box'; + import { LogoFull } from './LogoFull'; import { LogoIcon } from './LogoIcon'; -const useSidebarLogoStyles = makeStyles({ - root: { - width: sidebarConfig.drawerWidthClosed, - height: 3 * sidebarConfig.logoHeight, - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'center', - marginBottom: -14, - }, - link: { - width: sidebarConfig.drawerWidthClosed, - marginLeft: 24, - }, -}); - export const SidebarLogo = () => { - const classes = useSidebarLogoStyles(); const { isOpen } = useSidebarOpenState(); return ( -
- + + {isOpen ? : } -
+ ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/.eslintrc.js b/workspaces/lightspeed/plugins/lightspeed/.eslintrc.js index e2c39f94df..493ce7565d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/.eslintrc.js +++ b/workspaces/lightspeed/plugins/lightspeed/.eslintrc.js @@ -14,4 +14,5 @@ * limitations under the License. */ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports -- workspace ESLint shared config +module.exports = require('../../eslint.frontend-shared.cjs')(__dirname); diff --git a/workspaces/lightspeed/plugins/lightspeed/package.json b/workspaces/lightspeed/plugins/lightspeed/package.json index 9f7af121a8..c125fbb456 100644 --- a/workspaces/lightspeed/plugins/lightspeed/package.json +++ b/workspaces/lightspeed/plugins/lightspeed/package.json @@ -59,8 +59,6 @@ "@backstage/plugin-app-react": "^0.2.1", "@backstage/plugin-permission-react": "^0.4.41", "@backstage/theme": "^0.7.2", - "@material-ui/core": "^4.9.13", - "@material-ui/lab": "^4.0.0-alpha.61", "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^6.1.8", "@mui/material": "^5.12.2", diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/Attachment.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/Attachment.tsx index 00502f5409..89ebe97ffc 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/Attachment.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/Attachment.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core'; +import GlobalStyles from '@mui/material/GlobalStyles'; import { AttachmentEdit, ChatbotDisplayMode, @@ -24,13 +24,8 @@ import { import { useTranslation } from '../hooks/useTranslation'; import { useFileAttachmentContext } from './AttachmentContext'; -const useStyles = makeStyles(() => ({ - modalFooter: { - '&>button': { - width: '12% !important', - }, - }, -})); +const MODAL_FOOTER_CLASS = 'lightspeed-modal-footer'; + const Attachment = () => { const { currentFileContent, @@ -38,7 +33,6 @@ const Attachment = () => { modalState, setCurrentFileContent, } = useFileAttachmentContext(); - const classes = useStyles(); const { t } = useTranslation(); if (!currentFileContent) { @@ -55,6 +49,13 @@ const Attachment = () => { return ( <> + button`]: { + width: '12% !important', + }, + }} + /> { secondaryActionButtonText={t('modal.close')} primaryActionButtonText={t('modal.edit')} title={t('modal.title.preview')} - modalFooterClassName={classes.modalFooter} + modalFooterClassName={MODAL_FOOTER_CLASS} onEdit={() => { setIsPreviewModalOpen(false); setIsEditModalOpen(true); @@ -83,7 +84,7 @@ const Attachment = () => { title={t('modal.title.edit')} secondaryActionButtonText={t('modal.cancel')} primaryActionButtonText={t('modal.save')} - modalFooterClassName={classes.modalFooter} + modalFooterClassName={MODAL_FOOTER_CLASS} onSave={(_, content) => { setCurrentFileContent({ ...currentFileContent, @@ -95,7 +96,6 @@ const Attachment = () => { ); if (existingIndex !== -1) { - // Update the existing file's content const updated = [...prev]; updated[existingIndex] = { ...updated[existingIndex], @@ -104,7 +104,6 @@ const Attachment = () => { return updated; } - // File doesn't exist, add a new one return [ ...prev, { diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx index 8052ff5b17..bcfd695b07 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core'; +import { styled } from '@mui/material/styles'; import { Button, Tooltip } from '@patternfly/react-core'; import { useTranslation } from '../hooks/useTranslation'; @@ -39,51 +39,51 @@ export const PencilIcon = ({ className }: IconProps) => ( ); -const useStyles = makeStyles(theme => ({ - strip: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - paddingTop: theme.spacing(1.5), - gap: theme.spacing(1.5), - borderRight: '1px solid var(--pf-t--global--border--color--default)', - width: 48, - minWidth: 48, - flexShrink: 0, - backgroundColor: 'var(--pf-t--global--background--color--primary--default)', - height: '100%', +const StripRoot = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + paddingTop: theme.spacing(1.5), + gap: theme.spacing(1.5), + borderRight: '1px solid var(--pf-t--global--border--color--default)', + width: 48, + minWidth: 48, + flexShrink: 0, + backgroundColor: 'var(--pf-t--global--background--color--primary--default)', + height: '100%', +})); + +const StyledIconButton = styled(Button)({ + padding: '8px !important', + minWidth: 0, + lineHeight: 1, + borderRadius: '8px !important', + border: '1px solid var(--pf-t--global--border--color--default) !important', + color: 'var(--pf-t--global--icon--color--regular)', + '& svg': { + width: 18, + height: 18, }, - iconButton: { - padding: '8px !important', - minWidth: 0, - lineHeight: 1, - borderRadius: '8px !important', - border: '1px solid var(--pf-t--global--border--color--default) !important', - color: 'var(--pf-t--global--icon--color--regular)', - '& svg': { - width: 18, - height: 18, - }, - '&:hover': { - color: 'var(--pf-t--global--icon--color--hover) !important', - backgroundColor: - 'var(--pf-t--global--background--color--action--plain--hover) !important', - }, + '&:hover': { + color: 'var(--pf-t--global--icon--color--hover) !important', + backgroundColor: + 'var(--pf-t--global--background--color--action--plain--hover) !important', }, - newChatIconButton: { - padding: '8px !important', - minWidth: 0, - lineHeight: 1, - borderRadius: '8px !important', - border: '1px solid var(--pf-t--global--border--color--default) !important', - color: 'var(--pf-t--global--color--brand--default)', - '&:hover': { - color: 'var(--pf-t--global--color--brand--hover) !important', - backgroundColor: - 'var(--pf-t--global--background--color--action--plain--hover) !important', - }, +}); + +const StyledNewChatButton = styled(Button)({ + padding: '8px !important', + minWidth: 0, + lineHeight: 1, + borderRadius: '8px !important', + border: '1px solid var(--pf-t--global--border--color--default) !important', + color: 'var(--pf-t--global--color--brand--default)', + '&:hover': { + color: 'var(--pf-t--global--color--brand--hover) !important', + backgroundColor: + 'var(--pf-t--global--background--color--action--plain--hover) !important', }, -})); +}); type CollapsedHistoryStripProps = { onExpand: () => void; @@ -96,32 +96,29 @@ export const CollapsedHistoryStrip = ({ onNewChat, newChatDisabled = false, }: CollapsedHistoryStripProps) => { - const classes = useStyles(); const { t } = useTranslation(); return ( -
+ - + - + -
+ ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/FilePreview.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/FilePreview.tsx index fb9e588363..448c58c98f 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/FilePreview.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/FilePreview.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Divider } from '@material-ui/core'; +import Divider from '@mui/material/Divider'; import { FileDetailsLabel } from '@patternfly/chatbot'; import { useFileAttachmentContext } from './AttachmentContext'; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index dbf0ae45d4..7d493b8b48 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -34,7 +34,8 @@ import { useLocation, useMatch, useNavigate } from 'react-router-dom'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { Button, makeStyles } from '@material-ui/core'; +import Button from '@mui/material/Button'; +import { styled } from '@mui/material/styles'; import Tab from '@mui/material/Tab'; import Tabs from '@mui/material/Tabs'; import { @@ -140,10 +141,61 @@ const ConditionalWrapper = ({ children: React.ReactNode; }) => (condition ? wrapper(children) : children); -const useStyles = makeStyles(theme => ({ - body: { - // remove default margin and padding from common elements - // lists excluded for proper formatting +const PREFIX = 'LightspeedChat'; +const classes = { + body: `${PREFIX}-body`, + header: `${PREFIX}-header`, + errorContainer: `${PREFIX}-errorContainer`, + drawerFileDropZone: `${PREFIX}-drawerFileDropZone`, + headerMenu: `${PREFIX}-headerMenu`, + headerLogo: `${PREFIX}-headerLogo`, + headerTitle: `${PREFIX}-headerTitle`, + headerDivider: `${PREFIX}-headerDivider`, + notebooksContainer: `${PREFIX}-notebooksContainer`, + notebooksHeader: `${PREFIX}-notebooksHeader`, + notebooksHeading: `${PREFIX}-notebooksHeading`, + notebooksHeadingEmpty: `${PREFIX}-notebooksHeadingEmpty`, + notebooksEmptyState: `${PREFIX}-notebooksEmptyState`, + notebooksIcon: `${PREFIX}-notebooksIcon`, + notebooksDescription: `${PREFIX}-notebooksDescription`, + notebooksAction: `${PREFIX}-notebooksAction`, + notebooksActionEmpty: `${PREFIX}-notebooksActionEmpty`, + notebooksGrid: `${PREFIX}-notebooksGrid`, + notebookCard: `${PREFIX}-notebookCard`, + notebookCardHeader: `${PREFIX}-notebookCardHeader`, + notebookCardDivider: `${PREFIX}-notebookCardDivider`, + notebookCardBody: `${PREFIX}-notebookCardBody`, + notebookDocuments: `${PREFIX}-notebookDocuments`, + notebookUpdated: `${PREFIX}-notebookUpdated`, + notebookTitle: `${PREFIX}-notebookTitle`, + notebookCardHeaderActions: `${PREFIX}-notebookCardHeaderActions`, + notebookTitleText: `${PREFIX}-notebookTitleText`, + notebookMenuButton: `${PREFIX}-notebookMenuButton`, + notebookDropdownList: `${PREFIX}-notebookDropdownList`, + notebookDropdownMenu: `${PREFIX}-notebookDropdownMenu`, + notebookDropdownItem: `${PREFIX}-notebookDropdownItem`, + footer: `${PREFIX}-footer`, + fullscreenFooter: `${PREFIX}-fullscreenFooter`, + messageBar: `${PREFIX}-messageBar`, + sortDropdown: `${PREFIX}-sortDropdown`, + chatbotContent: `${PREFIX}-chatbotContent`, + chatbotContentHasOverflow: `${PREFIX}-chatbotContentHasOverflow`, + chatbotContentScroll: `${PREFIX}-chatbotContentScroll`, + chatbotContentScrollNewChat: `${PREFIX}-chatbotContentScrollNewChat`, + toastAlertGroup: `${PREFIX}-toastAlertGroup`, + toastAlert: `${PREFIX}-toastAlert`, + chatbotContentSpacer: `${PREFIX}-chatbotContentSpacer`, + settingsFlat: `${PREFIX}-settingsFlat`, + mcpFullscreenLayout: `${PREFIX}-mcpFullscreenLayout`, + mcpChatPane: `${PREFIX}-mcpChatPane`, + mcpSettingsPane: `${PREFIX}-mcpSettingsPane`, + mcpCollapsedDrawerOrderFix: `${PREFIX}-mcpCollapsedDrawerOrderFix`, + fullscreenChatLayout: `${PREFIX}-fullscreenChatLayout`, + fullscreenMainContent: `${PREFIX}-fullscreenMainContent`, +}; + +const StyledChatRoot = styled('div')(({ theme }) => ({ + [`& .${classes.body}`]: { '& h1, & h2, & h3, & h4, & h5, & h6, & p, & li': { margin: 0, padding: 0, @@ -153,17 +205,15 @@ const useStyles = makeStyles(theme => ({ 'var(--pf-t--global--background--color--floating--default)', }, }, - header: { - padding: `${theme.spacing(3)}px ${theme.spacing(3)}px 0 ${theme.spacing( - 3, - )}px !important`, + [`& .${classes.header}`]: { + padding: `${theme.spacing(3)} ${theme.spacing(3)} 0 ${theme.spacing(3)} !important`, backgroundColor: 'var(--pf-t--global--background--color--floating--default) !important', }, - errorContainer: { + [`& .${classes.errorContainer}`]: { padding: theme.spacing(3), }, - drawerFileDropZone: { + [`& .${classes.drawerFileDropZone}`]: { gap: 0, rowGap: 0, columnGap: 0, @@ -172,20 +222,19 @@ const useStyles = makeStyles(theme => ({ flex: 1, minWidth: 0, }, - headerMenu: { - // align hamburger icon with title + [`& .${classes.headerMenu}`]: { '& .pf-v6-c-button': { display: 'flex', alignItems: 'center', }, }, - headerLogo: { + [`& .${classes.headerLogo}`]: { width: 48, height: 48, marginRight: theme.spacing(1.5), flexShrink: 0, }, - headerTitle: { + [`& .${classes.headerTitle}`]: { justifyContent: 'left !important', '& h1': { fontSize: '32px !important', @@ -194,13 +243,13 @@ const useStyles = makeStyles(theme => ({ fontFamily: '"Red Hat Display", sans-serif !important', }, }, - headerDivider: { + [`& .${classes.headerDivider}`]: { paddingTop: 8, borderBottom: '1px solid var(--pf-t--global--border--color--default)', backgroundColor: 'var(--pf-t--global--background--color--floating--default)', }, - notebooksContainer: { + [`& .${classes.notebooksContainer}`]: { padding: theme.spacing(3), height: '100%', display: 'flex', @@ -211,22 +260,22 @@ const useStyles = makeStyles(theme => ({ backgroundColor: 'var(--pf-t--global--background--color--floating--default)', }, - notebooksHeader: { + [`& .${classes.notebooksHeader}`]: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: theme.spacing(4), }, - notebooksHeading: { + [`& .${classes.notebooksHeading}`]: { marginBottom: 0, }, - notebooksHeadingEmpty: { + [`& .${classes.notebooksHeadingEmpty}`]: { '&&': { marginBottom: theme.spacing(1), paddingBottom: theme.spacing(1), }, }, - notebooksEmptyState: { + [`& .${classes.notebooksEmptyState}`]: { flex: 1, display: 'flex', flexDirection: 'column', @@ -234,30 +283,30 @@ const useStyles = makeStyles(theme => ({ justifyContent: 'center', textAlign: 'center', }, - notebooksIcon: { + [`& .${classes.notebooksIcon}`]: { fontSize: 48, color: 'var(--pf-t--global--icon--color--subtle)', marginBottom: theme.spacing(1.5), }, - notebooksDescription: { + [`& .${classes.notebooksDescription}`]: { marginTop: theme.spacing(1), marginBottom: theme.spacing(3), maxWidth: 420, }, - notebooksAction: { + [`& .${classes.notebooksAction}`]: { textTransform: 'none', borderRadius: 999, paddingLeft: theme.spacing(3), paddingRight: theme.spacing(3), }, - notebooksActionEmpty: { + [`& .${classes.notebooksActionEmpty}`]: { textTransform: 'none', borderRadius: 999, paddingLeft: theme.spacing(3), paddingRight: theme.spacing(3), marginTop: theme.spacing(3), }, - notebooksGrid: { + [`& .${classes.notebooksGrid}`]: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: theme.spacing(2), @@ -271,7 +320,7 @@ const useStyles = makeStyles(theme => ({ gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', }, }, - notebookCard: { + [`& .${classes.notebookCard}`]: { borderRadius: theme.spacing(1.5), display: 'flex', flexDirection: 'column', @@ -282,62 +331,62 @@ const useStyles = makeStyles(theme => ({ cursor: 'pointer', }, }, - notebookCardHeader: { + [`& .${classes.notebookCardHeader}`]: { padding: theme.spacing(2), paddingBottom: 0, alignItems: 'center', }, - notebookCardDivider: { + [`& .${classes.notebookCardDivider}`]: { borderTop: '1px solid var(--pf-t--global--border--color--default)', marginTop: theme.spacing(1), }, - notebookCardBody: { + [`& .${classes.notebookCardBody}`]: { padding: theme.spacing(2), paddingTop: theme.spacing(1.5), }, - notebookDocuments: { + [`& .${classes.notebookDocuments}`]: { paddingTop: theme.spacing(1), paddingLeft: theme.spacing(2), }, - notebookUpdated: { + [`& .${classes.notebookUpdated}`]: { paddingBottom: theme.spacing(5), paddingLeft: theme.spacing(2), paddingTop: theme.spacing(2), }, - notebookTitle: { + [`& .${classes.notebookTitle}`]: { display: 'flex', alignItems: 'center', gap: theme.spacing(1), minWidth: 0, flex: 1, }, - notebookCardHeaderActions: { + [`& .${classes.notebookCardHeaderActions}`]: { marginLeft: theme.spacing(1), }, - notebookTitleText: { + [`& .${classes.notebookTitleText}`]: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, - notebookMenuButton: { + [`& .${classes.notebookMenuButton}`]: { color: theme.palette.text.secondary, }, - notebookDropdownList: { + [`& .${classes.notebookDropdownList}`]: { paddingTop: 0, paddingBottom: 0, paddingInlineStart: 0, }, - notebookDropdownMenu: { + [`& .${classes.notebookDropdownMenu}`]: { '--pf-v6-c-menu--PaddingBlockStart': '0', '--pf-v6-c-menu--PaddingBlockEnd': '0', }, - notebookDropdownItem: { + [`& .${classes.notebookDropdownItem}`]: { justifyContent: 'flex-start', textAlign: 'left', paddingLeft: theme.spacing(1.5), paddingRight: theme.spacing(1.5), }, - footer: { + [`& .${classes.footer}`]: { backgroundColor: 'var(--pf-t--global--background--color--floating--default)', '&>.pf-chatbot__footer-container': { @@ -346,12 +395,12 @@ const useStyles = makeStyles(theme => ({ }, '& .pf-chatbot__message-bar': { backgroundColor: - theme.palette.type === 'light' + theme.palette.mode === 'light' ? theme.palette.grey[100] : 'var(--pf-t--global--background--color--secondary--default)', }, }, - fullscreenFooter: { + [`& .${classes.fullscreenFooter}`]: { '&>.pf-chatbot__footer-container': { width: '100% !important', padding: theme.spacing(1.5), @@ -359,7 +408,7 @@ const useStyles = makeStyles(theme => ({ margin: '0 auto', }, }, - messageBar: { + [`& .${classes.messageBar}`]: { border: '1px solid var(--pf-t--global--border--color--default)', borderRadius: 24, padding: theme.spacing(0.5), @@ -367,12 +416,11 @@ const useStyles = makeStyles(theme => ({ display: 'none', }, }, - sortDropdown: { + [`& .${classes.sortDropdown}`]: { padding: 0, margin: 0, }, - // Outer content wrapper (library may override overflow; we rely on inner scroll wrapper). - chatbotContent: { + [`& .${classes.chatbotContent}`]: { minHeight: 0, display: 'flex', flexDirection: 'column', @@ -390,14 +438,13 @@ const useStyles = makeStyles(theme => ({ wordBreak: 'break-word', }, }, - chatbotContentHasOverflow: { + [`& .${classes.chatbotContentHasOverflow}`]: { '& .pf-chatbot__jump': { visibility: 'visible', pointerEvents: 'auto', }, }, - // Inner scroll container we control: always scrollable so zoomed-in users see full content. - chatbotContentScroll: { + [`& .${classes.chatbotContentScroll}`]: { minHeight: 0, flex: 1, display: 'flex', @@ -405,28 +452,27 @@ const useStyles = makeStyles(theme => ({ overflowY: 'auto', WebkitOverflowScrolling: 'touch', }, - chatbotContentScrollNewChat: { + [`& .${classes.chatbotContentScrollNewChat}`]: { backgroundColor: 'var(--pf-t--global--background--color--floating--default)', }, - toastAlertGroup: { - '--pf-v6-c-alert-group--m-toast--InsetInlineEnd': `${theme.spacing(2.5)}px`, - '--pf-v6-c-alert-group--m-toast--InsetBlockStart': `${theme.spacing(2.5)}px`, + [`& .${classes.toastAlertGroup}`]: { + '--pf-v6-c-alert-group--m-toast--InsetInlineEnd': `${theme.spacing(2.5)}`, + '--pf-v6-c-alert-group--m-toast--InsetBlockStart': `${theme.spacing(2.5)}`, '--pf-v6-c-alert-group--m-toast--MaxWidth': '350px', '--pf-v6-c-alert-group--m-toast--ZIndex': '9999', }, - toastAlert: { + [`& .${classes.toastAlert}`]: { maxWidth: '350px', '& .pf-v6-c-alert__title': { margin: 0, }, }, - // When present, pushes welcome content to bottom (zoom out). Scroll up to see important box (zoom in). - chatbotContentSpacer: { + [`& .${classes.chatbotContentSpacer}`]: { flex: 1, minHeight: 0, }, - settingsFlat: { + [`& .${classes.settingsFlat}`]: { height: '100%', width: '100%', '&.pf-chatbot__settings-form-container': { @@ -467,7 +513,7 @@ const useStyles = makeStyles(theme => ({ display: 'none', }, }, - mcpFullscreenLayout: { + [`& .${classes.mcpFullscreenLayout}`]: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', minHeight: 0, @@ -477,7 +523,7 @@ const useStyles = makeStyles(theme => ({ minWidth: 0, overflow: 'hidden', }, - mcpChatPane: { + [`& .${classes.mcpChatPane}`]: { display: 'flex', flexDirection: 'column', minHeight: 0, @@ -487,7 +533,7 @@ const useStyles = makeStyles(theme => ({ wordBreak: 'break-word', overflowWrap: 'break-word', }, - mcpSettingsPane: { + [`& .${classes.mcpSettingsPane}`]: { width: '100%', minWidth: 0, borderLeft: `1px solid ${theme.palette.divider}`, @@ -498,7 +544,7 @@ const useStyles = makeStyles(theme => ({ minHeight: 0, overflow: 'auto', }, - mcpCollapsedDrawerOrderFix: { + [`& .${classes.mcpCollapsedDrawerOrderFix}`]: { '& .pf-v6-c-drawer.pf-m-panel-left > .pf-v6-c-drawer__main > .pf-v6-c-drawer__content, & .pf-v5-c-drawer.pf-m-panel-left > .pf-v5-c-drawer__main > .pf-v5-c-drawer__content': { order: 'unset', @@ -510,11 +556,7 @@ const useStyles = makeStyles(theme => ({ transition: 'none !important', }, }, - // TODO: These PatternFly drawer overrides are needed because PF Chatbot doesn't - // provide clean APIs for custom expand/collapse icons and positioning. - // Remove once PatternFly supports these features. - // See: https://github.com/patternfly/chatbot/issues/834 - fullscreenChatLayout: { + [`& .${classes.fullscreenChatLayout}`]: { display: 'flex', flexDirection: 'row', flex: 1, @@ -569,7 +611,7 @@ const useStyles = makeStyles(theme => ({ opacity: 1, }, }, - fullscreenMainContent: { + [`& .${classes.fullscreenMainContent}`]: { display: 'flex', flexDirection: 'column', flex: 1, @@ -601,7 +643,6 @@ export const LightspeedChat = ({ models, }: LightspeedChatProps) => { const isMobile = useIsMobile(); - const classes = useStyles(); const { t } = useTranslation(); const navigate = useNavigate(); const configApi = useApi(configApiRef); @@ -1594,14 +1635,7 @@ export const LightspeedChat = ({ ), - [ - isSortSelectOpen, - selectedSort, - onSortSelect, - sortToggle, - t, - classes.sortDropdown, - ], + [isSortSelectOpen, selectedSort, onSortSelect, sortToggle, t], ); const handleAttach = (data: File[], event: ReactDropzoneDropEvent) => { @@ -1795,7 +1829,7 @@ export const LightspeedChat = ({ } return ( - <> + {notebookAlerts.length > 0 && ( - + ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx index c162197f66..36d0993273 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx @@ -22,7 +22,8 @@ import { useRef, } from 'react'; -import { makeStyles } from '@material-ui/core'; +import GlobalStyles from '@mui/material/GlobalStyles'; +import { styled } from '@mui/material/styles'; import { ChatbotDisplayMode, ChatbotWelcomePrompt, @@ -45,23 +46,14 @@ import { ToolCall } from '../types'; import { parseReasoning } from '../utils/reasoningParser'; import { mapToPatternFlyToolCall } from '../utils/toolCallMapper'; -const useStyles = makeStyles(theme => ({ - prompt: { - 'justify-content': 'flex-end', - }, - container: { - maxWidth: 'unset !important', - }, - alert: { - background: 'unset !important', - }, - promptSuggestions: { - '& div.pf-chatbot__prompt-suggestions': { - 'flex-direction': 'column !important', - }, - }, +const DEEP_THINKING_CLASS = 'lightspeed-deep-thinking'; - userMessageText: { +const StyledMessageBox = styled(MessageBox, { + shouldForwardProp: (prop: string) => + !['isNewChat', 'hasPrompts', 'isEmbeddedMode'].includes(prop), +})<{ isNewChat?: boolean; hasPrompts?: boolean; isEmbeddedMode?: boolean }>( + ({ theme, isNewChat, hasPrompts, isEmbeddedMode }) => ({ + maxWidth: 'unset !important', '& div.pf-chatbot__message--user': { '& div.pf-chatbot__message-text': { '& p': { @@ -69,37 +61,32 @@ const useStyles = makeStyles(theme => ({ }, }, }, - }, - deepThinking: { - animation: '$deepThinking 1.6s ease-in-out infinite', - }, + ...(isNewChat + ? { + flex: 'none', + height: 'auto', + overflow: 'visible', + } + : { + flex: 1, + minHeight: 0, + }), + ...(hasPrompts && { + justifyContent: 'flex-end', + }), + ...(hasPrompts && + !isEmbeddedMode && { + '& div.pf-chatbot__prompt-suggestions': { + flexDirection: 'column !important', + }, + }), + }), +); - '@keyframes deepThinking': { - '0%': { - opacity: 0.65, - }, - '50%': { - opacity: 1, - }, - '100%': { - opacity: 0.65, - }, - }, - // Message box fills remaining height and scrolls when there are messages. - messageBoxFlex: { - flex: 1, - minHeight: 0, - }, - // New chat: message box sizes to content so ChatbotContent is the scroll container. - // overflow: visible so full height is included in parent's scrollHeight (no clipping). - messageBoxAutoHeight: { - flex: 'none', - height: 'auto', - overflow: 'visible', - }, -})); +const StyledAlert = styled(Alert)({ + background: 'unset !important', +}); -// Extended message type that includes tool calls interface ExtendedMessageProps extends MessageProps { toolCalls?: ToolCall[]; } @@ -135,7 +122,6 @@ export const LightspeedChatBox = forwardRef( }: LightspeedChatBoxProps, ref: ForwardedRef, ) => { - const classes = useStyles(); const scrollQueued = useRef(false); const containerRef = useRef(null); const { t } = useTranslation(); @@ -189,27 +175,14 @@ export const LightspeedChatBox = forwardRef( // eslint-disable-next-line }, [autoScroll, cmessages, containerRef]); - const messageBoxClasses = `${classes.container} ${classes.userMessageText}`; - const isEmbeddedMode = displayMode === ChatbotDisplayMode.embedded; - const isNewChat = welcomePrompts.length > 0 && messages.length === 0; - const getMessageBoxClassName = () => { - const base = isNewChat - ? `${messageBoxClasses} ${classes.messageBoxAutoHeight}` - : `${messageBoxClasses} ${classes.messageBoxFlex}`; - if (!welcomePrompts.length) { - return base; - } - const withPrompt = `${base} ${classes.prompt}`; - if (isEmbeddedMode) { - return withPrompt; - } - return `${withPrompt} ${classes.promptSuggestions}`; - }; + const isEmbeddedMode = displayMode === ChatbotDisplayMode.embedded; return ( - 0} + isEmbeddedMode={isEmbeddedMode} announcement={announcement} ref={containerRef} onScrollToTopClick={scrollToTop} @@ -220,16 +193,23 @@ export const LightspeedChatBox = forwardRef( jumpButtonTopTooltipProps={{ content: t('tooltip.backToTop') }} >
- + + {topicRestrictionEnabled ? t('disclaimer.withValidation') : t('disclaimer.withoutValidation')} - +
{welcomePrompts.length ? ( @@ -250,13 +230,11 @@ export const LightspeedChatBox = forwardRef( const messageContent = message.content as string; const parsedReasoning = parseReasoning(messageContent || ''); - // Map first tool call to PatternFly's toolCall prop const firstToolCall = message.toolCalls?.[0]; const toolCallProp = firstToolCall ? mapToPatternFlyToolCall(firstToolCall, t, message.role) : undefined; - // Handle additional tool calls (if any) via extraContent const additionalToolCalls = message.toolCalls?.slice(1); const extraContentParts: { @@ -278,7 +256,7 @@ export const LightspeedChatBox = forwardRef( id: `deep-thinking-${index}`, style: { whiteSpace: 'pre-line' }, className: parsedReasoning.isReasoningInProgress - ? classes.deepThinking + ? DEEP_THINKING_CLASS : undefined, }, toggleContent: t('reasoning.thinking'), @@ -290,7 +268,6 @@ export const LightspeedChatBox = forwardRef( const allToolCalls: React.ReactNode[] = []; - // Add first tool call if it exists if (toolCallProp && firstToolCall) { allToolCalls.push(
0) { additionalToolCalls.forEach(tc => { const tcProps = mapToPatternFlyToolCall(tc, t, message.role); @@ -317,7 +293,6 @@ export const LightspeedChatBox = forwardRef( }); } - // Show Thinking first, then tool calls if (deepThinking || allToolCalls.length > 0) { extraContentParts.beforeMainContent = ( <> @@ -351,7 +326,7 @@ export const LightspeedChatBox = forwardRef( /> ); })} - + ); }, ); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBoxHeader.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBoxHeader.tsx index 9ef30eff01..24f0609257 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBoxHeader.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBoxHeader.tsx @@ -16,10 +16,10 @@ import { Ref, useState } from 'react'; -import { createStyles, makeStyles } from '@material-ui/core'; import ToggleOffOutlinedIcon from '@mui/icons-material/ToggleOffOutlined'; import ToggleOnOutlinedIcon from '@mui/icons-material/ToggleOnOutlined'; import Divider from '@mui/material/Divider'; +import { styled } from '@mui/material/styles'; import { ChatbotDisplayMode, ChatbotHeaderActions, @@ -58,27 +58,33 @@ type LightspeedChatBoxHeaderProps = { setDisplayMode: (mode: ChatbotDisplayMode) => void; }; -const useStyles = makeStyles(theme => - createStyles({ - dropdown: { - '& ul, & li': { - padding: 0, - margin: 0, - }, - }, - header: { - backgroundColor: theme.palette.action.disabled, - }, - optionsToggle: { - '& svg': { - transform: 'none !important', - }, - }, - groupTitle: { - fontWeight: 'bold', - }, +const dropdownOverrideStyles = { + '& ul, & li': { + padding: 0, + margin: 0, + }, +} as const; + +const StyledDropdown = styled(Dropdown)(dropdownOverrideStyles); + +const StyledOptionsDropdown = styled(ChatbotHeaderOptionsDropdown)({ + ...dropdownOverrideStyles, + '& .pf-v6-c-menu-toggle svg, & .pf-v5-c-menu-toggle svg': { + transform: 'none !important', + }, +}); + +const StyledMenuToggle = styled(MenuToggle, { + shouldForwardProp: (prop: string) => prop !== 'isDisabledStyle', +})<{ isDisabledStyle?: boolean }>(({ theme, isDisabledStyle }) => ({ + ...(isDisabledStyle && { + backgroundColor: theme.palette.action.disabled, }), -); +})); + +const StyledDropdownGroup = styled(DropdownGroup)({ + fontWeight: 'bold', +}); export const LightspeedChatBoxHeader = ({ selectedModel, @@ -96,8 +102,6 @@ export const LightspeedChatBoxHeader = ({ const [isOptionsMenuOpen, setIsOptionsMenuOpen] = useState(false); const { t } = useTranslation(); - const styles = useStyles(); - const maxLabelLength = Math.max( ...models.map(m => m.label.length), selectedModel.length, @@ -106,8 +110,8 @@ export const LightspeedChatBoxHeader = ({ const toggleMinWidth = `${maxLabelLength + 4}ch`; const toggle = (toggleRef: Ref) => ( - {selectedModel} - + ); const handlePinningChatsToggle = (state: boolean) => { @@ -143,8 +147,7 @@ export const LightspeedChatBoxHeader = ({ return ( {!hideModelSelector && ( - { handleSelectedModel(value as string); @@ -160,7 +163,7 @@ export const LightspeedChatBoxHeader = ({ > {models.map(model => ( - + {model.label} - + ))} - + )} - )} - + ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatContainer.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatContainer.tsx index aeece211b3..2086b070f4 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatContainer.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatContainer.tsx @@ -19,12 +19,9 @@ import { useAsync } from 'react-use'; import { identityApiRef, useApi } from '@backstage/core-plugin-api'; -import { Button } from '@material-ui/core'; -import { - StylesProvider as StylesProviderV4, - useTheme, -} from '@material-ui/core/styles'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import Button from '@mui/material/Button'; +import { useTheme } from '@mui/material/styles'; import { StylesProvider } from '@mui/styles'; import { QueryClientProvider } from '@tanstack/react-query'; @@ -32,10 +29,7 @@ import { useAllModels } from '../hooks/useAllModels'; import { useLightspeedViewPermission } from '../hooks/useLightspeedViewPermission'; import { useTopicRestrictionStatus } from '../hooks/useQuestionValidation'; import { useTranslation } from '../hooks/useTranslation'; -import { - generateClassName, - generateClassNameV4, -} from '../utils/generateClassName'; +import { generateClassName } from '../utils/generateClassName'; import queryClient from '../utils/queryClient'; import FileAttachmentContextProvider from './AttachmentContext'; import { LightspeedChat } from './LightSpeedChat'; @@ -55,7 +49,7 @@ const LAST_SELECTED_MODEL_KEY = 'lastSelectedModel'; */ const LightspeedChatContainerInner = () => { const { - palette: { type }, + palette: { mode: type }, } = useTheme(); const { t } = useTranslation(); @@ -222,11 +216,9 @@ const LightspeedChatContainerInner = () => { export const LightspeedChatContainer = () => { return ( - - - - - + + + ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatModelsState.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatModelsState.tsx index 6e015a03b3..c724417c39 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatModelsState.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatModelsState.tsx @@ -14,17 +14,15 @@ * limitations under the License. */ -import { - Box, - Button, - CircularProgress, - Link, - Typography, -} from '@material-ui/core'; -import { createStyles, makeStyles } from '@material-ui/core/styles'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import Link from '@mui/material/Link'; +import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; import { useTranslation } from '../hooks/useTranslation'; @@ -33,77 +31,46 @@ const LLAMA_STACK_CONFIGURE_DOCS_URL = const LIGHTSPEED_BACKEND_README_URL = 'https://github.com/redhat-developer/rhdh-plugins/blob/main/workspaces/lightspeed/plugins/lightspeed-backend/README.md'; -const useStyles = makeStyles(theme => - createStyles({ - root: { - display: 'flex', - flexDirection: 'column', - boxSizing: 'border-box', - width: '100%', - maxWidth: '100%', - minWidth: 0, - minHeight: '100%', - height: '100%', - flex: '1 1 auto', - alignItems: 'center', - justifyContent: 'center', - padding: theme.spacing(4, 2), - backgroundColor: theme.palette.background.default, - }, - panel: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - textAlign: 'center', - width: '100%', - maxWidth: 440, - gap: theme.spacing(2), - }, - emptyStateIcon: { - fontSize: 64, - color: theme.palette.text.secondary, - }, - errorIcon: { - fontSize: 64, - color: theme.palette.warning.main, - }, - description: { - lineHeight: 1.5, - color: theme.palette.text.secondary, - }, - actions: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: theme.spacing(1.5), - marginTop: theme.spacing(1), - }, - backendLink: { - display: 'inline-flex', - alignItems: 'center', - gap: theme.spacing(0.5), - fontSize: theme.typography.body1.fontSize, - fontWeight: 500, - }, - }), -); +const Root = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + width: '100%', + maxWidth: '100%', + minWidth: 0, + minHeight: '100%', + height: '100%', + flex: '1 1 auto', + alignItems: 'center', + justifyContent: 'center', + padding: theme.spacing(4, 2), + backgroundColor: theme.palette.background.default, +})); + +const Panel = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center', + width: '100%', + maxWidth: 440, + gap: theme.spacing(2), +})); /** * Shown while the models list is loading for an authorized user. */ export const LightspeedChatModelsLoading = () => { - const classes = useStyles(); const { t } = useTranslation(); return ( -
-
+ ); }; @@ -111,18 +78,13 @@ export const LightspeedChatModelsLoading = () => { * Shown when LCORE / Llama Stack is up but no LLM models are registered. */ export const LcoreNotConfiguredEmptyState = () => { - const classes = useStyles(); const { t } = useTranslation(); return ( -
- + + @@ -136,11 +98,19 @@ export const LcoreNotConfiguredEmptyState = () => { {t('lcore.notConfigured.description')} - + ({ + display: 'inline-flex', + alignItems: 'center', + gap: theme.spacing(0.5), + fontSize: theme.typography.body1.fontSize, + fontWeight: 500, + })} component="a" color="primary" href={LIGHTSPEED_BACKEND_README_URL} @@ -163,8 +139,8 @@ export const LcoreNotConfiguredEmptyState = () => { - -
+ + ); }; @@ -173,23 +149,21 @@ type ModelsLoadErrorEmptyStateProps = { }; /** - * Shown when the models API fails (distinct from “no models configured”). + * Shown when the models API fails (distinct from "no models configured"). */ export const ModelsLoadErrorEmptyState = ({ onRetry, }: ModelsLoadErrorEmptyStateProps) => { - const classes = useStyles(); const { t } = useTranslation(); return ( -
- + @@ -203,16 +177,24 @@ export const ModelsLoadErrorEmptyState = ({ {t('lcore.loadError.description')} - + - -
+ + ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedDrawerProvider.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedDrawerProvider.tsx index 560c490f54..0a8e670144 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedDrawerProvider.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedDrawerProvider.tsx @@ -16,7 +16,7 @@ import { PropsWithChildren } from 'react'; -import { makeStyles } from '@mui/styles'; +import { styled } from '@mui/material/styles'; import { ChatbotModal } from '@patternfly/chatbot'; import { DOCKED_CONTENT_OFFSET } from '../const'; @@ -24,18 +24,16 @@ import { useLightspeedProviderState } from '../hooks/useLightspeedProviderState' import { LightspeedChatContainer } from './LightspeedChatContainer'; import { LightspeedDrawerContext } from './LightspeedDrawerContext'; -const useStyles = makeStyles(theme => ({ - chatbotModal: { - boxShadow: - '0 14px 20px -7px rgba(0, 0, 0, 0.22), 0 32px 50px 6px rgba(0, 0, 0, 0.16), 0 12px 60px 12px rgba(0, 0, 0, 0.14) !important', - bottom: `calc(${theme?.spacing?.(2) ?? '16px'} + 5em)`, - right: `calc(${theme?.spacing?.(2) ?? '16px'} + 1.5em)`, - maxWidth: 'min(30rem, calc(100vw - 32px)) !important', - overflowX: 'hidden' as const, - transition: 'margin-right 0.3s ease', - 'body.docked-drawer-open &': { - marginRight: DOCKED_CONTENT_OFFSET, - }, +const StyledChatbotModal = styled(ChatbotModal)(({ theme }) => ({ + boxShadow: + '0 14px 20px -7px rgba(0, 0, 0, 0.22), 0 32px 50px 6px rgba(0, 0, 0, 0.16), 0 12px 60px 12px rgba(0, 0, 0, 0.14) !important', + bottom: `calc(${theme.spacing(2)} + 5em)`, + right: `calc(${theme.spacing(2)} + 1.5em)`, + maxWidth: 'min(30rem, calc(100vw - 32px)) !important', + overflowX: 'hidden', + transition: 'margin-right 0.3s ease', + 'body.docked-drawer-open &': { + marginRight: DOCKED_CONTENT_OFFSET, }, })); @@ -43,7 +41,6 @@ const useStyles = makeStyles(theme => ({ * @public */ export const LightspeedDrawerProvider = ({ children }: PropsWithChildren) => { - const classes = useStyles(); const { contextValue, shouldRenderOverlayModal, closeChatbot } = useLightspeedProviderState(); @@ -51,17 +48,16 @@ export const LightspeedDrawerProvider = ({ children }: PropsWithChildren) => { {children} {shouldRenderOverlayModal && ( - closeChatbot()} ouiaId="LightspeedChatbotModal" aria-labelledby="lightspeed-chatpopup-modal" - className={classes.chatbotModal} > - + )} ); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedPage.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedPage.tsx index aec6f2cde6..3de5adfe58 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedPage.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedPage.tsx @@ -16,25 +16,20 @@ import { Content, Header, Page } from '@backstage/core-components'; -import { createStyles, makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; import { useTranslation } from '../hooks/useTranslation'; import { LightspeedChatContainer } from './LightspeedChatContainer'; -const useStyles = makeStyles(() => - createStyles({ - container: { - padding: '0px', - }, - }), -); +const NoPaddingContent = styled(Content)({ + padding: 0, +}); /** * Lightspeed Page - Routable fullscreen/embedded mode * @public */ export const LightspeedPage = () => { - const classes = useStyles(); const { t } = useTranslation(); return ( @@ -44,9 +39,9 @@ export const LightspeedPage = () => { style={{ display: 'none' }} pageTitleOverride={t('page.title')} /> - + - + ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx index 91d65d7987..21714beb03 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx @@ -19,12 +19,13 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { configApiRef, fetchApiRef, useApi } from '@backstage/core-plugin-api'; import { usePermission } from '@backstage/plugin-permission-react'; -import { makeStyles } from '@material-ui/core'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined'; +import GlobalStyles from '@mui/material/GlobalStyles'; import IconButton from '@mui/material/IconButton'; import InputAdornment from '@mui/material/InputAdornment'; +import { styled } from '@mui/material/styles'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { @@ -74,23 +75,56 @@ type TokenValidationState = 'idle' | 'validating' | 'success' | 'error'; const SAVED_TOKEN_MASK = '********************'; -const useStyles = makeStyles(theme => ({ - '@global': { - '.pf-v6-c-backdrop': { - zIndex: '1400 !important', - }, - '.pf-v5-c-backdrop': { - zIndex: '1400 !important', - }, - }, - root: { +const PREFIX = 'McpServersSettings'; +const classes = { + root: `${PREFIX}-root`, + headerRow: `${PREFIX}-headerRow`, + selectedCount: `${PREFIX}-selectedCount`, + title: `${PREFIX}-title`, + closeButton: `${PREFIX}-closeButton`, + nameHeaderButton: `${PREFIX}-nameHeaderButton`, + nameHeaderText: `${PREFIX}-nameHeaderText`, + nameCell: `${PREFIX}-nameCell`, + statusHeader: `${PREFIX}-statusHeader`, + statusColumnCell: `${PREFIX}-statusColumnCell`, + rowName: `${PREFIX}-rowName`, + nameValue: `${PREFIX}-nameValue`, + statusCell: `${PREFIX}-statusCell`, + statusValue: `${PREFIX}-statusValue`, + statusOk: `${PREFIX}-statusOk`, + statusToken: `${PREFIX}-statusToken`, + statusWarn: `${PREFIX}-statusWarn`, + statusDisabled: `${PREFIX}-statusDisabled`, + actionButton: `${PREFIX}-actionButton`, + modalDescription: `${PREFIX}-modalDescription`, + modalContent: `${PREFIX}-modalContent`, + modalCustomCloseButton: `${PREFIX}-modalCustomCloseButton`, + modalHeading: `${PREFIX}-modalHeading`, + tokenRow: `${PREFIX}-tokenRow`, + tokenClearButton: `${PREFIX}-tokenClearButton`, + tokenHelper: `${PREFIX}-tokenHelper`, + tokenInput: `${PREFIX}-tokenInput`, + tokenInputSuccess: `${PREFIX}-tokenInputSuccess`, + tokenInputError: `${PREFIX}-tokenInputError`, + modalActions: `${PREFIX}-modalActions`, + modalActionButton: `${PREFIX}-modalActionButton`, + modalCancelButton: `${PREFIX}-modalCancelButton`, + forgetTokenButton: `${PREFIX}-forgetTokenButton`, + configureModal: `${PREFIX}-configureModal`, + toggleCell: `${PREFIX}-toggleCell`, + table: `${PREFIX}-table`, + alert: `${PREFIX}-alert`, +}; + +const StyledRoot = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { padding: 0, height: '100%', minHeight: '100%', width: '100%', overflow: 'auto', }, - headerRow: { + [`& .${classes.headerRow}`]: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', @@ -99,20 +133,20 @@ const useStyles = makeStyles(theme => ({ marginLeft: theme.spacing(3), marginRight: theme.spacing(2), }, - selectedCount: { + [`& .${classes.selectedCount}`]: { color: theme.palette.text.secondary, marginTop: theme.spacing(0.5), fontSize: '0.75rem', }, - title: { + [`& .${classes.title}`]: { fontSize: '1.125rem', }, - closeButton: { + [`& .${classes.closeButton}`]: { marginTop: -theme.spacing(1), marginRight: -theme.spacing(1), color: theme.palette.text.primary, }, - nameHeaderButton: { + [`& .${classes.nameHeaderButton}`]: { paddingLeft: 0, paddingTop: 0, paddingBottom: 0, @@ -126,72 +160,72 @@ const useStyles = makeStyles(theme => ({ display: 'inline-flex', alignItems: 'center', }, - nameHeaderText: { + [`& .${classes.nameHeaderText}`]: { paddingLeft: '7px', fontSize: '0.75rem', lineHeight: '1.25rem', fontWeight: 600, }, - nameCell: { + [`& .${classes.nameCell}`]: { paddingLeft: '8px !important', }, - statusHeader: { + [`& .${classes.statusHeader}`]: { paddingLeft: '0 !important', }, - statusColumnCell: { + [`& .${classes.statusColumnCell}`]: { paddingLeft: '0 !important', }, - rowName: { + [`& .${classes.rowName}`]: { fontSize: '1rem', fontWeight: 500, whiteSpace: 'nowrap', }, - nameValue: { + [`& .${classes.nameValue}`]: { fontSize: '0.875rem', fontWeight: 500, }, - statusCell: { + [`& .${classes.statusCell}`]: { display: 'flex', alignItems: 'center', gap: theme.spacing(1), whiteSpace: 'nowrap', }, - statusValue: { + [`& .${classes.statusValue}`]: { fontSize: '0.875rem', }, - statusOk: { + [`& .${classes.statusOk}`]: { color: '#147878', }, - statusToken: { + [`& .${classes.statusToken}`]: { color: '#147878', }, - statusWarn: { + [`& .${classes.statusWarn}`]: { color: '#B1380B', }, - statusDisabled: { + [`& .${classes.statusDisabled}`]: { color: theme.palette.text.secondary, }, - actionButton: { + [`& .${classes.actionButton}`]: { color: theme.palette.text.secondary, }, - modalDescription: { + [`& .${classes.modalDescription}`]: { color: theme.palette.text.secondary, fontSize: '0.875rem', marginTop: theme.spacing(2), marginBottom: theme.spacing(2), }, - modalContent: { + [`& .${classes.modalContent}`]: { position: 'relative', padding: theme.spacing(3, 0, 3, 3), marginRight: theme.spacing(3), }, - modalCustomCloseButton: { + [`& .${classes.modalCustomCloseButton}`]: { position: 'absolute', top: theme.spacing(2), right: theme.spacing(-0.5), color: theme.palette.text.primary, }, - modalHeading: { + [`& .${classes.modalHeading}`]: { display: 'flex', alignItems: 'center', gap: theme.spacing(0.75), @@ -205,10 +239,10 @@ const useStyles = makeStyles(theme => ({ fontWeight: 500, }, }, - tokenRow: { + [`& .${classes.tokenRow}`]: { position: 'relative', }, - tokenClearButton: { + [`& .${classes.tokenClearButton}`]: { position: 'absolute', right: theme.spacing(1), top: '50%', @@ -216,12 +250,12 @@ const useStyles = makeStyles(theme => ({ zIndex: 1, color: theme.palette.action.active, }, - tokenHelper: { + [`& .${classes.tokenHelper}`]: { color: theme.palette.text.secondary, fontSize: '0.75rem', marginTop: theme.spacing(0.5), }, - tokenInput: { + [`& .${classes.tokenInput}`]: { marginTop: '1rem !important', '& .MuiOutlinedInput-root': { height: '3.5rem', @@ -233,7 +267,7 @@ const useStyles = makeStyles(theme => ({ fontSize: '0.875rem', }, }, - tokenInputSuccess: { + [`& .${classes.tokenInputSuccess}`]: { '& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline': { borderColor: '#3E8635', borderWidth: 1, @@ -248,7 +282,7 @@ const useStyles = makeStyles(theme => ({ color: '#3E8635', }, }, - tokenInputError: { + [`& .${classes.tokenInputError}`]: { '& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline': { borderColor: '#C9190B', borderWidth: 1, @@ -263,18 +297,18 @@ const useStyles = makeStyles(theme => ({ color: '#C9190B', }, }, - modalActions: { + [`& .${classes.modalActions}`]: { marginTop: theme.spacing(3), display: 'flex', gap: theme.spacing(1), }, - modalActionButton: { + [`& .${classes.modalActionButton}`]: { fontSize: '1rem', }, - modalCancelButton: { + [`& .${classes.modalCancelButton}`]: { fontSize: '1rem', }, - forgetTokenButton: { + [`& .${classes.forgetTokenButton}`]: { fontSize: '1rem', border: '1px solid #B1380B', borderRadius: '1.25rem', @@ -288,7 +322,7 @@ const useStyles = makeStyles(theme => ({ backgroundColor: 'rgba(201, 25, 11, 0.08)', }, }, - configureModal: { + [`& .${classes.configureModal}`]: { '& .pf-v6-c-modal-box': { width: '608px', maxWidth: '608px', @@ -311,10 +345,10 @@ const useStyles = makeStyles(theme => ({ fontSize: '1.25rem !important', }, }, - toggleCell: { + [`& .${classes.toggleCell}`]: { paddingRight: '0 !important', }, - table: { + [`& .${classes.table}`]: { width: '100%', '& th': { borderBottom: 0, @@ -331,7 +365,7 @@ const useStyles = makeStyles(theme => ({ verticalAlign: 'middle', }, }, - alert: { + [`& .${classes.alert}`]: { marginLeft: theme.spacing(3), marginRight: theme.spacing(3), marginBottom: theme.spacing(2), @@ -408,7 +442,6 @@ export const McpServersSettings = ({ onClose, backgroundColor, }: McpServersSettingsProps) => { - const classes = useStyles(); const { t } = useTranslation(); const configApi = useApi(configApiRef); const fetchApi = useApi(fetchApiRef); @@ -821,10 +854,16 @@ export const McpServersSettings = ({ }; return ( -
+
@@ -1114,6 +1153,6 @@ export const McpServersSettings = ({ </div> </div> </Modal> - </div> + </StyledRoot> ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/MessageBarModelSelector.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/MessageBarModelSelector.tsx index 00f96c26ed..4dcd76c786 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/MessageBarModelSelector.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/MessageBarModelSelector.tsx @@ -16,7 +16,7 @@ import { Ref, useState } from 'react'; -import { makeStyles } from '@material-ui/core'; +import { styled } from '@mui/material/styles'; import { Dropdown, DropdownItem, @@ -35,35 +35,34 @@ type MessageBarModelSelectorProps = { disabled?: boolean; }; -const useStyles = makeStyles(theme => ({ - selectorToggle: { - display: 'flex', - alignItems: 'center', - gap: 4, - color: theme.palette.text.secondary, - fontSize: 14, - fontWeight: 500, - cursor: 'pointer', - padding: '4px 8px', - borderRadius: 8, - border: 'none', - background: 'transparent', - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&:disabled': { - cursor: 'not-allowed', - opacity: 0.5, - }, +const StyledMenuToggle = styled(MenuToggle)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: 4, + color: theme.palette.text.secondary, + fontSize: 14, + fontWeight: 500, + cursor: 'pointer', + padding: '4px 8px', + borderRadius: 8, + border: 'none', + background: 'transparent', + '&:hover': { + backgroundColor: theme.palette.action.hover, }, - dropdown: { - '& ul, & li': { - padding: 0, - margin: 0, - }, + '&:disabled': { + cursor: 'not-allowed', + opacity: 0.5, }, })); +const StyledDropdown = styled(Dropdown)({ + '& ul, & li': { + padding: 0, + margin: 0, + }, +}); + export const MessageBarModelSelector = ({ selectedModel, models, @@ -71,30 +70,27 @@ export const MessageBarModelSelector = ({ disabled = false, }: MessageBarModelSelectorProps) => { const [isOpen, setIsOpen] = useState(false); - const classes = useStyles(); const { t } = useTranslation(); const selectedModelLabel = models.find(m => m.value === selectedModel)?.label ?? selectedModel; const toggle = (toggleRef: Ref<MenuToggleElement>) => ( - <MenuToggle + <StyledMenuToggle ref={toggleRef} onClick={() => setIsOpen(!isOpen)} isExpanded={isOpen} isDisabled={disabled} variant="plain" - className={classes.selectorToggle} aria-label={t('aria.chatbotSelector')} > {selectedModelLabel} <AngleDownIcon /> - </MenuToggle> + </StyledMenuToggle> ); return ( - <Dropdown - className={classes.dropdown} + <StyledDropdown isOpen={isOpen} onSelect={(_e, value) => { onSelect(value as string); @@ -119,6 +115,6 @@ export const MessageBarModelSelector = ({ </DropdownItem> ))} </DropdownList> - </Dropdown> + </StyledDropdown> ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/PermissionRequiredState.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/PermissionRequiredState.tsx index 38ad489f9e..163b09fbff 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/PermissionRequiredState.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/PermissionRequiredState.tsx @@ -18,59 +18,55 @@ import { Fragment } from 'react'; import { EmptyState } from '@backstage/core-components'; -import { createStyles, makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; import { useTranslation } from '../hooks/useTranslation'; import { PermissionRequiredIcon } from './PermissionRequiredIcon'; import { Trans } from './Trans'; -const useStyles = makeStyles(theme => - createStyles({ - root: { +const Root = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + minHeight: '100%', + flex: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: theme.palette.background.default, + containerType: 'inline-size', + '& [class*="BackstageEmptyState-root"]': { + alignItems: 'center', + padding: theme.spacing(4), + }, + '& [class*="MuiTypography-h5"]': { + fontSize: 'clamp(1.875rem, 3.75cqi, 3.125rem)', + fontWeight: 400, + }, + '& [class*="MuiTypography-body1"]': { + fontSize: '1em', + color: theme.palette.text.secondary, + '& b': { + fontWeight: 500, + color: theme.palette.text.primary, + }, + }, + '@container (max-width: 899px)': { + '& [class*="BackstageEmptyState-root"]': { + textAlign: 'center', + }, + '& [class*="MuiGrid-grid-md-6"]': { + maxWidth: '100%', + flexBasis: '100%', + }, + '& [class*="BackstageEmptyState-imageContainer"]': { + order: -1, display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - minHeight: '100%', - flex: 1, - alignItems: 'center', justifyContent: 'center', - backgroundColor: theme.palette.background.default, - containerType: 'inline-size', - '& [class*="BackstageEmptyState-root"]': { - alignItems: 'center', - padding: theme.spacing(4), - }, - '& [class*="MuiTypography-h5"]': { - fontSize: 'clamp(1.875rem, 3.75cqi, 3.125rem)', - fontWeight: 400, - }, - '& [class*="MuiTypography-body1"]': { - fontSize: '1em', - color: theme.palette.text.secondary, - '& b': { - fontWeight: 500, - color: theme.palette.text.primary, - }, - }, - '@container (max-width: 899px)': { - '& [class*="BackstageEmptyState-root"]': { - textAlign: 'center', - }, - '& [class*="MuiGrid-grid-md-6"]': { - maxWidth: '100%', - flexBasis: '100%', - }, - '& [class*="BackstageEmptyState-imageContainer"]': { - order: -1, - display: 'flex', - justifyContent: 'center', - marginBottom: theme.spacing(-4), - }, - }, + marginBottom: theme.spacing(-4), }, - }), -); + }, +})); interface PermissionRequiredStateProps { subject: string; @@ -83,7 +79,6 @@ const PermissionRequiredState = ({ permissions, action, }: PermissionRequiredStateProps) => { - const classes = useStyles(); const { t } = useTranslation(); const permissionsList = ( @@ -98,7 +93,7 @@ const PermissionRequiredState = ({ ); return ( - <div className={classes.root}> + <Root> <EmptyState title={t('permission.required.title')} description={ @@ -113,7 +108,7 @@ const PermissionRequiredState = ({ missing={{ customImage: <PermissionRequiredIcon /> }} action={action} /> - </div> + </Root> ); }; export default PermissionRequiredState; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/RenameConversationModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/RenameConversationModal.tsx index c77be838d2..360fcf3ba7 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/RenameConversationModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/RenameConversationModal.tsx @@ -16,7 +16,6 @@ import { useEffect, useState } from 'react'; -import { TextField } from '@material-ui/core'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import CloseIcon from '@mui/icons-material/Close'; import Alert from '@mui/material/Alert'; @@ -27,6 +26,7 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { useConversations } from '../hooks/useConversations'; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/Router.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/Router.tsx index ddc2b957be..bc57f21744 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/Router.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/Router.tsx @@ -18,13 +18,9 @@ import { Route, Routes } from 'react-router-dom'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { StylesProvider as StylesProviderV4 } from '@material-ui/core/styles'; import { StylesProvider } from '@mui/styles'; -import { - generateClassName, - generateClassNameV4, -} from '../utils/generateClassName'; +import { generateClassName } from '../utils/generateClassName'; import { LightspeedPage } from './LightspeedPage'; /** @@ -37,24 +33,19 @@ export const Router = () => { return ( <StylesProvider generateClassName={generateClassName}> - <StylesProviderV4 generateClassName={generateClassNameV4}> - <Routes> - <Route path="/" element={<LightspeedPage />} /> - <Route - path="/conversation/:conversationId" - element={<LightspeedPage />} - /> - {notebooksEnabled && ( - <> - <Route path="/notebooks" element={<LightspeedPage />} /> - <Route - path="/notebooks/:notebookId" - element={<LightspeedPage />} - /> - </> - )} - </Routes> - </StylesProviderV4> + <Routes> + <Route path="/" element={<LightspeedPage />} /> + <Route + path="/conversation/:conversationId" + element={<LightspeedPage />} + /> + {notebooksEnabled && ( + <> + <Route path="/notebooks" element={<LightspeedPage />} /> + <Route path="/notebooks/:notebookId" element={<LightspeedPage />} /> + </> + )} + </Routes> </StylesProvider> ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/ToolCallContent.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/ToolCallContent.tsx index 8effe8cbdb..c252fcf4ee 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/ToolCallContent.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/ToolCallContent.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core'; +import { styled } from '@mui/material/styles'; import { Message } from '@patternfly/chatbot'; import { Content, @@ -38,14 +38,12 @@ interface ToolCallContentProps { role?: 'user' | 'bot'; } -const useStyles = makeStyles(() => ({ - codeBlock: { - '& .pf-chatbot__message-code-block': { - border: '1px solid var(--pf-t--global--border--color--default)', - borderRadius: 'var(--pf-t--global--border--radius--small)', - }, +const CodeBlockWrapper = styled(FlexItem)({ + '& .pf-chatbot__message-code-block': { + border: '1px solid var(--pf-t--global--border--color--default)', + borderRadius: 'var(--pf-t--global--border--radius--small)', }, -})); +}); /** * Lightweight component for rendering tool call expandable content. @@ -55,7 +53,6 @@ export const ToolCallContent = ({ toolCall, role = 'bot', }: ToolCallContentProps) => { - const classes = useStyles(); const { t } = useTranslation(); const formatExecutionTime = (seconds?: number): string => { @@ -236,7 +233,7 @@ export const ToolCallContent = ({ direction={{ default: 'column' }} spaceItems={{ default: 'spaceItemsXs' }} > - <FlexItem className={classes.codeBlock}> + <CodeBlockWrapper> <Message content={formatToolResponseForMarkdown( toolCall.response, @@ -249,7 +246,7 @@ export const ToolCallContent = ({ }, }} /> - </FlexItem> + </CodeBlockWrapper> </Flex> </DescriptionListDescription> </DescriptionListGroup> diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedPage.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedPage.test.tsx index 676acd6749..005e8e7041 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedPage.test.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedPage.test.tsx @@ -58,15 +58,6 @@ const mockLightspeedApi = { isTopicRestrictionEnabled: jest.fn().mockResolvedValue(false), }; -jest.mock('@mui/material', () => ({ - ...jest.requireActual('@mui/material'), - makeStyles: () => () => { - return { - container: 'container', - }; - }, -})); - jest.mock('../../hooks/useAllModels', () => ({ useAllModels: jest.fn(), })); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx index 7b85fbb594..71676cd7d6 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx @@ -17,7 +17,6 @@ import { useEffect, useState } from 'react'; import { FileRejection } from 'react-dropzone'; -import { makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@mui/icons-material/Close'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; @@ -27,6 +26,7 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; +import { styled } from '@mui/material/styles'; import Typography from '@mui/material/Typography'; import { MultipleFileUpload, @@ -43,69 +43,22 @@ import { } from '../../utils/notebook-upload-utils'; import { FileListItem } from './FileListItem'; -const useStyles = makeStyles(theme => ({ - dialogPaper: { - borderRadius: 24, - maxWidth: 578, +const StyledDropzone = styled('div')({ + '& .pf-v6-c-multiple-file-upload__main': { + borderColor: 'var(--pf-t--global--border--color--brand--default)', + transition: 'background-color 0.2s ease', + cursor: 'pointer', }, - dialogTitle: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '24px 24px 16px', - }, - titleText: { - fontWeight: 500, - fontSize: '1.25rem', - lineHeight: '1.625rem', - letterSpacing: '-0.25px', - }, - closeButton: { - color: theme.palette.text.primary, - }, - dialogContent: { - padding: '0 24px 24px', - }, - errorAlert: { - marginBottom: theme.spacing(2), - }, - dropzone: { - '& .pf-v6-c-multiple-file-upload__main': { - borderColor: 'var(--pf-t--global--border--color--brand--default)', - transition: 'background-color 0.2s ease', - cursor: 'pointer', - }, - '& .pf-v6-c-multiple-file-upload__main:hover': { - backgroundColor: - 'color-mix(in srgb, var(--pf-t--global--color--brand--default) 10%, transparent)', - }, - }, - fileListContainer: { - marginTop: theme.spacing(2), - maxHeight: 200, - overflowY: 'auto', - }, - fileListHeader: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: theme.spacing(1), - }, - fileCount: { - fontSize: '0.875rem', - color: theme.palette.text.secondary, - }, - dialogActions: { - padding: '16px 24px', - justifyContent: 'flex-end', - gap: theme.spacing(1), - }, - addButton: { - textTransform: 'none', - }, - cancelButton: { - textTransform: 'none', + '& .pf-v6-c-multiple-file-upload__main:hover': { + backgroundColor: + 'color-mix(in srgb, var(--pf-t--global--color--brand--default) 10%, transparent)', }, +}); + +const FileListContainer = styled(Box)(({ theme }) => ({ + marginTop: theme.spacing(2), + maxHeight: 200, + overflowY: 'auto', })); type AddDocumentModalProps = { @@ -135,7 +88,6 @@ export const AddDocumentModal = ({ filesToAdd, onFilesAdded, }: AddDocumentModalProps) => { - const classes = useStyles(); const { t } = useTranslation(); const uploadMutation = useUploadDocument(); const [validationErrors, setValidationErrors] = useState<string[]>([]); @@ -230,11 +182,26 @@ export const AddDocumentModal = ({ onClose={handleClose} aria-labelledby="add-document-modal-title" PaperProps={{ - className: classes.dialogPaper, + sx: { borderRadius: '24px', maxWidth: 578 }, }} > - <DialogTitle className={classes.dialogTitle}> - <Typography component="h2" className={classes.titleText}> + <DialogTitle + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '24px 24px 16px', + }} + > + <Typography + component="h2" + sx={{ + fontWeight: 500, + fontSize: '1.25rem', + lineHeight: '1.625rem', + letterSpacing: '-0.25px', + }} + > {t('notebook.upload.modal.title')} {selectedFiles.length > 0 && ` (${selectedFiles.length}/${NOTEBOOK_MAX_FILES - existingDocumentNames.length})`} @@ -242,16 +209,16 @@ export const AddDocumentModal = ({ <IconButton aria-label={t('common.close')} onClick={handleClose} - className={classes.closeButton} + sx={{ color: 'text.primary' }} size="small" > <CloseIcon /> </IconButton> </DialogTitle> - <DialogContent className={classes.dialogContent}> + <DialogContent sx={{ padding: '0 24px 24px' }}> {validationErrors.length > 0 && ( - <Alert severity="error" className={classes.errorAlert}> + <Alert severity="error" sx={{ mb: 2 }}> {validationErrors .map(errorKey => { const message = (t as Function)(errorKey) as string; @@ -264,34 +231,44 @@ export const AddDocumentModal = ({ )} {hasUploadsInProgress && ( - <Alert severity="info" className={classes.errorAlert}> + <Alert severity="info" sx={{ mb: 2 }}> {t('notebook.view.documents.uploadsInProgress')} </Alert> )} {remainingSlots > 0 && ( - <MultipleFileUpload - className={classes.dropzone} - dropzoneProps={{ - accept: getNotebookAcceptedFileTypes(), - onDropRejected: handleDropRejected, - }} - onFileDrop={handleFileDrop} - > - <MultipleFileUploadMain - titleIcon={<UploadIcon />} - titleText={t('notebook.upload.modal.dragDropTitle')} - titleTextSeparator={t('notebook.upload.modal.separator')} - infoText={t('notebook.upload.modal.infoText')} - browseButtonText={t('notebook.upload.modal.browseButton')} - /> - </MultipleFileUpload> + <StyledDropzone> + <MultipleFileUpload + dropzoneProps={{ + accept: getNotebookAcceptedFileTypes(), + onDropRejected: handleDropRejected, + }} + onFileDrop={handleFileDrop} + > + <MultipleFileUploadMain + titleIcon={<UploadIcon />} + titleText={t('notebook.upload.modal.dragDropTitle')} + titleTextSeparator={t('notebook.upload.modal.separator')} + infoText={t('notebook.upload.modal.infoText')} + browseButtonText={t('notebook.upload.modal.browseButton')} + /> + </MultipleFileUpload> + </StyledDropzone> )} {selectedFiles.length > 0 && ( - <Box className={classes.fileListContainer}> - <Box className={classes.fileListHeader}> - <Typography className={classes.fileCount}> + <FileListContainer> + <Box + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + mb: 1, + }} + > + <Typography + sx={{ fontSize: '0.875rem', color: 'text.secondary' }} + > {(t as Function)('notebook.upload.modal.selectedFiles', { count: selectedFiles.length, max: NOTEBOOK_MAX_FILES - existingDocumentNames.length, @@ -311,21 +288,23 @@ export const AddDocumentModal = ({ )} /> ))} - </Box> + </FileListContainer> )} </DialogContent> - <DialogActions className={classes.dialogActions}> + <DialogActions + sx={{ padding: '16px 24px', justifyContent: 'flex-end', gap: 1 }} + > <Button onClick={handleClose} - className={classes.cancelButton} + sx={{ textTransform: 'none' }} color="inherit" > {t('common.cancel')} </Button> <Button onClick={handleAddFiles} - className={classes.addButton} + sx={{ textTransform: 'none' }} variant="contained" color="primary" disabled={selectedFiles.length === 0 || hasUploadsInProgress} diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DeleteDocumentModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DeleteDocumentModal.tsx index 6e7bb00144..56a2827dce 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DeleteDocumentModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DeleteDocumentModal.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@mui/icons-material/Close'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -28,46 +27,6 @@ import Typography from '@mui/material/Typography'; import { useTranslation } from '../../hooks/useTranslation'; import { Trans } from '../Trans'; -const useStyles = makeStyles(theme => ({ - dialogPaper: { - borderRadius: 16, - }, - dialogTitle: { - padding: '16px 20px', - fontStyle: 'inherit', - }, - dialogContent: { - paddingTop: 0, - }, - titleRow: { - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1), - }, - titleText: { - fontWeight: 'bold', - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.text.primary, - }, - dialogActions: { - justifyContent: 'left', - padding: theme.spacing(2.5), - gap: theme.spacing(1), - }, - removeButton: { - textTransform: 'none', - borderRadius: 999, - }, - cancelButton: { - textTransform: 'none', - borderRadius: 999, - }, -})); - type DeleteDocumentModalProps = { isOpen: boolean; onClose: () => void; @@ -81,7 +40,6 @@ export const DeleteDocumentModal = ({ onConfirm, documentName, }: DeleteDocumentModalProps) => { - const classes = useStyles(); const { t } = useTranslation(); return ( @@ -92,12 +50,12 @@ export const DeleteDocumentModal = ({ aria-describedby="delete-document-modal-body" fullWidth PaperProps={{ - className: classes.dialogPaper, + sx: { borderRadius: 4 }, }} > - <DialogTitle className={classes.dialogTitle}> - <Box className={classes.titleRow}> - <Typography component="span" className={classes.titleText}> + <DialogTitle sx={{ padding: '16px 20px', fontStyle: 'inherit' }}> + <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> + <Typography component="span" sx={{ fontWeight: 'bold' }}> {t('notebook.document.delete.title')} </Typography> <IconButton @@ -105,16 +63,18 @@ export const DeleteDocumentModal = ({ onClick={onClose} title={t('common.close')} size="large" - className={classes.closeButton} + sx={{ + position: 'absolute', + right: theme => theme.spacing(1), + top: theme => theme.spacing(1), + color: 'text.primary', + }} > <CloseIcon /> </IconButton> </Box> </DialogTitle> - <DialogContent - id="delete-document-modal-body" - className={classes.dialogContent} - > + <DialogContent id="delete-document-modal-body" sx={{ paddingTop: 0 }}> <Typography variant="body2"> <Trans message="notebook.document.delete.description" @@ -124,18 +84,18 @@ export const DeleteDocumentModal = ({ /> </Typography> </DialogContent> - <DialogActions className={classes.dialogActions}> + <DialogActions sx={{ justifyContent: 'left', padding: 2.5, gap: 1 }}> <Button variant="contained" color="error" - className={classes.removeButton} + sx={{ textTransform: 'none', borderRadius: 999 }} onClick={onConfirm} > {t('notebook.document.delete.action')} </Button> <Button variant="outlined" - className={classes.cancelButton} + sx={{ textTransform: 'none', borderRadius: 999 }} onClick={onClose} > {t('common.cancel')} diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DeleteNotebookModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DeleteNotebookModal.tsx index 1f75472861..e78487c0de 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DeleteNotebookModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DeleteNotebookModal.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@mui/icons-material/Close'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; @@ -29,52 +28,6 @@ import Typography from '@mui/material/Typography'; import { useDeleteNotebook } from '../../hooks/notebooks/useDeleteNotebook'; import { useTranslation } from '../../hooks/useTranslation'; -const useStyles = makeStyles(theme => ({ - dialogPaper: { - borderRadius: 16, - }, - dialogTitle: { - padding: '16px 20px', - fontStyle: 'inherit', - }, - dialogContent: { - paddingTop: 0, - paddingBottom: theme.spacing(5), - }, - titleRow: { - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1), - }, - titleText: { - fontWeight: 'bold', - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.text.primary, - }, - errorBox: { - maxWidth: 650, - marginLeft: theme.spacing(2.5), - marginRight: theme.spacing(2.5), - }, - dialogActions: { - justifyContent: 'left', - padding: theme.spacing(2.5), - gap: theme.spacing(1), - }, - deleteButton: { - textTransform: 'none', - borderRadius: 999, - }, - cancelButton: { - textTransform: 'none', - borderRadius: 999, - }, -})); - export const DeleteNotebookModal = ({ isOpen, onClose, @@ -88,7 +41,6 @@ export const DeleteNotebookModal = ({ sessionId: string; name: string; }) => { - const classes = useStyles(); const { t } = useTranslation(); const { mutateAsync: deleteNotebook, isError, error } = useDeleteNotebook(); @@ -111,12 +63,12 @@ export const DeleteNotebookModal = ({ aria-describedby="delete-notebook-modal-body" fullWidth PaperProps={{ - className: classes.dialogPaper, + sx: { borderRadius: 4 }, }} > - <DialogTitle className={classes.dialogTitle}> - <Box className={classes.titleRow}> - <Typography component="span" className={classes.titleText}> + <DialogTitle sx={{ padding: '16px 20px', fontStyle: 'inherit' }}> + <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> + <Typography component="span" sx={{ fontWeight: 'bold' }}> {t('notebooks.delete.title', { name } as any)} </Typography> <IconButton @@ -124,7 +76,12 @@ export const DeleteNotebookModal = ({ onClick={onClose} title={t('common.close')} size="large" - className={classes.closeButton} + sx={{ + position: 'absolute', + right: theme => theme.spacing(1), + top: theme => theme.spacing(1), + color: 'text.primary', + }} > <CloseIcon /> </IconButton> @@ -132,20 +89,20 @@ export const DeleteNotebookModal = ({ </DialogTitle> <DialogContent id="delete-notebook-modal-body" - className={classes.dialogContent} + sx={{ paddingTop: 0, paddingBottom: theme => theme.spacing(5) }} > <Typography variant="body2">{t('notebooks.delete.message')}</Typography> </DialogContent> {isError && ( - <Box className={classes.errorBox}> + <Box sx={{ maxWidth: 650, mx: 2.5 }}> <Alert severity="error">{String(error)}</Alert> </Box> )} - <DialogActions className={classes.dialogActions}> + <DialogActions sx={{ justifyContent: 'left', padding: 2.5, gap: 1 }}> <Button variant="contained" color="error" - className={classes.deleteButton} + sx={{ textTransform: 'none', borderRadius: 999 }} onClick={handleDelete} > {t('notebooks.delete.action')} @@ -153,7 +110,7 @@ export const DeleteNotebookModal = ({ <Button key="cancel" variant="outlined" - className={classes.cancelButton} + sx={{ textTransform: 'none', borderRadius: 999 }} onClick={onClose} > {t('common.cancel')} diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DocumentSidebar.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DocumentSidebar.tsx index 34111003bb..b805b8e2ae 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DocumentSidebar.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DocumentSidebar.tsx @@ -16,7 +16,8 @@ import { useState } from 'react'; -import { makeStyles, Typography } from '@material-ui/core'; +import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; import { Button, Dropdown, @@ -34,94 +35,48 @@ import { SessionDocument } from '../../types'; import { FileTypeIcon } from './FileTypeIcon'; import { SidebarCollapseIcon } from './SidebarCollapseIcon'; -const useStyles = makeStyles(theme => ({ - sidebar: { - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - padding: theme.spacing(2), - overflow: 'hidden', - }, - titleRow: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: theme.spacing(2), - gap: theme.spacing(1), - }, - title: { - fontWeight: 500, - fontSize: '1.25rem', - lineHeight: '2rem', - letterSpacing: '-0.25px', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - flex: 1, - minWidth: 0, - }, - collapseButton: { - flexShrink: 0, - }, - documentsRow: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - }, - documentCount: { - fontWeight: 700, - fontSize: '1.125rem', - lineHeight: '2rem', - }, - addButton: { - textTransform: 'none', - }, - documentsList: { - marginTop: theme.spacing(2), - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(0.5), - overflowY: 'auto', - flex: 1, - }, - documentItem: { - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1), - padding: `${theme.spacing(1)}px ${theme.spacing(0.5)}px`, - borderRadius: 4, - }, - fileIcon: { - flexShrink: 0, - color: theme.palette.grey[500], - fontSize: '1rem', - }, - fileName: { - flex: 1, - minWidth: 0, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - fontSize: '0.875rem', - lineHeight: '1.25rem', - }, - spinnerContainer: { - flexShrink: 0, - }, - kebabToggle: { - padding: 0, - flexShrink: 0, - }, - kebabDropdownMenu: { - '& .pf-v6-c-menu__list': { - paddingInlineStart: 0, - marginBlockStart: 0, - marginBlockEnd: 0, - }, - }, +const Sidebar = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + padding: theme.spacing(2), + overflow: 'hidden', +})); + +const TitleRow = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing(2), + gap: theme.spacing(1), +})); + +const DocumentsList = styled('div')(({ theme }) => ({ + marginTop: theme.spacing(2), + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(0.5), + overflowY: 'auto', + flex: 1, +})); + +const DocumentItem = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: `${theme.spacing(1)} ${theme.spacing(0.5)}`, + borderRadius: 4, })); +const KebabDropdownMenu = styled(Dropdown)({ + '& .pf-v6-c-menu__list': { + paddingInlineStart: 0, + marginBlockStart: 0, + marginBlockEnd: 0, + }, +}); + type DocumentSidebarProps = { notebookName: string; documents: SessionDocument[]; @@ -147,7 +102,6 @@ export const DocumentSidebar = ({ onAddDocument, onDeleteDocument, }: DocumentSidebarProps) => { - const classes = useStyles(); const { t } = useTranslation(); const [openMenuDocId, setOpenMenuDocId] = useState<string | null>(null); @@ -163,23 +117,49 @@ export const DocumentSidebar = ({ const isAddDisabled = totalCount >= NOTEBOOK_MAX_FILES; return ( - <div className={classes.sidebar}> - <div className={classes.titleRow}> - <Typography className={classes.title}>{notebookName}</Typography> + <Sidebar> + <TitleRow> + <Typography + sx={{ + fontWeight: 500, + fontSize: '1.25rem', + lineHeight: '2rem', + letterSpacing: '-0.25px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + flex: 1, + minWidth: 0, + }} + > + {notebookName} + </Typography> <Tooltip content={t('notebook.view.sidebar.collapse')} position="right"> <Button variant="plain" - className={classes.collapseButton} + style={{ flexShrink: 0 }} onClick={onToggleCollapse} aria-label={t('notebook.view.sidebar.collapse')} > <SidebarCollapseIcon /> </Button> </Tooltip> - </div> + </TitleRow> - <div className={classes.documentsRow}> - <Typography className={classes.documentCount}> + <div + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }} + > + <Typography + sx={{ + fontWeight: 700, + fontSize: '1.125rem', + lineHeight: '2rem', + }} + > {t('notebook.view.documents.count', { count: totalCount, } as any)} @@ -196,7 +176,7 @@ export const DocumentSidebar = ({ <Typography component="div"> <Button variant="link" - className={classes.addButton} + style={{ textTransform: 'none' }} icon={<PlusCircleIcon />} isDisabled > @@ -207,7 +187,7 @@ export const DocumentSidebar = ({ ) : ( <Button variant="link" - className={classes.addButton} + style={{ textTransform: 'none' }} icon={<PlusCircleIcon />} onClick={onAddDocument} > @@ -217,21 +197,32 @@ export const DocumentSidebar = ({ </div> {(documents.length > 0 || activePending.length > 0) && ( - <div className={classes.documentsList}> + <DocumentsList> {documents.map(doc => ( - <div key={doc.document_id} className={classes.documentItem}> + <DocumentItem key={doc.document_id}> <FileTypeIcon fileName={doc.title} /> - <Typography className={classes.fileName}>{doc.title}</Typography> + <Typography + sx={{ + flex: 1, + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontSize: '0.875rem', + lineHeight: '1.25rem', + }} + > + {doc.title} + </Typography> {deletingDocumentIds?.has(doc.document_id) ? ( - <div className={classes.spinnerContainer}> + <div style={{ flexShrink: 0 }}> <Spinner size="md" aria-label={t('notebook.document.delete')} /> </div> ) : ( - <Dropdown - className={classes.kebabDropdownMenu} + <KebabDropdownMenu isOpen={openMenuDocId === doc.document_id} popperProps={{ position: 'end', @@ -244,7 +235,7 @@ export const DocumentSidebar = ({ <MenuToggle ref={toggleRef} variant="plain" - className={classes.kebabToggle} + style={{ padding: 0, flexShrink: 0 }} isExpanded={openMenuDocId === doc.document_id} onClick={event => { event.stopPropagation(); @@ -270,26 +261,38 @@ export const DocumentSidebar = ({ {t('notebook.document.delete')} </DropdownItem> </DropdownList> - </Dropdown> + </KebabDropdownMenu> )} - </div> + </DocumentItem> ))} {activePending.map(fileName => ( - <div key={`pending-${fileName}`} className={classes.documentItem}> + <DocumentItem key={`pending-${fileName}`}> <FileTypeIcon fileName={fileName} /> - <Typography className={classes.fileName}>{fileName}</Typography> + <Typography + sx={{ + flex: 1, + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontSize: '0.875rem', + lineHeight: '1.25rem', + }} + > + {fileName} + </Typography> {!completedFileNames?.has(fileName) && ( - <div className={classes.spinnerContainer}> + <div style={{ flexShrink: 0 }}> <Spinner size="md" aria-label={t('notebook.view.documents.uploading')} /> </div> )} - </div> + </DocumentItem> ))} - </div> + </DocumentsList> )} - </div> + </Sidebar> ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx index de0c59b830..f56b8bbcad 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx @@ -14,53 +14,31 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@mui/icons-material/Close'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; +import { styled } from '@mui/material/styles'; import Typography from '@mui/material/Typography'; import { FileTypeIcon } from './FileTypeIcon'; -const useStyles = makeStyles(theme => ({ - container: { - display: 'flex', - alignItems: 'center', - padding: '8px 12px', - borderRadius: 8, - backgroundColor: theme.palette.type === 'dark' ? '#2a2a2a' : '#f5f5f5', - marginBottom: 8, - '&:last-child': { - marginBottom: 0, - }, +const Container = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: '8px 12px', + borderRadius: 8, + backgroundColor: theme.palette.mode === 'dark' ? '#2a2a2a' : '#f5f5f5', + marginBottom: 8, + '&:last-child': { + marginBottom: 0, }, - fileInfo: { - display: 'flex', - alignItems: 'center', - flex: 1, - minWidth: 0, - gap: 12, - }, - fileName: { - flex: 1, - minWidth: 0, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - fontSize: '0.875rem', - }, - fileSize: { - color: theme.palette.text.secondary, - fontSize: '0.75rem', - flexShrink: 0, - marginRight: 8, - }, - removeButton: { - padding: 4, - color: theme.palette.action.active, - '&:hover': { - color: theme.palette.error.main, - }, +})); + +const RemoveButton = styled(IconButton)(({ theme }) => ({ + padding: 4, + color: theme.palette.action.active, + '&:hover': { + color: theme.palette.error.main, }, })); @@ -98,28 +76,51 @@ export const FileListItem = ({ onRemove, removeAriaLabel = 'Remove file', }: FileListItemProps) => { - const classes = useStyles(); const displayName = truncateFileName(file.name, MAX_FILENAME_LENGTH); return ( - <Box className={classes.container}> - <Box className={classes.fileInfo}> + <Container> + <Box + sx={{ + display: 'flex', + alignItems: 'center', + flex: 1, + minWidth: 0, + gap: '12px', + }} + > <FileTypeIcon fileName={file.name} /> - <Typography className={classes.fileName} title={file.name}> + <Typography + sx={{ + flex: 1, + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontSize: '0.875rem', + }} + title={file.name} + > {displayName} </Typography> </Box> - <Typography className={classes.fileSize}> + <Typography + sx={{ + color: 'text.secondary', + fontSize: '0.75rem', + flexShrink: 0, + mr: 1, + }} + > {formatFileSize(file.size)} </Typography> - <IconButton - className={classes.removeButton} + <RemoveButton onClick={onRemove} aria-label={removeAriaLabel} size="small" > <CloseIcon fontSize="small" /> - </IconButton> - </Box> + </RemoveButton> + </Container> ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileTypeIcon.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileTypeIcon.tsx index 75cf586637..6a37c5106d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileTypeIcon.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileTypeIcon.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core'; +import { styled } from '@mui/material/styles'; const FILE_TYPE_COLORS: Record<string, string> = { pdf: '#C9190B', @@ -35,23 +35,21 @@ const FILE_TYPE_COLORS: Record<string, string> = { const DEFAULT_COLOR = '#6A6E73'; -const useStyles = makeStyles(() => ({ - badge: { - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - minWidth: 28, - height: 22, - padding: '0 4px', - borderRadius: 4, - border: '1.5px solid', - fontSize: '0.625rem', - fontWeight: 700, - textTransform: 'uppercase', - lineHeight: 1, - flexShrink: 0, - }, -})); +const Badge = styled('span')({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: 28, + height: 22, + padding: '0 4px', + borderRadius: 4, + border: '1.5px solid', + fontSize: '0.625rem', + fontWeight: 700, + textTransform: 'uppercase', + lineHeight: 1, + flexShrink: 0, +}); type FileTypeIconProps = { fileName: string; @@ -65,17 +63,13 @@ const getExtension = (fileName: string): string => { }; export const FileTypeIcon = ({ fileName, className }: FileTypeIconProps) => { - const classes = useStyles(); const ext = getExtension(fileName); const color = FILE_TYPE_COLORS[ext] ?? DEFAULT_COLOR; const label = ext || '?'; return ( - <span - className={`${classes.badge}${className ? ` ${className}` : ''}`} - style={{ borderColor: color, color }} - > + <Badge className={className} style={{ borderColor: color, color }}> {label} - </span> + </Badge> ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index b774c97b38..9fa99ff573 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -18,7 +18,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { makeStyles, Typography } from '@material-ui/core'; +import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; import { ChatbotContent, ChatbotFooter, @@ -66,206 +67,142 @@ import { OverwriteConfirmModal } from './OverwriteConfirmModal'; import { AddCircleFilledIcon, SidebarExpandIcon } from './SidebarCollapseIcon'; import { UploadResourceScreen } from './UploadResourceScreen'; -const useStyles = makeStyles(theme => ({ - root: { - display: 'flex', - flexDirection: 'column', - flex: 1, - minHeight: 0, - height: '100%', - backgroundColor: 'var(--pf-t--global--background--color--primary--default)', - }, - drawerContainer: { - flex: 1, - minHeight: 0, - '& .pf-v6-c-drawer__panel, & .pf-v5-c-drawer__panel': { - backgroundColor: - 'var(--pf-t--global--background--color--floating--default) !important', - }, - '& .pf-v6-c-drawer__panel-main, & .pf-v5-c-drawer__panel-main': { - backgroundColor: - 'var(--pf-t--global--background--color--floating--default) !important', - }, - '& .pf-v6-c-drawer__panel-body, & .pf-v5-c-drawer__panel-body': { - backgroundColor: - 'var(--pf-t--global--background--color--floating--default) !important', - }, - '& .pf-v6-c-drawer__splitter, & .pf-v5-c-drawer__splitter': { - backgroundColor: - 'var(--pf-t--global--background--color--floating--default)', - }, - }, - expandStrip: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - paddingTop: theme.spacing(1.5), - gap: theme.spacing(1), - borderRight: '1px solid var(--pf-t--global--border--color--default)', +const Root = styled('div')({ + display: 'flex', + flexDirection: 'column', + flex: 1, + minHeight: 0, + height: '100%', + backgroundColor: 'var(--pf-t--global--background--color--primary--default)', +}); + +const DrawerContainer = styled(Drawer)({ + flex: 1, + minHeight: 0, + '& .pf-v6-c-drawer__panel, & .pf-v5-c-drawer__panel': { backgroundColor: - 'var(--pf-t--global--background--color--floating--default)', + 'var(--pf-t--global--background--color--floating--default) !important', }, - addIconButton: { - padding: 0, - minWidth: 0, - lineHeight: 1, - }, - mainArea: { - display: 'flex', - flexDirection: 'row', - height: '100%', - minWidth: 0, - }, - topBar: { - display: 'flex', - justifyContent: 'flex-end', - padding: `${theme.spacing(1.5)}px ${theme.spacing(2)}px`, + '& .pf-v6-c-drawer__panel-main, & .pf-v5-c-drawer__panel-main': { backgroundColor: - 'var(--pf-t--global--background--color--floating--default)', - }, - closeButton: { - textTransform: 'none', - }, - mainContent: { - display: 'flex', - flexDirection: 'column', - flex: 1, - minHeight: 0, - }, - drawerContentBody: { - backgroundColor: 'var(--pf-t--global--background--color--primary--default)', - height: '100%', + 'var(--pf-t--global--background--color--floating--default) !important', }, - contentColumn: { - display: 'flex', - flexDirection: 'column', - flex: 1, - minWidth: 0, - minHeight: 0, - overflow: 'hidden', - }, - notebookDisclaimerStrip: { - width: '100%', - maxWidth: 'unset', - margin: 0, - padding: `0 0 ${theme.spacing(1)}px`, - boxSizing: 'border-box', + '& .pf-v6-c-drawer__panel-body, & .pf-v5-c-drawer__panel-body': { backgroundColor: - 'var(--pf-t--global--background--color--floating--default)', - '& .pf-v6-c-alert, & .pf-v5-c-alert': { - backgroundColor: - 'var(--pf-t--global--background--color--secondary--default) !important', - }, - '& .pf-v6-c-alert__content, & .pf-v5-c-alert__content': { - backgroundColor: 'transparent !important', - }, - '& .pf-v6-c-alert__body, & .pf-v5-c-alert__body': { - backgroundColor: 'transparent !important', - }, - '& .pf-v6-c-alert__description, & .pf-v5-c-alert__description': { - backgroundColor: 'transparent !important', - }, + 'var(--pf-t--global--background--color--floating--default) !important', }, - notebookDisclaimerInner: { - width: '95%', - maxWidth: 'unset', - margin: '0 auto', - }, - toastAlertGroup: { - '--pf-v6-c-alert-group--m-toast--InsetInlineEnd': `${theme.spacing(2.5)}px`, - '--pf-v6-c-alert-group--m-toast--InsetBlockStart': `${theme.spacing(2.5)}px`, - '--pf-v6-c-alert-group--m-toast--MaxWidth': '350px', - '--pf-v6-c-alert-group--m-toast--ZIndex': '9999', - }, - toastAlert: { - maxWidth: '350px', - '& .pf-v6-c-alert__title': { - margin: 0, - }, - }, - welcomeContainer: { - display: 'flex', - flexDirection: 'column', - flex: 1, - minHeight: 0, - overflow: 'auto', + '& .pf-v6-c-drawer__splitter, & .pf-v5-c-drawer__splitter': { backgroundColor: 'var(--pf-t--global--background--color--floating--default)', }, - notebookEmptyUpload: { - flex: 1, - display: 'flex', - flexDirection: 'column', - minHeight: 0, +}); + +const ExpandStrip = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + paddingTop: theme.spacing(1.5), + gap: theme.spacing(1), + borderRight: '1px solid var(--pf-t--global--border--color--default)', + backgroundColor: 'var(--pf-t--global--background--color--floating--default)', +})); + +const TopBar = styled('div')(({ theme }) => ({ + display: 'flex', + justifyContent: 'flex-end', + padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`, + backgroundColor: 'var(--pf-t--global--background--color--floating--default)', +})); + +const NotebookDisclaimerStrip = styled('div')(({ theme }) => ({ + width: '100%', + maxWidth: 'unset', + margin: 0, + padding: `0 0 ${theme.spacing(1)}`, + boxSizing: 'border-box', + backgroundColor: 'var(--pf-t--global--background--color--floating--default)', + '& .pf-v6-c-alert, & .pf-v5-c-alert': { backgroundColor: - 'var(--pf-t--global--background--color--floating--default)', + 'var(--pf-t--global--background--color--secondary--default) !important', }, - notebookContentArea: { - width: '95%', - maxWidth: 'unset', - margin: `${theme.spacing(3)}px auto 0 auto`, - padding: 0, + '& .pf-v6-c-alert__content, & .pf-v5-c-alert__content': { + backgroundColor: 'transparent !important', }, - notebookHeading: { - fontSize: '2rem', - fontWeight: 500, - lineHeight: 1.25, - padding: `${theme.spacing(1)}px 0`, + '& .pf-v6-c-alert__body, & .pf-v5-c-alert__body': { + backgroundColor: 'transparent !important', }, - notebookSummary: { - fontSize: '1rem', - lineHeight: 2, - color: 'var(--pf-t--global--text--color--regular)', - paddingTop: theme.spacing(0.5), + '& .pf-v6-c-alert__description, & .pf-v5-c-alert__description': { + backgroundColor: 'transparent !important', }, - promptSuggestions: { - display: 'flex', - flexWrap: 'wrap' as const, - gap: theme.spacing(1), - width: '95%', - maxWidth: 'unset', - margin: `${theme.spacing(3)}px auto ${theme.spacing(3)}px auto`, - justifyContent: 'flex-start', +})); + +const WelcomeContainer = styled('div')({ + display: 'flex', + flexDirection: 'column', + flex: 1, + minHeight: 0, + overflow: 'auto', + backgroundColor: 'var(--pf-t--global--background--color--floating--default)', +}); + +const PromptSuggestions = styled('div')(({ theme }) => ({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(1), + width: '95%', + maxWidth: 'unset', + margin: `${theme.spacing(3)} auto ${theme.spacing(3)} auto`, + justifyContent: 'flex-start', +})); + +const PromptPill = styled('button')(({ theme }) => ({ + appearance: 'none', + background: 'transparent', + border: `1px solid var(--pf-t--global--border--color--default)`, + borderRadius: '999px', + padding: `${theme.spacing(1)} ${theme.spacing(2.5)}`, + fontSize: '0.875rem', + color: 'var(--pf-t--global--text--color--regular)', + cursor: 'pointer', + transition: 'background-color 0.15s, border-color 0.15s', + '&:hover': { + backgroundColor: + 'var(--pf-t--global--background--color--secondary--default)', + borderColor: 'var(--pf-t--global--border--color--hover)', }, - promptPill: { - appearance: 'none' as const, - background: 'transparent', - border: `1px solid var(--pf-t--global--border--color--default)`, - borderRadius: '999px', - padding: `${theme.spacing(1)}px ${theme.spacing(2.5)}px`, - fontSize: '0.875rem', - color: 'var(--pf-t--global--text--color--regular)', - cursor: 'pointer', - transition: 'background-color 0.15s, border-color 0.15s', - '&:hover': { - backgroundColor: - 'var(--pf-t--global--background--color--secondary--default)', - borderColor: 'var(--pf-t--global--border--color--hover)', - }, +})); + +const StyledFooter = styled(ChatbotFooter)(({ theme }) => ({ + backgroundColor: 'var(--pf-t--global--background--color--floating--default)', + '&>.pf-chatbot__footer-container': { + width: '95% !important', + maxWidth: 'unset !important', }, - footer: { + '& .pf-chatbot__message-bar': { backgroundColor: - 'var(--pf-t--global--background--color--floating--default)', - '&>.pf-chatbot__footer-container': { - width: '95% !important', - maxWidth: 'unset !important', - }, - '& .pf-chatbot__message-bar': { - backgroundColor: - theme.palette.type === 'light' - ? theme.palette.grey[100] - : 'var(--pf-t--global--background--color--secondary--default)', - }, - }, - chatContent: { - minHeight: 0, - display: 'flex', - flexDirection: 'column', - flex: 1, - overflow: 'auto', + theme.palette.mode === 'light' + ? theme.palette.grey[100] + : 'var(--pf-t--global--background--color--secondary--default)', }, })); +const ToastAlertGroup = styled(AlertGroup)( + ({ theme }) => + ({ + '--pf-v6-c-alert-group--m-toast--InsetInlineEnd': `${theme.spacing(2.5)}`, + '--pf-v6-c-alert-group--m-toast--InsetBlockStart': `${theme.spacing(2.5)}`, + '--pf-v6-c-alert-group--m-toast--MaxWidth': '350px', + '--pf-v6-c-alert-group--m-toast--ZIndex': '9999', + }) as any, +); + +const ToastAlert = styled(Alert)({ + maxWidth: '350px', + '& .pf-v6-c-alert__title': { + margin: 0, + }, +}); + type NotebookViewProps = { sessionId: string; notebookName?: string; @@ -293,14 +230,12 @@ export const NotebookView = ({ topicRestrictionEnabled, onClose, }: NotebookViewProps) => { - const classes = useStyles(); const { t } = useTranslation(); const queryClient = useQueryClient(); const configApi = useApi(configApiRef); const notebooksApi = useApi(notebooksApiRef); const { mutateAsync: notebookCreateMessage } = useCreateNotebookMessage(); - // Use notebook-specific model from config instead of chat's selected model const notebookModel = configApi.getOptionalString('lightspeed.notebooks.queryDefaults.model') || ''; @@ -578,26 +513,44 @@ export const NotebookView = ({ ); const renderNotebookDisclaimerAlert = () => ( - <div className={classes.notebookDisclaimerStrip}> - <div className={classes.notebookDisclaimerInner}> + <NotebookDisclaimerStrip> + <div style={{ width: '95%', maxWidth: 'unset', margin: '0 auto' }}> <Alert isInline variant="info" title={t('aria.important')}> {t('disclaimer.withoutValidation')} </Alert> </div> - </div> + </NotebookDisclaimerStrip> ); const renderMainContent = () => { if (!hasDocuments && messages.length === 0) { return ( - <Typography component="span" className={classes.notebookEmptyUpload}> + <Typography + component="span" + sx={{ + flex: 1, + display: 'flex', + flexDirection: 'column', + minHeight: 0, + backgroundColor: + 'var(--pf-t--global--background--color--floating--default)', + }} + > <UploadResourceScreen onUploadClick={handleOpenUploadModal} /> </Typography> ); } if (messages.length > 0) { return ( - <ChatbotContent className={classes.chatContent}> + <ChatbotContent + style={{ + minHeight: 0, + display: 'flex', + flexDirection: 'column', + flex: 1, + overflow: 'auto', + }} + > <LightspeedChatBox userName={userName} messages={messages} @@ -613,52 +566,66 @@ export const NotebookView = ({ ); } return ( - <div className={classes.welcomeContainer}> + <WelcomeContainer> <div style={{ flex: 1 }} /> {renderNotebookDisclaimerAlert()} - <div className={classes.notebookContentArea}> - <Typography className={classes.notebookHeading}> + <div + style={{ + width: '95%', + maxWidth: 'unset', + margin: '24px auto 0 auto', + padding: 0, + }} + > + <Typography + sx={{ + fontSize: '2rem', + fontWeight: 500, + lineHeight: 1.25, + padding: '8px 0', + }} + > {notebookName} </Typography> {topicSummary && ( - <Typography className={classes.notebookSummary}> + <Typography + sx={{ + fontSize: '1rem', + lineHeight: 2, + color: 'var(--pf-t--global--text--color--regular)', + paddingTop: 0.5, + }} + > {topicSummary} </Typography> )} </div> {welcomePrompts.length > 0 && ( - <div className={classes.promptSuggestions}> + <PromptSuggestions> {welcomePrompts.map(prompt => ( - <button + <PromptPill key={prompt.title} type="button" - className={classes.promptPill} onClick={prompt.onClick} > {prompt.title} - </button> + </PromptPill> ))} - </div> + </PromptSuggestions> )} - </div> + </WelcomeContainer> ); }; return ( - <div className={classes.root}> + <Root> {toastAlerts.length > 0 && ( - <AlertGroup - hasAnimations - isToast - isLiveRegion - className={classes.toastAlertGroup} - > + <ToastAlertGroup hasAnimations isToast isLiveRegion> {toastAlerts.map(({ key, title, variant }) => ( - <Alert + <ToastAlert key={key} variant={AlertVariant[variant ?? 'success']} title={title} - className={classes.toastAlert} timeout={2000} onTimeout={() => handleRemoveToastAlert(key as React.Key)} actionClose={ @@ -670,21 +637,29 @@ export const NotebookView = ({ } /> ))} - </AlertGroup> + </ToastAlertGroup> )} - <Drawer - isExpanded={!sidebarCollapsed} - isInline - position="start" - className={classes.drawerContainer} - > + <DrawerContainer isExpanded={!sidebarCollapsed} isInline position="start"> <DrawerContent panelContent={!sidebarCollapsed ? panelContent : undefined} > - <DrawerContentBody className={classes.drawerContentBody}> - <div className={classes.mainArea}> + <DrawerContentBody + style={{ + backgroundColor: + 'var(--pf-t--global--background--color--primary--default)', + height: '100%', + }} + > + <div + style={{ + display: 'flex', + flexDirection: 'row', + height: '100%', + minWidth: 0, + }} + > {sidebarCollapsed && ( - <div className={classes.expandStrip}> + <ExpandStrip> <Tooltip content={t('notebook.view.sidebar.expand')} position="right" @@ -711,7 +686,7 @@ export const NotebookView = ({ <Typography component="span"> <Button variant="plain" - className={classes.addIconButton} + style={{ padding: 0, minWidth: 0, lineHeight: 1 }} onClick={ isAddDisabled ? undefined : handleOpenUploadModal } @@ -722,29 +697,47 @@ export const NotebookView = ({ </Button> </Typography> </Tooltip> - </div> + </ExpandStrip> )} - <div className={classes.contentColumn}> - <div className={classes.topBar}> + <div + style={{ + display: 'flex', + flexDirection: 'column', + flex: 1, + minWidth: 0, + minHeight: 0, + overflow: 'hidden', + }} + > + <TopBar> <Button variant="link" - className={classes.closeButton} + style={{ textTransform: 'none' }} onClick={onClose} icon={<TimesIcon />} iconPosition="end" > {t('notebook.view.close')} </Button> + </TopBar> + + <div + style={{ + display: 'flex', + flexDirection: 'column', + flex: 1, + minHeight: 0, + }} + > + {renderMainContent()} </div> - <div className={classes.mainContent}>{renderMainContent()}</div> - {!hasDocuments && messages.length === 0 && renderNotebookDisclaimerAlert()} - <ChatbotFooter className={classes.footer}> + <StyledFooter> {documents.length === 0 ? ( <Tooltip content={t('notebook.view.input.disabledTooltip')} @@ -773,12 +766,12 @@ export const NotebookView = ({ /> )} <ChatbotFootnote label={t('footer.accuracy.label')} /> - </ChatbotFooter> + </StyledFooter> </div> </div> </DrawerContentBody> </DrawerContent> - </Drawer> + </DrawerContainer> <AddDocumentModal isOpen={isUploadModalOpen} @@ -807,6 +800,6 @@ export const NotebookView = ({ onConfirm={confirmDeleteDocument} documentName={deleteDocumentTarget?.name ?? ''} /> - </div> + </Root> ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebooksTab.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebooksTab.tsx index 49077ebc5a..c78cdcd7cc 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebooksTab.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebooksTab.tsx @@ -16,7 +16,7 @@ import type { TranslationFunction } from '@backstage/core-plugin-api/alpha'; -import { Typography } from '@material-ui/core'; +import Typography from '@mui/material/Typography'; import { Button } from '@patternfly/react-core'; import { PlusCircleIcon } from '@patternfly/react-icons'; import { CatalogIcon } from '@patternfly/react-icons/dist/esm/icons'; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx index 61327087ef..d38858dd52 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@mui/icons-material/Close'; import Alert from '@mui/material/Alert'; import Button from '@mui/material/Button'; @@ -23,73 +22,26 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; +import { styled } from '@mui/material/styles'; import Typography from '@mui/material/Typography'; import { useTranslation } from '../../hooks/useTranslation'; import { FileTypeIcon } from './FileTypeIcon'; -const useStyles = makeStyles(theme => ({ - dialogPaper: { - borderRadius: 24, - maxWidth: 578, - }, - dialogTitle: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '24px 24px 16px', - }, - titleText: { - fontWeight: 500, - fontSize: '1.25rem', - lineHeight: '1.625rem', - letterSpacing: '-0.25px', - }, - closeButton: { - color: theme.palette.text.primary, - }, - dialogContent: { - padding: '0 24px 24px', - }, - fileList: { - margin: 0, - padding: 0, - listStyle: 'none', - }, - fileItem: { - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1), - padding: `${theme.spacing(2)}px 0`, - borderBottom: - '1px solid var(--pf-t--global--border--color--default, #c7c7c7)', - cursor: 'pointer', - }, - fileName: { - flex: 1, - minWidth: 0, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - fontSize: '0.875rem', - lineHeight: '1.25rem', - }, - dialogActions: { - justifyContent: 'left', - padding: theme.spacing(2.5), - gap: theme.spacing(1), - }, - overwriteButton: { - textTransform: 'none', - borderRadius: 999, - }, - cancelButton: { - textTransform: 'none', - borderRadius: 999, - }, - warningAlert: { - borderRadius: '6px', - }, +const FileList = styled('ul')({ + margin: 0, + padding: 0, + listStyle: 'none', +}); + +const FileItem = styled('li')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: `${theme.spacing(2)} 0`, + borderBottom: + '1px solid var(--pf-t--global--border--color--default, #c7c7c7)', + cursor: 'pointer', })); type OverwriteConfirmModalProps = { @@ -105,7 +57,6 @@ export const OverwriteConfirmModal = ({ onConfirm, fileNames, }: OverwriteConfirmModalProps) => { - const classes = useStyles(); const { t } = useTranslation(); return ( @@ -114,50 +65,77 @@ export const OverwriteConfirmModal = ({ onClose={onClose} aria-labelledby="overwrite-confirm-modal-title" PaperProps={{ - className: classes.dialogPaper, + sx: { borderRadius: '24px', maxWidth: 578 }, }} > - <DialogTitle className={classes.dialogTitle}> - <Typography component="h2" className={classes.titleText}> + <DialogTitle + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '24px 24px 16px', + }} + > + <Typography + component="h2" + sx={{ + fontWeight: 500, + fontSize: '1.25rem', + lineHeight: '1.625rem', + letterSpacing: '-0.25px', + }} + > {t('notebook.overwrite.modal.title')} </Typography> <IconButton aria-label={t('common.close')} onClick={onClose} - className={classes.closeButton} + sx={{ color: 'text.primary' }} size="small" > <CloseIcon /> </IconButton> </DialogTitle> - <DialogContent className={classes.dialogContent}> - <Alert severity="warning" className={classes.warningAlert}> + <DialogContent sx={{ padding: '0 24px 24px' }}> + <Alert severity="warning" sx={{ borderRadius: '6px' }}> {t('notebook.overwrite.modal.description')} </Alert> - <ul className={classes.fileList}> + <FileList> {fileNames.map(name => ( - <li key={name} className={classes.fileItem}> + <FileItem key={name}> <FileTypeIcon fileName={name} /> - <Typography className={classes.fileName}>{name}</Typography> - </li> + <Typography + sx={{ + flex: 1, + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontSize: '0.875rem', + lineHeight: '1.25rem', + }} + > + {name} + </Typography> + </FileItem> ))} - </ul> + </FileList> </DialogContent> - <DialogActions className={classes.dialogActions}> + <DialogActions sx={{ justifyContent: 'left', padding: 2.5, gap: 1 }}> <Button variant="contained" color="error" - className={classes.overwriteButton} + sx={{ textTransform: 'none', borderRadius: 999 }} onClick={onConfirm} > {t('notebook.overwrite.modal.action')} </Button> <Button variant="outlined" - className={classes.cancelButton} + sx={{ textTransform: 'none', borderRadius: 999 }} onClick={onClose} > {t('common.cancel')} diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/RenameNotebookModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/RenameNotebookModal.tsx index 0bc79b8852..9584167e69 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/RenameNotebookModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/RenameNotebookModal.tsx @@ -16,8 +16,6 @@ import { useEffect, useState } from 'react'; -import { TextField } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@mui/icons-material/Close'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; @@ -27,66 +25,21 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; +import { styled } from '@mui/material/styles'; +import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { useRenameNotebook } from '../../hooks/notebooks/useRenameNotebook'; import { useTranslation } from '../../hooks/useTranslation'; -const useStyles = makeStyles(theme => ({ - dialogPaper: { - borderRadius: 16, - }, - dialogTitle: { - padding: '16px 20px', - fontStyle: 'inherit', - }, - titleRow: { - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1), - }, - titleText: { - fontWeight: 'bold', - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.text.primary, - }, - dialogContent: { - paddingTop: 0, - paddingLeft: theme.spacing(2.5), - }, - description: { - marginBottom: theme.spacing(3), - }, - textField: { - marginTop: 0, - }, - errorBox: { - maxWidth: 650, - marginLeft: theme.spacing(2.5), - marginRight: theme.spacing(2.5), - }, - dialogActions: { - justifyContent: 'left', - padding: theme.spacing(2.5), - gap: theme.spacing(1), - }, - submitButton: { - textTransform: 'none', - borderRadius: 999, +const SubmitButton = styled(Button)({ + textTransform: 'none', + borderRadius: 999, + backgroundColor: 'var(--pf-t--global--color--brand--default)', + '&:hover': { backgroundColor: 'var(--pf-t--global--color--brand--default)', - '&:hover': { - backgroundColor: 'var(--pf-t--global--color--brand--default)', - }, - }, - cancelButton: { - textTransform: 'none', - borderRadius: 999, }, -})); +}); export const RenameNotebookModal = ({ isOpen, @@ -99,7 +52,6 @@ export const RenameNotebookModal = ({ sessionId: string; currentName: string; }) => { - const classes = useStyles(); const { t } = useTranslation(); const { mutateAsync: renameNotebook, isError, error } = useRenameNotebook(); const [name, setName] = useState<string>(''); @@ -130,12 +82,12 @@ export const RenameNotebookModal = ({ aria-describedby="rename-notebook-modal-body" fullWidth PaperProps={{ - className: classes.dialogPaper, + sx: { borderRadius: 4 }, }} > - <DialogTitle className={classes.dialogTitle}> - <Box className={classes.titleRow}> - <Typography component="span" className={classes.titleText}> + <DialogTitle sx={{ padding: '16px 20px', fontStyle: 'inherit' }}> + <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> + <Typography component="span" sx={{ fontWeight: 'bold' }}> {t('notebooks.rename.title').replace('{{name}}', currentName)} </Typography> <IconButton @@ -143,7 +95,12 @@ export const RenameNotebookModal = ({ onClick={onClose} title={t('common.close')} size="large" - className={classes.closeButton} + sx={{ + position: 'absolute', + right: theme => theme.spacing(1), + top: theme => theme.spacing(1), + color: 'text.primary', + }} > <CloseIcon /> </IconButton> @@ -151,13 +108,9 @@ export const RenameNotebookModal = ({ </DialogTitle> <DialogContent id="rename-notebook-modal-body" - className={classes.dialogContent} + sx={{ paddingTop: 0, paddingLeft: 2.5 }} > - <Typography - variant="body2" - color="textSecondary" - className={classes.description} - > + <Typography variant="body2" color="textSecondary" sx={{ mb: 3 }}> {t('notebooks.rename.description')} </Typography> <TextField @@ -166,32 +119,31 @@ export const RenameNotebookModal = ({ onChange={event => setName(event.target.value)} fullWidth value={name} - className={classes.textField} + sx={{ marginTop: 0 }} variant="outlined" placeholder={t('notebooks.rename.placeholder')} - InputProps={{ + inputProps={{ autoFocus: true, }} /> </DialogContent> {isError && ( - <Box className={classes.errorBox}> + <Box sx={{ maxWidth: 650, mx: 2.5 }}> <Alert severity="error">{String(error)}</Alert> </Box> )} - <DialogActions className={classes.dialogActions}> - <Button + <DialogActions sx={{ justifyContent: 'left', padding: 2.5, gap: 1 }}> + <SubmitButton variant="contained" - className={classes.submitButton} disabled={name.trim() === '' || name.trim() === originalName.trim()} onClick={handleRename} > {t('notebooks.rename.action')} - </Button> + </SubmitButton> <Button key="cancel" variant="outlined" - className={classes.cancelButton} + sx={{ textTransform: 'none', borderRadius: 999 }} onClick={onClose} > {t('common.cancel')} diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/UploadResourceScreen.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/UploadResourceScreen.tsx index 1d86d57779..9427348632 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/UploadResourceScreen.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/UploadResourceScreen.tsx @@ -14,39 +14,29 @@ * limitations under the License. */ -import { makeStyles, Typography } from '@material-ui/core'; +import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; import { Button } from '@patternfly/react-core'; import { AddCircleOIcon } from '@patternfly/react-icons'; import { CatalogIcon } from '@patternfly/react-icons/dist/esm/icons'; import { useTranslation } from '../../hooks/useTranslation'; -const useStyles = makeStyles(theme => ({ - container: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - flex: 1, - textAlign: 'center', - gap: theme.spacing(2), - }, - icon: { - fontSize: 48, - color: 'var(--pf-t--global--icon--color--subtle)', - }, - heading: { - fontWeight: 500, - fontSize: '1.5rem', - lineHeight: '2rem', - letterSpacing: '-0.25px', - }, - uploadButton: { - textTransform: 'none', - borderRadius: 999, - paddingLeft: theme.spacing(3), - paddingRight: theme.spacing(3), - }, +const Container = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + flex: 1, + textAlign: 'center', + gap: theme.spacing(2), +})); + +const UploadButton = styled(Button)(({ theme }) => ({ + textTransform: 'none', + borderRadius: 999, + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), })); type UploadResourceScreenProps = { @@ -56,24 +46,34 @@ type UploadResourceScreenProps = { export const UploadResourceScreen = ({ onUploadClick, }: UploadResourceScreenProps) => { - const classes = useStyles(); const { t } = useTranslation(); return ( - <div className={classes.container}> - <CatalogIcon className={classes.icon} /> - <Typography className={classes.heading}> + <Container> + <CatalogIcon + style={{ + fontSize: 48, + color: 'var(--pf-t--global--icon--color--subtle)', + }} + /> + <Typography + sx={{ + fontWeight: 500, + fontSize: '1.5rem', + lineHeight: '2rem', + letterSpacing: '-0.25px', + }} + > {t('notebook.view.upload.heading')} </Typography> - <Button + <UploadButton variant="secondary" - className={classes.uploadButton} icon={<AddCircleOIcon />} iconPosition="end" onClick={onUploadClick} > {t('notebook.view.upload.action')} - </Button> - </div> + </UploadButton> + </Container> ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/utils/generateClassName.ts b/workspaces/lightspeed/plugins/lightspeed/src/utils/generateClassName.ts index 72c5e87d17..9c4f42a28e 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/utils/generateClassName.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/utils/generateClassName.ts @@ -14,13 +14,8 @@ * limitations under the License. */ -import { createGenerateClassName as createGenerateClassNameV4 } from '@material-ui/core/styles'; import { createGenerateClassName } from '@mui/styles'; export const generateClassName = createGenerateClassName({ seed: 'lightspeed', }); - -export const generateClassNameV4 = createGenerateClassNameV4({ - seed: 'lightspeed', -}); diff --git a/workspaces/lightspeed/yarn.lock b/workspaces/lightspeed/yarn.lock index 99aba1bfb9..28390819b5 100644 --- a/workspaces/lightspeed/yarn.lock +++ b/workspaces/lightspeed/yarn.lock @@ -11239,8 +11239,6 @@ __metadata: "@backstage/theme": "npm:^0.7.2" "@emotion/is-prop-valid": "npm:^1.3.1" "@ianvs/prettier-plugin-sort-imports": "npm:^4.4.0" - "@material-ui/core": "npm:^4.9.13" - "@material-ui/lab": "npm:^4.0.0-alpha.61" "@monaco-editor/react": "npm:^4.7.0" "@mui/icons-material": "npm:^6.1.8" "@mui/material": "npm:^5.12.2" @@ -16277,8 +16275,8 @@ __metadata: "@backstage/plugin-search": "npm:^1.7.0" "@backstage/plugin-user-settings": "npm:^0.9.1" "@backstage/ui": "npm:^0.13.1" - "@material-ui/core": "npm:^4.12.2" - "@material-ui/icons": "npm:^4.9.1" + "@mui/icons-material": "npm:^6.1.8" + "@mui/material": "npm:^5.12.2" "@playwright/test": "npm:1.60.0" "@red-hat-developer-hub/backstage-plugin-app-react": "npm:^0.0.5" "@red-hat-developer-hub/backstage-plugin-lightspeed": "workspace:^"