diff --git a/locales/en/plugin__lightspeed-console-plugin.json b/locales/en/plugin__lightspeed-console-plugin.json index 583d51d0..ab09b90e 100644 --- a/locales/en/plugin__lightspeed-console-plugin.json +++ b/locales/en/plugin__lightspeed-console-plugin.json @@ -17,11 +17,15 @@ "Configure events attachment": "Configure events attachment", "Configure log attachment": "Configure log attachment", "Confirm chat deletion": "Confirm chat deletion", + "Conversation history": "Conversation history", "Conversation history has been truncated to fit within context window.": "Conversation history has been truncated to fit within context window.", + "Conversation options": "Conversation options", + "Conversations": "Conversations", "Copied": "Copied", "Copy conversation": "Copy conversation", "Copy to clipboard": "Copy to clipboard", "Currently viewing": "Currently viewing", + "Delete": "Delete", "Dismiss": "Dismiss", "Do not include personal information or other sensitive information in your feedback. Feedback may be used to improve Red Hat's products or services.": "Do not include personal information or other sensitive information in your feedback. Feedback may be used to improve Red Hat's products or services.", "Do you want to leave this page?": "Do you want to leave this page?", @@ -62,6 +66,7 @@ "Minimize": "Minimize", "Most recent {{lines}} lines": "Most recent {{lines}} lines", "Most recent {{numEvents}} events": "Most recent {{numEvents}} events", + "New chat": "New chat", "No events": "No events", "No logs found": "No logs found", "No output returned": "No output returned", @@ -69,6 +74,7 @@ "No pods found for {{kind}} {{name}}": "No pods found for {{kind}} {{name}}", "Not authenticated": "Not authenticated", "Not authorized": "Not authorized", + "Older": "Older", "OpenShift Lightspeed authentication failed. Contact your system administrator for more information.": "OpenShift Lightspeed authentication failed. Contact your system administrator for more information.", "OpenShift Lightspeed chat history": "OpenShift Lightspeed chat history", "OpenShift Lightspeed is now available to help you with your OpenShift questions and tasks. Try asking about deployments, troubleshooting, best practices, or any other OpenShift-related topics. This notice will disappear once you minimize the chat.": "OpenShift Lightspeed is now available to help you with your OpenShift questions and tasks. Try asking about deployments, troubleshooting, best practices, or any other OpenShift-related topics. This notice will disappear once you minimize the chat.", @@ -76,15 +82,20 @@ "Please retry or contact support if the issue persists.": "Please retry or contact support if the issue persists.", "Preview attachment": "Preview attachment", "Preview attachment - modified": "Preview attachment - modified", + "Previous 30 days": "Previous 30 days", + "Previous 7 days": "Previous 7 days", "Red Hat OpenShift Lightspeed": "Red Hat OpenShift Lightspeed", "Revert to original": "Revert to original", "Save": "Save", + "Search conversations": "Search conversations", + "Search conversations...": "Search conversations...", "Send a message...": "Send a message...", "Stay": "Stay", "Submit": "Submit", "The following output was generated when running <2>{{name}} with arguments <5>{{argsFormatted}}.": "The following output was generated when running <2>{{name}} with arguments <5>{{argsFormatted}}.", "The following output was generated when running <2>{{name}} with no arguments.": "The following output was generated when running <2>{{name}} with no arguments.", "The OpenShift Lightspeed service is not yet ready to receive requests. If this message persists, please check the OLSConfig.": "The OpenShift Lightspeed service is not yet ready to receive requests. If this message persists, please check the OLSConfig.", + "Today": "Today", "Tool output": "Tool output", "Total size of attachments exceeds {{max}} characters.": "Total size of attachments exceeds {{max}} characters.", "Upload from computer": "Upload from computer", diff --git a/src/components/GeneralPage.tsx b/src/components/GeneralPage.tsx index 31e0acda..011bbbdc 100644 --- a/src/components/GeneralPage.tsx +++ b/src/components/GeneralPage.tsx @@ -4,12 +4,22 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk'; -import { Alert, Badge, Button, ExpandableSection, Title, Tooltip } from '@patternfly/react-core'; +import { + Alert, + Badge, + Button, + DropdownItem, + DropdownList, + ExpandableSection, + Title, + Tooltip, +} from '@patternfly/react-core'; import { CheckIcon, CompressIcon, ExpandIcon, ExternalLinkAltIcon, + OutlinedCommentsIcon, OutlinedCopyIcon, TrashIcon, WindowMinimizeIcon, @@ -17,6 +27,7 @@ import { import { Chatbot, ChatbotContent, + ChatbotConversationHistoryNav, ChatbotDisplayMode, ChatbotFooter, ChatbotFootnote, @@ -24,6 +35,7 @@ import { ChatbotHeaderActions, ChatbotHeaderMain, ChatbotHeaderTitle, + type Conversation, Message, MessageBox, type SourcesCardProps, @@ -32,6 +44,13 @@ import { import { toOLSAttachment } from '../attachments'; import { getApiUrl } from '../config'; import { copyToClipboard } from '../clipboard'; +import { + deleteConversation, + fetchConversation, + fetchConversationsList, + setLastConversationId, + transformChatHistory, +} from '../conversations'; import { ErrorType, getFetchErrorMessage } from '../error'; import { AuthStatus, getRequestInitWithAuthHeader, useAuth } from '../hooks/useAuth'; import { useBoolean } from '../hooks/useBoolean'; @@ -40,13 +59,17 @@ import { useIsDarkTheme } from '../hooks/useIsDarkTheme'; import { attachmentsClear, chatHistoryClear, + chatHistoryPush, + removeConversation, setConversationID, + setConversations, + setIsConversationsLoading, userFeedbackClose, userFeedbackOpen, userFeedbackSetSentiment, } from '../redux-actions'; import { State } from '../redux-reducers'; -import { Attachment, ChatEntry, ReferencedDoc } from '../types'; +import { Attachment, ChatEntry, ConversationSummary, ReferencedDoc } from '../types'; import AttachmentLabel from './AttachmentLabel'; import AttachmentsSizeAlert from './AttachmentsSizeAlert'; import ImportAction from './ImportAction'; @@ -414,6 +437,50 @@ type GeneralPageProps = { onExpand?: () => void; }; +const groupConversationsByDate = ( + conversations: ConversationSummary[], + t: (key: string) => string, +): { [key: string]: Conversation[] } => { + const now = Date.now(); + const oneDay = 86400000; + const sevenDays = 7 * oneDay; + const thirtyDays = 30 * oneDay; + + const groups: { [key: string]: Conversation[] } = {}; + const todayLabel = t('Today'); + const last7Label = t('Previous 7 days'); + const last30Label = t('Previous 30 days'); + const olderLabel = t('Older'); + + const sorted = [...conversations].sort( + (a, b) => b.last_message_timestamp - a.last_message_timestamp, + ); + + for (const conv of sorted) { + const age = now - conv.last_message_timestamp * 1000; + let label: string; + if (age < oneDay) { + label = todayLabel; + } else if (age < sevenDays) { + label = last7Label; + } else if (age < thirtyDays) { + label = last30Label; + } else { + label = olderLabel; + } + + if (!groups[label]) { + groups[label] = []; + } + groups[label].push({ + id: conv.conversation_id, + text: conv.topic_summary || t('Conversation'), + }); + } + + return groups; +}; + const GeneralPage: React.FC = ({ ariaLabel, className, @@ -431,14 +498,26 @@ const GeneralPage: React.FC = ({ const conversationID: string = useSelector((s: State) => s.plugins?.ols?.get('conversationID')); + const conversations: ConversationSummary[] = useSelector((s: State) => + s.plugins?.ols?.get('conversations'), + ); + + const isConversationsLoading: boolean = useSelector((s: State) => + s.plugins?.ols?.get('isConversationsLoading'), + ); + const [authStatus] = useAuth(); const [isFirstTimeUser] = useFirstTimeUser(); + const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); const [isNewChatModalOpen, , openNewChatModal, closeNewChatModal] = useBoolean(false); const [isCopied, , setCopied, setNotCopied] = useBoolean(false); + const [drawerFilter, setDrawerFilter] = React.useState(''); const chatHistoryEndRef = React.useRef(null); + const displayMode = onCollapse ? ChatbotDisplayMode.fullscreen : ChatbotDisplayMode.default; + const scrollIntoView = React.useCallback((behavior = 'smooth') => { defer(() => { chatHistoryEndRef?.current?.scrollIntoView({ behavior }); @@ -451,17 +530,124 @@ const GeneralPage: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const loadConversationsList = React.useCallback(() => { + dispatch(setIsConversationsLoading(true)); + fetchConversationsList() + .then((response) => { + dispatch(setConversations(response.conversations || [])); + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.error('Failed to load conversations list:', err); + }) + .finally(() => { + dispatch(setIsConversationsLoading(false)); + }); + }, [dispatch]); + const clearChat = React.useCallback(() => { dispatch(setConversationID(null)); dispatch(chatHistoryClear()); dispatch(attachmentsClear()); + setLastConversationId(null); }, [dispatch]); + const onNewChat = React.useCallback(() => { + clearChat(); + setIsDrawerOpen(false); + }, [clearChat]); + const onConfirmNewChat = React.useCallback(() => { clearChat(); closeNewChatModal(); }, [clearChat, closeNewChatModal]); + const loadConversation = React.useCallback( + (id: string) => { + dispatch(chatHistoryClear()); + dispatch(attachmentsClear()); + fetchConversation(id) + .then((detail) => { + const entries = transformChatHistory(detail); + dispatch(setConversationID(id)); + setLastConversationId(id); + entries.forEach((entry: ChatEntry) => { + dispatch(chatHistoryPush(entry)); + }); + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.error('Failed to load conversation:', err); + }); + }, + [dispatch], + ); + + const onSelectConversation = React.useCallback( + (_event?: React.MouseEvent, itemId?: string | number) => { + if (itemId && itemId !== conversationID) { + loadConversation(String(itemId)); + } + setIsDrawerOpen(false); + }, + [conversationID, loadConversation], + ); + + const onDeleteConversation = React.useCallback( + (id: string) => { + deleteConversation(id) + .then(() => { + dispatch(removeConversation(id)); + if (id === conversationID) { + clearChat(); + } + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.error('Failed to delete conversation:', err); + }); + }, + [clearChat, conversationID, dispatch], + ); + + const onDrawerToggle = React.useCallback(() => { + const willOpen = !isDrawerOpen; + setIsDrawerOpen(willOpen); + if (willOpen) { + loadConversationsList(); + } + }, [isDrawerOpen, loadConversationsList]); + + const filteredConversations = React.useMemo(() => { + if (!drawerFilter) { + return conversations; + } + const lower = drawerFilter.toLowerCase(); + return conversations.filter( + (c) => + c.topic_summary?.toLowerCase().includes(lower) || + c.conversation_id.toLowerCase().includes(lower), + ); + }, [conversations, drawerFilter]); + + const groupedConversations = React.useMemo(() => { + const groups = groupConversationsByDate(filteredConversations, t); + for (const key of Object.keys(groups)) { + groups[key] = groups[key].map((conv) => ({ + ...conv, + menuItems: ( + + onDeleteConversation(conv.id)} value="delete"> + {t('Delete')} + + + ), + label: t('Conversation options'), + })); + } + return groups; + }, [filteredConversations, onDeleteConversation, t]); + const copyConversation = React.useCallback(async () => { try { const chatEntries = chatHistory.toJS(); @@ -489,13 +675,65 @@ const GeneralPage: React.FC = ({ } }, [chatHistory, setCopied, setNotCopied]); + const chatBodyContent = ( + <> + {/* @ts-expect-error: TS2786 */} + + {/* @ts-expect-error: TS2786 */} + +
+ + {t( + 'Explore deeper insights, engage in meaningful discussions, and unlock new possibilities with Red Hat OpenShift Lightspeed. Answers are provided by generative AI technology, please use appropriate caution when following recommendations.', + )} + + + + {isFirstTimeUser && } + {chatHistory + .map((entry, i) => ( + + )) + .toArray()} + + +
+ + + + {authStatus !== AuthStatus.NotAuthenticated && authStatus !== AuthStatus.NotAuthorized && ( + // @ts-expect-error: TS2786 + + + {/* @ts-expect-error: TS2786 */} + +
+ {t('For questions or feedback about OpenShift Lightspeed,')}{' '} + + email the Red Hat team + +
+ +
+ )} + + ); + return ( // @ts-expect-error: TS2786 {/* @ts-expect-error: TS2786 */} @@ -508,6 +746,15 @@ const GeneralPage: React.FC = ({ {/* @ts-expect-error: TS2786 */} + +