diff --git a/workspaces/lightspeed/.changeset/hot-bottles-lie.md b/workspaces/lightspeed/.changeset/hot-bottles-lie.md new file mode 100644 index 0000000000..79906bf7a1 --- /dev/null +++ b/workspaces/lightspeed/.changeset/hot-bottles-lie.md @@ -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 diff --git a/workspaces/lightspeed/plugins/lightspeed/package.json b/workspaces/lightspeed/plugins/lightspeed/package.json index 07a2f2960c..b67487afd8 100644 --- a/workspaces/lightspeed/plugins/lightspeed/package.json +++ b/workspaces/lightspeed/plugins/lightspeed/package.json @@ -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" }, diff --git a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md index 6108a96832..14258d738d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md +++ b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md @@ -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 diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index d675b1db5c..d778bc1c0a 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -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'; @@ -47,6 +62,7 @@ import { useIsMobile, useLastOpenedConversation, useLightspeedDeletePermission, + usePinnedChatsSettings, } from '../hooks'; import { useLightspeedUpdatePermission } from '../hooks/useLightspeedUpdatePermission'; import { useTranslation } from '../hooks/useTranslation'; @@ -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'; @@ -100,6 +117,10 @@ const useStyles = makeStyles(theme => ({ maxWidth: '100%', }, }, + sortDropdown: { + padding: 0, + margin: 0, + }, })); type LightspeedChatProps = { @@ -134,14 +155,23 @@ export const LightspeedChat = ({ const [newChatCreated, setNewChatCreated] = useState(false); const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false); - const [isPinningChatsEnabled, setIsPinningChatsEnabled] = useState(true); // read from user settings in future - const [pinnedChats, setPinnedChats] = useState([]); // read from user settings in future const [targetConversationId, setTargetConversationId] = useState(''); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isRenameModalOpen, setIsRenameModalOpen] = useState(false); + const [isSortSelectOpen, setIsSortSelectOpen] = useState(false); const { isReady, lastOpenedId, setLastOpenedId, clearLastOpenedId } = useLastOpenedConversation(user); + const { + isPinningChatsEnabled, + pinnedChats, + selectedSort, + handlePinningChatsToggle, + pinChat, + unpinChat, + handleSortChange, + } = usePinnedChatsSettings(user); + const { uploadError, showAlert, @@ -159,12 +189,6 @@ export const LightspeedChat = ({ } }, [lastOpenedId, isReady]); - useEffect(() => { - if (!isPinningChatsEnabled) { - setPinnedChats([]); - } - }, [isPinningChatsEnabled]); - const queryClient = useQueryClient(); const { @@ -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( @@ -337,7 +353,15 @@ export const LightspeedChat = ({ ), }; }, - [pinnedChats, hasDeleteAccess, isPinningChatsEnabled, hasUpdateAccess, t], + [ + pinnedChats, + hasDeleteAccess, + isPinningChatsEnabled, + hasUpdateAccess, + t, + pinChat, + unpinChat, + ], ); const categorizedMessages = useMemo( @@ -347,8 +371,9 @@ export const LightspeedChat = ({ pinnedChats, additionalMessageProps, t, + selectedSort, ), - [additionalMessageProps, conversations, pinnedChats, t], + [additionalMessageProps, conversations, pinnedChats, t, selectedSort], ); const filterConversations = useCallback( @@ -469,6 +494,85 @@ export const LightspeedChat = ({ setIsDrawerOpen(isOpen => !isOpen); }, []); + const onSortToggle = useCallback(() => { + setIsSortSelectOpen(prev => !prev); + }, []); + + const onSortSelect = useCallback( + (_event?: React.MouseEvent, value?: string | number) => { + handleSortChange(value as SortOption); + setIsSortSelectOpen(false); + }, + [handleSortChange], + ); + + const getSortLabel = useCallback( + (option: SortOption): string => { + const labels: Record = { + newest: t('sort.newest'), + oldest: t('sort.oldest'), + alphabeticalAsc: t('sort.alphabeticalAsc'), + alphabeticalDesc: t('sort.alphabeticalDesc'), + }; + return labels[option]; + }, + [t], + ); + + const sortDropdown = useMemo( + () => ( + + ), + [ + isSortSelectOpen, + selectedSort, + onSortSelect, + onSortToggle, + getSortLabel, + t, + classes.sortDropdown, + ], + ); + const handleAttach = (data: File[], event: DropEvent) => { event.preventDefault(); handleFileUpload(data); @@ -527,7 +631,7 @@ export const LightspeedChat = ({ handleSelectedModel={item => handleSelectedModel(item)} models={models} isPinningChatsEnabled={isPinningChatsEnabled} - onPinnedChatsToggle={setIsPinningChatsEnabled} + onPinnedChatsToggle={handlePinningChatsToggle} /> @@ -560,6 +664,7 @@ export const LightspeedChat = ({ setFilterValue(''); }, }} + searchActionEnd={sortDropdown} noResultsState={ filterValue && Object.keys(filterConversations(filterValue)).length === 0 diff --git a/workspaces/lightspeed/plugins/lightspeed/src/hooks/index.ts b/workspaces/lightspeed/plugins/lightspeed/src/hooks/index.ts index 53da33fe59..927f91292d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/hooks/index.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/hooks/index.ts @@ -23,4 +23,5 @@ export * from './useIsMobile'; export * from './useLastOpenedConversation'; export * from './useLightspeedDeletePermission'; export * from './useLightspeedViewPermission'; +export * from './usePinnedChatsSettings'; export * from './useTranslation'; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/hooks/usePinnedChatsSettings.ts b/workspaces/lightspeed/plugins/lightspeed/src/hooks/usePinnedChatsSettings.ts new file mode 100644 index 0000000000..ec48209ac9 --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/hooks/usePinnedChatsSettings.ts @@ -0,0 +1,199 @@ +/* + * 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 { useCallback, useEffect, useState } from 'react'; + +import { storageApiRef, useApi } from '@backstage/core-plugin-api'; + +import { SortOption } from '../utils/lightspeed-chatbox-utils'; + +const BUCKET_NAME = 'lightspeed'; +const PINNED_ENABLED_KEY = 'pinnedChatsEnabled'; +const PINNED_CHATS_KEY = 'pinnedChats'; +const SORT_ORDER_KEY = 'sortOrder'; + +type UserSettings = { + [userId: string]: boolean | string[] | SortOption; +}; + +type UsePinnedChatsSettingsReturn = { + isPinningChatsEnabled: boolean; + pinnedChats: string[]; + selectedSort: SortOption; + handlePinningChatsToggle: (enabled: boolean) => void; + pinChat: (convId: string) => void; + unpinChat: (convId: string) => void; + handleSortChange: (sortOption: SortOption) => void; +}; + +/** + * Hook to manage pinned chats settings with persistence using Backstage StorageApi. + * Settings are scoped per-user to support multi-user environments. + * + * @param user - The user entity ref (e.g., "user:default/guest") + * @returns Object containing pinned chats state and management functions + */ +export const usePinnedChatsSettings = ( + user: string | undefined, +): UsePinnedChatsSettingsReturn => { + const storageApi = useApi(storageApiRef); + const bucket = storageApi.forBucket(BUCKET_NAME); + + const [isPinningChatsEnabled, setIsPinningChatsEnabled] = useState(true); + const [pinnedChats, setPinnedChats] = useState([]); + const [selectedSort, setSelectedSort] = useState('newest'); + + // Initialize from storage on mount or when user changes + useEffect(() => { + if (!user) { + setIsPinningChatsEnabled(true); + setPinnedChats([]); + setSelectedSort('newest'); + return; + } + + try { + const enabledSnapshot = bucket.snapshot(PINNED_ENABLED_KEY); + const chatsSnapshot = bucket.snapshot(PINNED_CHATS_KEY); + const sortSnapshot = bucket.snapshot(SORT_ORDER_KEY); + + const enabledData = enabledSnapshot.value ?? {}; + const chatsData = chatsSnapshot.value ?? {}; + const sortData = sortSnapshot.value ?? {}; + + setIsPinningChatsEnabled( + (enabledData[user] as boolean | undefined) ?? true, + ); + setPinnedChats((chatsData[user] as string[] | undefined) ?? []); + setSelectedSort((sortData[user] as SortOption | undefined) ?? 'newest'); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error reading pinned chats settings from storage:', error); + } + }, [bucket, user]); + + // Toggle pinning feature on/off + const handlePinningChatsToggle = useCallback( + (enabled: boolean) => { + if (!user) return; + + setIsPinningChatsEnabled(enabled); + + try { + const enabledSnapshot = + bucket.snapshot(PINNED_ENABLED_KEY); + // Create a copy to avoid mutating the read-only snapshot + const enabledData = { ...(enabledSnapshot.value ?? {}) }; + enabledData[user] = enabled; + bucket.set(PINNED_ENABLED_KEY, enabledData); + + // Clear pinned chats when disabling + if (!enabled) { + setPinnedChats([]); + const chatsSnapshot = bucket.snapshot(PINNED_CHATS_KEY); + // Create a copy to avoid mutating the read-only snapshot + const chatsData = { ...(chatsSnapshot.value ?? {}) }; + chatsData[user] = []; + bucket.set(PINNED_CHATS_KEY, chatsData); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error saving pinning toggle state:', error); + } + }, + [bucket, user], + ); + + // Pin a chat + const pinChat = useCallback( + (convId: string) => { + if (!user) return; + + setPinnedChats(prev => { + const updated = [...prev, convId]; + + try { + const chatsSnapshot = bucket.snapshot(PINNED_CHATS_KEY); + // Create a copy to avoid mutating the read-only snapshot + const chatsData = { ...(chatsSnapshot.value ?? {}) }; + chatsData[user] = updated; + bucket.set(PINNED_CHATS_KEY, chatsData); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error saving pinned chat:', error); + } + + return updated; + }); + }, + [bucket, user], + ); + + // Unpin a chat + const unpinChat = useCallback( + (convId: string) => { + if (!user) return; + + setPinnedChats(prev => { + const updated = prev.filter(id => id !== convId); + + try { + const chatsSnapshot = bucket.snapshot(PINNED_CHATS_KEY); + // Create a copy to avoid mutating the read-only snapshot + const chatsData = { ...(chatsSnapshot.value ?? {}) }; + chatsData[user] = updated; + bucket.set(PINNED_CHATS_KEY, chatsData); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error saving unpinned chat:', error); + } + + return updated; + }); + }, + [bucket, user], + ); + + // Change sort order + const handleSortChange = useCallback( + (sortOption: SortOption) => { + if (!user) return; + + setSelectedSort(sortOption); + + try { + const sortSnapshot = bucket.snapshot(SORT_ORDER_KEY); + // Create a copy to avoid mutating the read-only snapshot + const sortData = { ...(sortSnapshot.value ?? {}) }; + sortData[user] = sortOption; + bucket.set(SORT_ORDER_KEY, sortData); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error saving sort order:', error); + } + }, + [bucket, user], + ); + + return { + isPinningChatsEnabled, + pinnedChats, + selectedSort, + handlePinningChatsToggle, + pinChat, + unpinChat, + handleSortChange, + }; +}; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts index e3a9106c1e..3d5f811e3a 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts @@ -227,6 +227,13 @@ const lightspeedTranslationDe = createTranslationMessages({ 'Angeheftete Chats sind derzeit aktiviert', 'settings.pinned.disabled.description': 'Angeheftete Chats sind derzeit deaktiviert', + + // Sort options + 'sort.label': 'Konversationen sortieren', + 'sort.newest': 'Datum (neueste zuerst)', + 'sort.oldest': 'Datum (älteste zuerst)', + 'sort.alphabeticalAsc': 'Name (A-Z)', + 'sort.alphabeticalDesc': 'Name (Z-A)', }, }); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts index 13a13a2abc..1820f22bb6 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts @@ -230,6 +230,13 @@ const lightspeedTranslationEs = createTranslationMessages({ 'Los chats fijados están actualmente habilitados', 'settings.pinned.disabled.description': 'Los chats fijados están actualmente deshabilitados', + + // Sort options + 'sort.label': 'Ordenar conversaciones', + 'sort.newest': 'Fecha (más reciente primero)', + 'sort.oldest': 'Fecha (más antiguo primero)', + 'sort.alphabeticalAsc': 'Nombre (A-Z)', + 'sort.alphabeticalDesc': 'Nombre (Z-A)', }, }); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts index d2a15643aa..00488b49a3 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts @@ -231,6 +231,13 @@ const lightspeedTranslationFr = createTranslationMessages({ 'Les chats épinglés sont actuellement activés', 'settings.pinned.disabled.description': 'Les chats épinglés sont actuellement désactivés', + + // Sort options + 'sort.label': 'Trier les conversations', + 'sort.newest': 'Date (plus récent en premier)', + 'sort.oldest': 'Date (plus ancien en premier)', + 'sort.alphabeticalAsc': 'Nom (A-Z)', + 'sort.alphabeticalDesc': 'Nom (Z-A)', }, }); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts index 486e8cdf1d..1c0e5dd768 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts @@ -218,6 +218,13 @@ export const lightspeedMessages = { 'settings.pinned.disable': 'Disable pinned chats', 'settings.pinned.enabled.description': 'Pinned chats are currently enabled', 'settings.pinned.disabled.description': 'Pinned chats are currently disabled', + + // Sort options + 'sort.label': 'Sort conversations', + 'sort.newest': 'Date (newest first)', + 'sort.oldest': 'Date (oldest first)', + 'sort.alphabeticalAsc': 'Name (A-Z)', + 'sort.alphabeticalDesc': 'Name (Z-A)', }; /** diff --git a/workspaces/lightspeed/plugins/lightspeed/src/utils/lightspeed-chatbox-utils.tsx b/workspaces/lightspeed/plugins/lightspeed/src/utils/lightspeed-chatbox-utils.tsx index 9deae1d774..1c3d658faf 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/utils/lightspeed-chatbox-utils.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/utils/lightspeed-chatbox-utils.tsx @@ -189,11 +189,41 @@ export const transformDocumentsToSources = ( }; }; +export type SortOption = + | 'newest' + | 'oldest' + | 'alphabeticalAsc' + | 'alphabeticalDesc'; + +const sortConversations = ( + messages: ConversationList, + sortOption: SortOption, +): ConversationList => { + return [...messages].sort((a, b) => { + switch (sortOption) { + case 'oldest': + return a.last_message_timestamp - b.last_message_timestamp; + case 'alphabeticalAsc': + return a.topic_summary.localeCompare(b.topic_summary, undefined, { + sensitivity: 'base', + }); + case 'alphabeticalDesc': + return b.topic_summary.localeCompare(a.topic_summary, undefined, { + sensitivity: 'base', + }); + case 'newest': + default: + return b.last_message_timestamp - a.last_message_timestamp; + } + }); +}; + export const getCategorizeMessages = ( messages: ConversationList, pinnedChats: string[], addProps: (c: ConversationSummary) => { [k: string]: any }, t?: (key: string, params?: any) => string, + sortOption: SortOption = 'newest', ): { [k: string]: Conversation[] } => { const pinnedChatsKey = t?.('conversation.category.pinnedChats') || 'Pinned'; const recentKey = t?.('conversation.category.recent') || 'Recent'; @@ -201,9 +231,7 @@ export const getCategorizeMessages = ( [pinnedChatsKey]: [], [recentKey]: [], }; - const sortedMessages = [...messages].sort( - (a, b) => b.last_message_timestamp - a.last_message_timestamp, - ); + const sortedMessages = sortConversations(messages, sortOption); sortedMessages.forEach(c => { const message: Conversation = { id: c.conversation_id, diff --git a/workspaces/lightspeed/yarn.lock b/workspaces/lightspeed/yarn.lock index bba71afed5..a4a0c0f294 100644 --- a/workspaces/lightspeed/yarn.lock +++ b/workspaces/lightspeed/yarn.lock @@ -7442,27 +7442,25 @@ __metadata: languageName: node linkType: hard -"@monaco-editor/loader@npm:^1.4.0": - version: 1.4.0 - resolution: "@monaco-editor/loader@npm:1.4.0" +"@monaco-editor/loader@npm:^1.5.0": + version: 1.7.0 + resolution: "@monaco-editor/loader@npm:1.7.0" dependencies: state-local: ^1.0.6 - peerDependencies: - monaco-editor: ">= 0.21.0 < 1" - checksum: 374ec0ea872ee15b33310e105a43217148161480d3955c5cece87d0f801754cd2c45a3f6c539a75da18a066c1615756fb87eaf1003f1df6a64a0cbce5d2c3749 + checksum: e3a4f095827101216d72d0a0dcb4e4d29ae2c911a7ed59210b00e9ad48680a4132983a31170668918dd9b486c98fe97996357d58cc76fda34f3217848a19befb languageName: node linkType: hard -"@monaco-editor/react@npm:^4.6.0": - version: 4.6.0 - resolution: "@monaco-editor/react@npm:4.6.0" +"@monaco-editor/react@npm:^4.6.0, @monaco-editor/react@npm:^4.7.0": + version: 4.7.0 + resolution: "@monaco-editor/react@npm:4.7.0" dependencies: - "@monaco-editor/loader": ^1.4.0 + "@monaco-editor/loader": ^1.5.0 peerDependencies: monaco-editor: ">= 0.25.0 < 1" - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 9d44e76c5baad6db5f84c90a5540fbd3c9af691b97d76cf2a99b3c8273004d0efe44c2572d80e9d975c9af10022c21e4a66923924950a5201e82017c8b20428c + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 8b3bd8adfcd6af70dc5f965e986932269e1e2c2a0f6beb5a3c632c8c7942c1341f6086d9664f9a949983bdf4a04a706e529a93bfec3b5884642915dfcc0354c3 languageName: node linkType: hard @@ -8562,13 +8560,14 @@ __metadata: languageName: node linkType: hard -"@patternfly/chatbot@npm:6.4.1": - version: 6.4.1 - resolution: "@patternfly/chatbot@npm:6.4.1" +"@patternfly/chatbot@npm:6.5.0-prerelease.28": + version: 6.5.0-prerelease.28 + resolution: "@patternfly/chatbot@npm:6.5.0-prerelease.28" dependencies: "@patternfly/react-code-editor": ^6.1.0 "@patternfly/react-core": ^6.1.0 "@patternfly/react-icons": ^6.1.0 + "@patternfly/react-styles": ^6.1.0 "@patternfly/react-table": ^6.1.0 "@segment/analytics-next": ^1.76.0 clsx: ^2.1.0 @@ -8576,14 +8575,22 @@ __metadata: posthog-js: ^1.194.4 react-markdown: ^9.0.1 rehype-external-links: ^3.0.0 + rehype-highlight: ^7.0.0 rehype-sanitize: ^6.0.0 rehype-unwrap-images: ^1.0.0 remark-gfm: ^4.0.0 unist-util-visit: ^5.0.0 peerDependencies: + "@monaco-editor/react": ^4.7.0 + monaco-editor: ^0.54.0 react: ^18 || ^19 react-dom: ^18 || ^19 - checksum: ca0526885670b0c7d4dce2d204da4f323c59bdd1df449fecd3f15fbf4a0e189252e7e58c6e186dce7f1749004f520d7869846866d739724330dbc8c59ee8152e + peerDependenciesMeta: + "@monaco-editor/react": + optional: false + monaco-editor: + optional: false + checksum: 7e3c3ac13fc2a459ec93d45d448808debe3ac90879ffe889d417042fb880cf4151946dd12682441e9be1811f32c5de44705a275f711c2323c69c9fcf892391f9 languageName: node linkType: hard @@ -11063,9 +11070,10 @@ __metadata: "@ianvs/prettier-plugin-sort-imports": ^4.4.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:^" @@ -11075,6 +11083,7 @@ __metadata: "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^15.0.0 "@testing-library/user-event": 14.6.1 + monaco-editor: ^0.54.0 msw: 1.3.5 prettier: 3.6.2 react: 16.13.1 || ^17.0.0 || ^18.0.0 @@ -19182,6 +19191,13 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:3.1.7": + version: 3.1.7 + resolution: "dompurify@npm:3.1.7" + checksum: 0a9b811bbc94f3dba60cf6486962362b0f1a5b4ab789f5e1cbd4749b6ba1a1fad190a677a962dc8850ce28764424765fe425e9d6508e4e93ba648ef15d54bc24 + languageName: node + linkType: hard + "dompurify@npm:=3.2.6": version: 3.2.6 resolution: "dompurify@npm:3.2.6" @@ -22085,6 +22101,18 @@ __metadata: languageName: node linkType: hard +"hast-util-to-text@npm:^4.0.0": + version: 4.0.2 + resolution: "hast-util-to-text@npm:4.0.2" + dependencies: + "@types/hast": ^3.0.0 + "@types/unist": ^3.0.0 + hast-util-is-element: ^3.0.0 + unist-util-find-after: ^5.0.0 + checksum: 72cce08666b86511595d3eef52236b86897cfbac166f2a0752b70b16d1f590b5aa91ea1a553c0d1603f9e0c7e373ceacab381be3d8f176129ad6e301d2a56d94 + languageName: node + linkType: hard + "hast-util-whitespace@npm:^2.0.0": version: 2.0.1 resolution: "hast-util-whitespace@npm:2.0.1" @@ -22178,6 +22206,13 @@ __metadata: languageName: node linkType: hard +"highlight.js@npm:~11.11.0": + version: 11.11.1 + resolution: "highlight.js@npm:11.11.1" + checksum: 841ddd329a92be123a61ef4051698f824c3575deef588fbb810bd638f1575ddc96b7bdd925b97c05a29276bb119dfcb8e5bcb0acb31c86249371bf046e131d72 + languageName: node + linkType: hard + "highlightjs-vue@npm:^1.0.0": version: 1.0.0 resolution: "highlightjs-vue@npm:1.0.0" @@ -25586,6 +25621,17 @@ __metadata: languageName: node linkType: hard +"lowlight@npm:^3.0.0": + version: 3.3.0 + resolution: "lowlight@npm:3.3.0" + dependencies: + "@types/hast": ^3.0.0 + devlop: ^1.0.0 + highlight.js: ~11.11.0 + checksum: 0903db036d938d7bd4a7ce9b55f9e4cd02c46b932d632f512540d700fb0f61a689553231f6d15bc6ebf8a7372c58254f7efc3f18a7d4cb328ee366290a694801 + languageName: node + linkType: hard + "lru-cache@npm:@wolfy1339/lru-cache@^11.0.2-patch.1": version: 11.0.2-patch.1 resolution: "@wolfy1339/lru-cache@npm:11.0.2-patch.1" @@ -25804,6 +25850,15 @@ __metadata: languageName: node linkType: hard +"marked@npm:14.0.0": + version: 14.0.0 + resolution: "marked@npm:14.0.0" + bin: + marked: bin/marked.js + checksum: 965405cde11d180e5da78cf51074b1947f1e5483ed99691d3ec990a078aa6ca9113cf19bbd44da45473d0e7eec7298255e7d860a650dd5e7aed78ca5a8e0161b + languageName: node + linkType: hard + "marked@npm:^4.0.14": version: 4.3.0 resolution: "marked@npm:4.3.0" @@ -27401,6 +27456,16 @@ __metadata: languageName: node linkType: hard +"monaco-editor@npm:^0.54.0": + version: 0.54.0 + resolution: "monaco-editor@npm:0.54.0" + dependencies: + dompurify: 3.1.7 + marked: 14.0.0 + checksum: 27bda0afafe5bbce693be9c72531e772bf71cfce6362a8358acd28815ddb95113cc1738ddccb28359a9d9def00da4dd928c1f32514dc0d1611ba0251758c3e2a + languageName: node + linkType: hard + "moo@npm:^0.5.0": version: 0.5.2 resolution: "moo@npm:0.5.2" @@ -31414,6 +31479,19 @@ __metadata: languageName: node linkType: hard +"rehype-highlight@npm:^7.0.0": + version: 7.0.2 + resolution: "rehype-highlight@npm:7.0.2" + dependencies: + "@types/hast": ^3.0.0 + hast-util-to-text: ^4.0.0 + lowlight: ^3.0.0 + unist-util-visit: ^5.0.0 + vfile: ^6.0.0 + checksum: 1382766418e3cd5fb21a4a1aced577cb6472058de958ceb1ebce763a17321786ef0658ecd34bdf096f0733973dbcb284653302b6ee7bd775442c0de07291000d + languageName: node + linkType: hard + "rehype-sanitize@npm:^6.0.0": version: 6.0.0 resolution: "rehype-sanitize@npm:6.0.0" @@ -34953,6 +35031,16 @@ __metadata: languageName: node linkType: hard +"unist-util-find-after@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-find-after@npm:5.0.0" + dependencies: + "@types/unist": ^3.0.0 + unist-util-is: ^6.0.0 + checksum: e64bd5ebee7ac021cf990bf33e9ec29fc6452159187d4a7fa0f77334bea8e378fea7a7fb0bcf957300b2ffdba902ff25b62c165fc8b86309613da35ad793ada0 + languageName: node + linkType: hard + "unist-util-generated@npm:^2.0.0": version: 2.0.1 resolution: "unist-util-generated@npm:2.0.1"