Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions workspaces/lightspeed/.changeset/hot-bottles-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-lightspeed': minor
---

feat: add conversation sorting with persistence, persisting pinned chats and pinned chats toggle per-user
4 changes: 3 additions & 1 deletion workspaces/lightspeed/plugins/lightspeed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@
"@backstage/theme": "^0.7.0",
"@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",
"@patternfly/chatbot": "6.4.1",
"@patternfly/chatbot": "6.5.0-prerelease.28",
"@patternfly/react-core": "6.4.0",
"@patternfly/react-icons": "^6.3.1",
"@red-hat-developer-hub/backstage-plugin-lightspeed-common": "workspace:^",
"@tanstack/react-query": "^5.59.15",
"monaco-editor": "^0.54.0",
"react-markdown": "^9.0.1",
"react-use": "^17.2.4"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ readonly "settings.pinned.enable": string;
readonly "settings.pinned.disable": string;
readonly "settings.pinned.enabled.description": string;
readonly "settings.pinned.disabled.description": string;
readonly "sort.label": string;
readonly "sort.newest": string;
readonly "sort.oldest": string;
readonly "sort.alphabeticalAsc": string;
readonly "sort.alphabeticalDesc": string;
}>;

// @alpha
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,23 @@ import {
MessageProps,
} from '@patternfly/chatbot';
import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
import { DropdownItem, DropEvent, Title } from '@patternfly/react-core';
import { PlusIcon, SearchIcon } from '@patternfly/react-icons';
import {
DropdownItem,
DropEvent,
MenuToggle,
MenuToggleElement,
Select,
SelectList,
SelectOption,
Title,
Tooltip,
} from '@patternfly/react-core';
import {
PlusIcon,
SearchIcon,
SortAmountDownAltIcon,
SortAmountDownIcon,
} from '@patternfly/react-icons';
import { useQueryClient } from '@tanstack/react-query';

import { supportedFileTypes, TEMP_CONVERSATION_ID } from '../const';
Expand All @@ -47,6 +62,7 @@ import {
useIsMobile,
useLastOpenedConversation,
useLightspeedDeletePermission,
usePinnedChatsSettings,
} from '../hooks';
import { useLightspeedUpdatePermission } from '../hooks/useLightspeedUpdatePermission';
import { useTranslation } from '../hooks/useTranslation';
Expand All @@ -56,6 +72,7 @@ import { getAttachments } from '../utils/attachment-utils';
import {
getCategorizeMessages,
getFootnoteProps,
SortOption,
} from '../utils/lightspeed-chatbox-utils';
import Attachment from './Attachment';
import { useFileAttachmentContext } from './AttachmentContext';
Expand Down Expand Up @@ -100,6 +117,10 @@ const useStyles = makeStyles(theme => ({
maxWidth: '100%',
},
},
sortDropdown: {
padding: 0,
margin: 0,
},
}));

