-
Notifications
You must be signed in to change notification settings - Fork 103
feat(lightspeed): fullscreen chat UX updates - history panel, message bar, and header redesign #3057
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(lightspeed): fullscreen chat UX updates - history panel, message bar, and header redesign #3057
Changes from 4 commits
f45c3cb
4cc7667
8651b5e
b582f39
c0a1f04
3d39598
d228bec
023fcf3
0e24932
2e80921
0b74421
7fecb49
5171b96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| '@red-hat-developer-hub/backstage-plugin-lightspeed': minor | ||
| --- | ||
|
|
||
| Implemented fullscreen chat UX updates including: | ||
|
|
||
| - Collapsible history panel with new expand/collapse icons | ||
| - Redesigned message bar with inline model selector and attachment menu | ||
| - New collapsed history strip with quick new chat functionality | ||
| - Updated header with Lightspeed logo | ||
| - Improved conversation list with hover-only options menu |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| import { ChangeEvent, Ref, useRef, useState } from 'react'; | ||
| import { DropEvent, FileRejection } from 'react-dropzone'; | ||
|
|
||
| import { makeStyles } from '@material-ui/core'; | ||
| import { | ||
| Dropdown, | ||
| DropdownItem, | ||
| DropdownList, | ||
| MenuToggle, | ||
| MenuToggleElement, | ||
| } from '@patternfly/react-core'; | ||
| import { PaperclipIcon, PlusIcon } from '@patternfly/react-icons'; | ||
|
|
||
| import { useTranslation } from '../hooks/useTranslation'; | ||
|
|
||
| type AttachPlusMenuProps = { | ||
| onAttach: ( | ||
| data: File[], | ||
| event: DropEvent | ChangeEvent<HTMLInputElement>, | ||
| ) => void; | ||
| allowedFileTypes?: { [key: string]: string[] }; | ||
| onAttachRejected?: (rejections: FileRejection[]) => void; | ||
| }; | ||
|
|
||
| const useStyles = makeStyles(theme => ({ | ||
| plusButton: { | ||
| width: 40, | ||
| height: 40, | ||
| padding: 0, | ||
| minWidth: 0, | ||
| borderRadius: '50%', | ||
| backgroundColor: 'transparent', | ||
| color: '#6a7282', | ||
| '&:hover': { | ||
| backgroundColor: 'rgba(0, 0, 0, 0.05)', | ||
| }, | ||
| }, | ||
| dropdown: { | ||
| padding: 0, | ||
| '& ul': { | ||
| padding: 0, | ||
| }, | ||
| '& .pf-v6-c-menu__content, & .pf-v5-c-menu__content': { | ||
| paddingTop: 4, | ||
| paddingBottom: 4, | ||
| }, | ||
| }, | ||
| menuItem: { | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'flex-start', | ||
| gap: 4, | ||
| padding: theme.spacing(1.5), | ||
| cursor: 'pointer', | ||
| minWidth: 280, | ||
| '--pf-v6-c-menu__item--PaddingTop': `${theme.spacing(1.5)}px`, | ||
| '--pf-v6-c-menu__item--PaddingBottom': `${theme.spacing(1.5)}px`, | ||
| }, | ||
| menuItemContent: { | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'flex-start', | ||
| gap: 4, | ||
| width: '100%', | ||
| }, | ||
| menuItemHeader: { | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: 8, | ||
| fontSize: 14, | ||
| fontWeight: 500, | ||
| color: '#151515', | ||
| }, | ||
| menuItemDescription: { | ||
| fontSize: 14, | ||
| color: '#707070', | ||
| paddingLeft: 26, | ||
| }, | ||
| paperclipIcon: { | ||
| width: 17.5, | ||
| height: 20, | ||
| color: '#151515', | ||
| }, | ||
| hiddenInput: { | ||
| display: 'none', | ||
| }, | ||
| })); | ||
|
|
||
| export const AttachPlusMenu = ({ | ||
| onAttach, | ||
| allowedFileTypes, | ||
| onAttachRejected, | ||
| }: AttachPlusMenuProps) => { | ||
| const [isMenuOpen, setIsMenuOpen] = useState(false); | ||
| const classes = useStyles(); | ||
| const { t } = useTranslation(); | ||
|
|
||
| const fileInputRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| const handleAttachClick = () => { | ||
| setIsMenuOpen(false); | ||
| fileInputRef.current?.click(); | ||
| }; | ||
|
|
||
| const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => { | ||
| const files = Array.from(event.target.files || []); | ||
| if (files.length === 0) return; | ||
|
|
||
| if (allowedFileTypes && onAttachRejected) { | ||
| const allowedExtensions = Object.values(allowedFileTypes).flat(); | ||
|
Check warning on line 126 in workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx
|
||
|
its-mitesh-kumar marked this conversation as resolved.
Outdated
|
||
| const accepted: File[] = []; | ||
| const rejected: FileRejection[] = []; | ||
|
|
||
| files.forEach(file => { | ||
| const ext = `.${file.name.split('.').pop()?.toLowerCase()}`; | ||
| if (allowedExtensions.includes(ext)) { | ||
| accepted.push(file); | ||
| } else { | ||
| rejected.push({ | ||
| file, | ||
| errors: [ | ||
| { | ||
| code: 'file-invalid-type', | ||
| message: `File type ${ext} is not supported`, | ||
| }, | ||
| ], | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| if (rejected.length > 0) { | ||
| onAttachRejected(rejected); | ||
| } | ||
|
|
||
| if (accepted.length > 0) { | ||
| onAttach(accepted, event); | ||
| } | ||
| } else { | ||
| onAttach(files, event); | ||
| } | ||
|
|
||
| if (fileInputRef.current) { | ||
| fileInputRef.current.value = ''; | ||
| } | ||
| }; | ||
|
|
||
| const acceptTypes = allowedFileTypes | ||
| ? Object.entries(allowedFileTypes) | ||
| .map(([mime, exts]) => [mime, ...exts]) | ||
| .flat() | ||
|
Check warning on line 166 in workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx
|
||
| .join(',') | ||
| : undefined; | ||
|
|
||
| const toggle = (toggleRef: Ref<MenuToggleElement>) => ( | ||
| <MenuToggle | ||
| ref={toggleRef} | ||
| onClick={() => setIsMenuOpen(!isMenuOpen)} | ||
| isExpanded={isMenuOpen} | ||
| variant="plain" | ||
| className={classes.plusButton} | ||
| aria-label={t('tooltip.attach')} | ||
| > | ||
| <PlusIcon /> | ||
| </MenuToggle> | ||
| ); | ||
|
|
||
| return ( | ||
| <> | ||
| <Dropdown | ||
| className={classes.dropdown} | ||
| isOpen={isMenuOpen} | ||
| onSelect={() => setIsMenuOpen(false)} | ||
| onOpenChange={isOpen => setIsMenuOpen(isOpen)} | ||
| toggle={toggle} | ||
| popperProps={{ position: 'left' }} | ||
| > | ||
| <DropdownList> | ||
| <DropdownItem | ||
| onClick={handleAttachClick} | ||
| className={classes.menuItem} | ||
| > | ||
| <div className={classes.menuItemContent}> | ||
| <div className={classes.menuItemHeader}> | ||
| <PaperclipIcon className={classes.paperclipIcon} /> | ||
| {t('attach.menu.title')} | ||
| </div> | ||
| <div className={classes.menuItemDescription}> | ||
| {t('attach.menu.description')} | ||
| </div> | ||
| </div> | ||
| </DropdownItem> | ||
| </DropdownList> | ||
| </Dropdown> | ||
| <input | ||
| ref={fileInputRef} | ||
| type="file" | ||
| className={classes.hiddenInput} | ||
| onChange={handleFileChange} | ||
| accept={acceptTypes} | ||
| data-testid="attachment-input" | ||
| /> | ||
| </> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| import { makeStyles } from '@material-ui/core'; | ||
| import { Button, Tooltip } from '@patternfly/react-core'; | ||
|
|
||
| import { useTranslation } from '../hooks/useTranslation'; | ||
|
|
||
| type IconProps = { | ||
| className?: string; | ||
| }; | ||
|
|
||
| const ExpandPanelIcon = ({ className }: IconProps) => ( | ||
| <svg | ||
| className={className} | ||
| width="24" | ||
| height="24" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <path d="M9 21V3H11V21H9ZM13 17V7L18 12L13 17Z" fill="currentColor" /> | ||
| </svg> | ||
| ); | ||
|
|
||
| export const EditSquareIcon = ({ className }: IconProps) => ( | ||
| <svg | ||
| className={className} | ||
| width="20" | ||
| height="20" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| style={{ verticalAlign: 'middle' }} | ||
| > | ||
| <path | ||
| d="M5 21C4.45 21 3.97917 20.8042 3.5875 20.4125C3.19583 20.0208 3 19.55 3 19V5C3 4.45 3.19583 3.97917 3.5875 3.5875C3.97917 3.19583 4.45 3 5 3H14L12 5H5V19H19V12L21 10V19C21 19.55 20.8042 20.0208 20.4125 20.4125C20.0208 20.8042 19.55 21 19 21H5ZM16.175 5.775L17.6 7.2L12 12.8V14H13.2L18.8 8.4L20.225 9.825L14.275 15.775C14.1083 15.9417 13.9167 16.0667 13.7 16.15C13.4833 16.2333 13.2583 16.275 13.025 16.275H11C10.7167 16.275 10.4792 16.1792 10.2875 15.9875C10.0958 15.7958 10 15.5583 10 15.275V13.25C10 13.0167 10.0417 12.7917 10.125 12.575C10.2083 12.3583 10.3333 12.1667 10.5 12L16.175 5.775ZM20.225 9.825L16.175 5.775L18.175 3.775C18.5583 3.39167 19.0292 3.2 19.5875 3.2C20.1458 3.2 20.6167 3.39167 21 3.775L22.225 5C22.6083 5.38333 22.8 5.85417 22.8 6.4125C22.8 6.97083 22.6083 7.44167 22.225 7.825L20.225 9.825Z" | ||
| fill="currentColor" | ||
| /> | ||
| </svg> | ||
| ); | ||
|
|
||
| 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%', | ||
| }, | ||
| iconButton: { | ||
| padding: 0, | ||
| minWidth: 0, | ||
| lineHeight: 1, | ||
| color: 'var(--pf-t--global--icon--color--regular)', | ||
| '&:hover': { | ||
| color: 'var(--pf-t--global--icon--color--hover)', | ||
| }, | ||
| }, | ||
| newChatIconButton: { | ||
| padding: 0, | ||
| minWidth: 0, | ||
| lineHeight: 1, | ||
| color: '#0066CC', | ||
|
its-mitesh-kumar marked this conversation as resolved.
Outdated
|
||
| '&:hover': { | ||
| color: 'var(--pf-t--global--color--brand--hover)', | ||
| }, | ||
| }, | ||
| })); | ||
|
|
||
| type CollapsedHistoryStripProps = { | ||
| onExpand: () => void; | ||
| onNewChat: () => void; | ||
| newChatDisabled?: boolean; | ||
| }; | ||
|
|
||
| export const CollapsedHistoryStrip = ({ | ||
| onExpand, | ||
| onNewChat, | ||
| newChatDisabled = false, | ||
| }: CollapsedHistoryStripProps) => { | ||
| const classes = useStyles(); | ||
| const { t } = useTranslation(); | ||
|
|
||
| return ( | ||
| <div className={classes.strip}> | ||
| <Tooltip content={t('tooltip.expandHistoryPanel')} position="right"> | ||
| <Button | ||
| variant="plain" | ||
| className={classes.iconButton} | ||
| onClick={onExpand} | ||
| aria-label={t('tooltip.expandHistoryPanel')} | ||
| > | ||
| <ExpandPanelIcon /> | ||
| </Button> | ||
| </Tooltip> | ||
| <Tooltip content={t('tooltip.quickNewChat')} position="right"> | ||
| <Button | ||
| variant="plain" | ||
| className={classes.newChatIconButton} | ||
| onClick={onNewChat} | ||
| aria-label={t('tooltip.quickNewChat')} | ||
| isDisabled={newChatDisabled} | ||
| > | ||
| <EditSquareIcon /> | ||
| </Button> | ||
| </Tooltip> | ||
| </div> | ||
| ); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.