type LightspeedChatProps = {
Expand Down Expand Up @@ -134,14 +155,23 @@ export const LightspeedChat = ({
const [newChatCreated, setNewChatCreated] = useState<boolean>(false);
const [isSendButtonDisabled, setIsSendButtonDisabled] =
useState<boolean>(false);
const [isPinningChatsEnabled, setIsPinningChatsEnabled] = useState(true); // read from user settings in future
const [pinnedChats, setPinnedChats] = useState<string[]>([]); // read from user settings in future
const [targetConversationId, setTargetConversationId] = useState<string>('');
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
const [isRenameModalOpen, setIsRenameModalOpen] = useState<boolean>(false);
const [isSortSelectOpen, setIsSortSelectOpen] = useState<boolean>(false);
const { isReady, lastOpenedId, setLastOpenedId, clearLastOpenedId } =
useLastOpenedConversation(user);

const {
isPinningChatsEnabled,
pinnedChats,
selectedSort,
handlePinningChatsToggle,
pinChat,
unpinChat,
handleSortChange,
} = usePinnedChatsSettings(user);

const {
uploadError,
showAlert,
Expand All @@ -159,12 +189,6 @@ export const LightspeedChat = ({
}
}, [lastOpenedId, isReady]);

useEffect(() => {
if (!isPinningChatsEnabled) {
setPinnedChats([]);
}
}, [isPinningChatsEnabled]);

const queryClient = useQueryClient();

const {
Expand Down Expand Up @@ -282,14 +306,6 @@ export const LightspeedChat = ({
setIsDeleteModalOpen(false);
}, [clearLastOpenedId, lastOpenedId, onNewChat, targetConversationId]);

const pinChat = (convId: string) => {
setPinnedChats(prev => [...prev, convId]); // write to user settings in future
};

const unpinChat = (convId: string) => {
setPinnedChats(prev => prev.filter(id => id !== convId)); // write to user settings in future
};

const additionalMessageProps = useCallback(
(conversationSummary: ConversationSummary) => {
const isChatFavorite = pinnedChats?.find(
Expand Down Expand Up @@ -337,7 +353,15 @@ export const LightspeedChat = ({
),
};
},
[pinnedChats, hasDeleteAccess, isPinningChatsEnabled, hasUpdateAccess, t],
[
pinnedChats,
hasDeleteAccess,
isPinningChatsEnabled,
hasUpdateAccess,
t,
pinChat,
unpinChat,
],
);

const categorizedMessages = useMemo(
Expand All @@ -347,8 +371,9 @@ export const LightspeedChat = ({
pinnedChats,
additionalMessageProps,
t,
selectedSort,
),
[additionalMessageProps, conversations, pinnedChats, t],
[additionalMessageProps, conversations, pinnedChats, t, selectedSort],
);

const filterConversations = useCallback(
Expand Down Expand Up @@ -469,6 +494,85 @@ export const LightspeedChat = ({
setIsDrawerOpen(isOpen => !isOpen);
}, []);

const onSortToggle = useCallback(() => {
setIsSortSelectOpen(prev => !prev);
}, []);

const onSortSelect = useCallback(
(_event?: React.MouseEvent<Element>, value?: string | number) => {
handleSortChange(value as SortOption);
setIsSortSelectOpen(false);
},
[handleSortChange],
);

const getSortLabel = useCallback(
(option: SortOption): string => {
const labels: Record<SortOption, string> = {
newest: t('sort.newest'),
oldest: t('sort.oldest'),
alphabeticalAsc: t('sort.alphabeticalAsc'),
alphabeticalDesc: t('sort.alphabeticalDesc'),
};
return labels[option];
},
[t],
);

const sortDropdown = useMemo(
() => (
<Select
id="sort-select"
isOpen={isSortSelectOpen}
selected={selectedSort}
onSelect={onSortSelect}
onOpenChange={(isOpen: boolean) => setIsSortSelectOpen(isOpen)}
popperProps={{ position: 'end' }}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<Tooltip
content={`${t('sort.label')} - ${getSortLabel(selectedSort)}`}
>
<MenuToggle
ref={toggleRef}
aria-label={t('sort.label')}
variant="plain"
onClick={onSortToggle}
isExpanded={isSortSelectOpen}
>
{selectedSort === 'oldest' ||
selectedSort === 'alphabeticalDesc' ? (
<SortAmountDownAltIcon />
) : (
<SortAmountDownIcon />
)}
</MenuToggle>
</Tooltip>
)}
shouldFocusToggleOnSelect
>
<SelectList className={classes.sortDropdown}>
<SelectOption value="newest">{t('sort.newest')}</SelectOption>
<SelectOption value="oldest">{t('sort.oldest')}</SelectOption>
<SelectOption value="alphabeticalAsc">
{t('sort.alphabeticalAsc')}
</SelectOption>
<SelectOption value="alphabeticalDesc">
{t('sort.alphabeticalDesc')}
</SelectOption>
</SelectList>
</Select>
),
[
isSortSelectOpen,
selectedSort,
onSortSelect,
onSortToggle,
getSortLabel,
t,
classes.sortDropdown,
],
);

const handleAttach = (data: File[], event: DropEvent) => {
event.preventDefault();
handleFileUpload(data);
Expand Down Expand Up @@ -527,7 +631,7 @@ export const LightspeedChat = ({
handleSelectedModel={item => handleSelectedModel(item)}
models={models}
isPinningChatsEnabled={isPinningChatsEnabled}
onPinnedChatsToggle={setIsPinningChatsEnabled}
onPinnedChatsToggle={handlePinningChatsToggle}
/>
</ChatbotHeader>
<Divider />
Expand Down Expand Up @@ -560,6 +664,7 @@ export const LightspeedChat = ({
setFilterValue('');
},
}}
searchActionEnd={sortDropdown}
noResultsState={
filterValue &&
Object.keys(filterConversations(filterValue)).length === 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export * from './useIsMobile';
export * from './useLastOpenedConversation';
export * from './useLightspeedDeletePermission';
export * from './useLightspeedViewPermission';
export * from './usePinnedChatsSettings';
export * from './useTranslation';
Loading
Loading