diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css new file mode 100644 index 00000000..97e1c7ba --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css @@ -0,0 +1,102 @@ +/* Version History Panel Styles */ +.version-history-panel { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + height: 100%; + padding: var(--pf-t--global--spacer--lg); + background-color: var(--pf-t--global--background--color--primary--default); + box-sizing: border-box; +} + +.version-history-header { + padding-bottom: var(--pf-t--global--spacer--md); + border-bottom: 1px solid var(--pf-t--global--border--color--default); + flex-shrink: 0; +} + +.version-history-content { + flex: 1; + min-height: 0; + overflow: auto; +} + +/* Remove top border from first data list item */ +.version-history-panel .pf-v6-c-data-list { + border-top: none; +} + +.version-history-panel .pf-v6-c-data-list__item:first-child { + border-block-start: none; +} + +/* Option 2: Split-screen layout styles */ +.version-history-split-screen-demo { + display: flex; + height: 100vh; + width: 100%; + background-color: var(--pf-t--global--background--color--secondary--default); +} + +.version-history-split-canvas { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; +} + +.version-history-split-canvas .pf-chatbot__canvas-section { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--pf-t--global--background--color--secondary--default); +} + +.version-history-split-canvas .pf-chatbot__canvas-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--pf-t--global--spacer--md); + padding-top: var(--pf-t--global--spacer--lg); + padding-bottom: var(--pf-t--global--spacer--md); + background-color: var(--pf-t--global--background--color--secondary--default); + flex-shrink: 0; +} + +.pf-chatbot__canvas-head .pf-v6-c-drawer__actions { + gap: var(--pf-t--global--spacer--md); +} + +.version-history-split-canvas .pf-chatbot__canvas-panel-body { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + padding: var(--pf-t--global--spacer--md); + min-height: 0; +} + +.version-history-split-canvas .pf-chatbot__canvas-editor { + flex: 1; + overflow: hidden; + border-radius: var(--pf-t--global--border--radius--medium); + min-height: 0; +} + +.version-history-split-canvas .pf-v6-c-code-editor { + height: 100%; +} + +.version-history-split-panel { + width: 400px; + flex-shrink: 0; + overflow: hidden; + border-left: 1px solid var(--pf-t--global--border--color--default); + background-color: var(--pf-t--global--background--color--primary--default); +} + +.version-history-split-panel .version-history-panel { + height: 100%; +} diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md new file mode 100644 index 00000000..e524afda --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -0,0 +1,125 @@ +--- +id: Version history +section: patterns +source: demo +--- + +import { useState, useRef, useCallback, useEffect, FunctionComponent, ReactNode } from 'react'; +import { +Brand, +Button, +Divider, +Drawer, +DrawerActions, +DrawerContent, +DrawerContentBody, +DrawerHead, +DrawerPanelContent, +Dropdown, +DropdownItem, +DropdownList, +Flex, +FlexItem, +Label, +MenuToggle, +MenuToggleAction, +Modal, +ModalBody, +ModalFooter, +ModalHeader, +Popover, +Select, +SelectList, +SelectOption, +Title, +Tooltip, +TextInput, +DataList, +DataListItem, +DataListItemRow, +DataListItemCells, +DataListCell, +DataListAction, +Content, +ContentVariants +} from '@patternfly/react-core'; +import { CodeEditor, CodeEditorControl, Language } from '@patternfly/react-code-editor'; +import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; +import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent'; +import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter'; +import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar'; +import MessageBox from '@patternfly/chatbot/dist/dynamic/MessageBox'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; +import ChatbotHeader, { +ChatbotHeaderActions, +ChatbotHeaderCloseButton, +ChatbotHeaderMain, +ChatbotHeaderMenu, +ChatbotHeaderTitle +} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; +import RhUiAiInfoIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-info-icon'; +import RhUiImageFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-image-fill-icon'; +import RhUiAddIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-icon'; +import RhUiTaskFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-task-fill-icon'; +import RhUiHistoryIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-history-icon'; +import RhUiSaveIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-save-icon'; +import CheckIcon from '@patternfly/react-icons/dist/esm/icons/check-icon'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import RhUiRedoIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-redo-icon'; +import RhUiUndoIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-undo-icon'; +import RhUiServerUploadFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-server-upload-fill-icon'; +import RhUiNotificationFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-notification-fill-icon'; +import RhUiCalendarFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-calendar-fill-icon'; +import RhMicronsCloseIcon from '@patternfly/react-icons/dist/esm/icons/rh-microns-close-icon'; +import { useDropzone } from 'react-dropzone'; +import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; +import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; +import userAvatar from '../Messages/user_avatar.svg'; + +**Version history** allows users to view, compare, and restore previous versions of AI-generated or user-edited content in canvas mode. This pattern is essential for collaborative AI workflows where users need to track changes over time and recover earlier states of their work. It is recommended to use version history and canvas mode in full width and fullscreen layouts only. + +## Use cases + +Version history is useful when: +- Users need to track changes made by both AI and humans over time +- Users want to restore a previous version of their work +- Users need to compare different iterations of AI-generated content +- Teams collaborate on AI-assisted documents or code + +## Version list features + +Each version in the list displays: +- **Version name**: A user editable name for each version +- **Author**: Who created the version (user or AI bot) +- **Timestamp**: Meta data that displays relative time since the version was created +- **AI label**: A badge indicating AI-generated edits + +Version actions include: +- **Name this version**: Rename a version for easier identification +- **Restore this version**: Revert the canvas to a past version's content +- **Duplicate**: Create a copy of the version + +## Class names + +This demo reuses the [canvas](/patterns/canvas) `pf-chatbot__canvas*` classes from `@patternfly/chatbot` for the canvas and editor chrome. The classes below are defined in this demo's stylesheet (`VersionHistory.css`) and are used for the version history panel and split-screen layout. They are not exported from `@patternfly/chatbot`. + +| Class name | Purpose | +| ----------------------------------- | ----------------------------------------------------------------------- | +| `version-history-panel` | Flex column container for the version history panel and its spacing. | +| `version-history-header` | Styles the panel header and its bottom border. | +| `version-history-content` | Scrollable area that holds the version list. | +| `version-history-split-screen-demo` | Full-viewport flex row that hosts the split-screen layout. | +| `version-history-split-canvas` | Left pane that contains the read-only canvas preview. | +| `version-history-split-panel` | Fixed-width right pane that contains the version history panel. | + +## Demo + +### Fullscreen version history + +This demo shows a read-only code preview (canvas view) on the left and version history list on the right, utilizing a split-screen layout. Open the version history with the split menu toggle in the canvas panel header (next to close)—click the main button or **Show full history** in the dropdown. The chatbot panel is automatically hidden when the version history toggle is triggered. Close the version history with the X icon button to return to the chatbot and canvas. + +```js file="./VersionHistorySplitScreen.tsx" isFullscreen + +``` diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx new file mode 100644 index 00000000..c83635f2 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx @@ -0,0 +1,1199 @@ +import React, { useState, useRef, useCallback, useEffect, FunctionComponent, ReactNode } from 'react'; +import { + Brand, + Button, + Divider, + Drawer, + DrawerActions, + DrawerContent, + DrawerContentBody, + DrawerHead, + DrawerPanelContent, + Dropdown, + DropdownItem, + DropdownList, + Flex, + FlexItem, + Label, + MenuToggle, + MenuToggleAction, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + Popover, + Select, + SelectList, + SelectOption, + Title, + Tooltip, + TextInput, + DataList, + DataListItem, + DataListItemRow, + DataListItemCells, + DataListCell, + DataListAction, + Content, + ContentVariants +} from '@patternfly/react-core'; +import { CodeEditor, CodeEditorControl, Language } from '@patternfly/react-code-editor'; +import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; +import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent'; +import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter'; +import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar'; +import MessageBox from '@patternfly/chatbot/dist/dynamic/MessageBox'; +import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message'; +import ChatbotConversationHistoryNav, { + Conversation +} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; +import ChatbotHeader, { + ChatbotHeaderActions, + ChatbotHeaderCloseButton, + ChatbotHeaderMain, + ChatbotHeaderMenu, + ChatbotHeaderTitle +} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; +import { RhUiAiInfoIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-info-icon'; +import { RhUiImageFillIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-image-fill-icon'; +import { RhUiAddIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-icon'; +import { RhUiTaskFillIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-task-fill-icon'; +import { RhUiHistoryIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-history-icon'; +import { RhUiSaveIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-save-icon'; +import { CheckIcon } from '@patternfly/react-icons/dist/esm/icons/check-icon'; +import { TimesIcon } from '@patternfly/react-icons/dist/esm/icons/times-icon'; +import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import { RhUiRedoIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-redo-icon'; +import { RhUiUndoIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-undo-icon'; +import { RhUiServerUploadFillIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-server-upload-fill-icon'; +import { RhUiNotificationFillIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-notification-fill-icon'; +import { RhUiCalendarFillIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-calendar-fill-icon'; +import { RhMicronsCloseIcon } from '@patternfly/react-icons/dist/esm/icons/rh-microns-close-icon'; +import { useDropzone } from 'react-dropzone'; +import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; +import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; +import userAvatar from '../Messages/user_avatar.svg'; +import '@patternfly/react-core/dist/styles/base.css'; +import '@patternfly/react-styles/css/components/InlineEdit/inline-edit.css'; +import '@patternfly/chatbot/dist/css/main.css'; +import './VersionHistory.css'; + +const sampleCode = `apiVersion: v1 +kind: ConfigMap +metadata: + name: canvas-demo +data: + greeting: Hello, how can I help you today? +`; + +const date = new Date(); + +const initialMessages: MessageProps[] = [ + { + id: '1', + role: 'bot', + content: 'How can I help?', + name: 'Bot', + timestamp: date.toLocaleString() + }, + { + id: '2', + role: 'user', + content: 'I want to live edit YAML code using canvas mode', + name: 'You', + avatar: userAvatar, + avatarProps: { isBordered: true }, + timestamp: date.toLocaleString() + }, + { + id: '3', + role: 'bot', + content: 'The canvas mode text editor is ready. Edit, share or ask me to change something for you.', + name: 'Bot', + timestamp: date.toLocaleString(), + attachments: [{ name: 'canvas mode.yaml', id: 'canvas-mode' }] + } +]; + +const initialConversations = { + Today: [{ id: '1', text: 'I want to live edit YAML code using canvas mode' }], + 'This month': [ + { id: '2', text: 'Enterprise Linux installation and setup' }, + { id: '3', text: 'Troubleshoot system crash' } + ] +}; + +const footnoteProps = { + label: 'Always review AI-generated content prior to use.' +}; + +const modelOptions = ['GPT-4', 'GPT-3.5', 'Claude', 'Llama 2']; + +interface CanvasEditor { + trigger: (source: string, handlerId: string, payload?: unknown) => void; +} + +interface Version { + id: string; + timestamp: Date; + author: 'user' | 'bot'; + authorName: string; + code: string; + name: string; +} + +const initialVersions: Version[] = [ + { + id: 'v1', + timestamp: new Date(Date.now() - 3600000 * 2), + author: 'bot', + authorName: 'Bot', + code: `apiVersion: v1 +kind: ConfigMap +metadata: + name: canvas-demo +data: + greeting: Hello, how can I help you today? +`, + name: 'Initial configuration' + }, + { + id: 'v2', + timestamp: new Date(Date.now() - 3600000), + author: 'user', + authorName: 'You', + code: `apiVersion: v1 +kind: ConfigMap +metadata: + name: canvas-demo + labels: + app: my-app +data: + greeting: Hello, how can I help you today? + environment: development +`, + name: 'Added labels and environment' + }, + { + id: 'v3', + timestamp: new Date(Date.now() - 1800000), + author: 'bot', + authorName: 'Bot', + code: `apiVersion: v1 +kind: ConfigMap +metadata: + name: canvas-demo + labels: + app: my-app + version: "1.0" +data: + greeting: Hello, how can I help you today? + environment: development + logLevel: info +`, + name: 'Bot added version label and logLevel' + } +]; + +const iconLogo = ( + <> + + + +); + +export const VersionHistorySplitScreen: FunctionComponent = () => { + const [messages, setMessages] = useState(initialMessages); + const [announcement, setAnnouncement] = useState(); + const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false); + const [isAttachMenuOpen, setIsAttachMenuOpen] = useState(false); + const [isModelSelectOpen, setIsModelSelectOpen] = useState(false); + const [selectedModel, setSelectedModel] = useState('GPT-4'); + const [showCanvasLabel, setShowCanvasLabel] = useState(true); + const [isCanvasOpen, setIsCanvasOpen] = useState(true); + const [isGeneratedAiPopoverOpen, setIsGeneratedAiPopoverOpen] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [chatbotVisible, setChatbotVisible] = useState(true); + const [conversations, setConversations] = useState( + initialConversations + ); + const [code, setCode] = useState(sampleCode); + const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(false); + const [versions, setVersions] = useState(initialVersions); + const [selectedVersionId, setSelectedVersionId] = useState(null); + const [editingVersionId, setEditingVersionId] = useState(null); + const [editingName, setEditingName] = useState(''); + const [openMenuVersionId, setOpenMenuVersionId] = useState(null); + const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false); + const [versionToRestore, setVersionToRestore] = useState(null); + const [previewCode, setPreviewCode] = useState(null); + const [isVersionHistoryMenuOpen, setIsVersionHistoryMenuOpen] = useState(false); + const [canvasTitle, setCanvasTitle] = useState('Edit code'); + const [isEditingCanvasTitle, setIsEditingCanvasTitle] = useState(false); + const [editingCanvasTitle, setEditingCanvasTitle] = useState('Edit code'); + + const scrollToBottomRef = useRef(null); + const historyRef = useRef(null); + const editorRef = useRef(null); + const messageInputRef = useRef(null); + const messageActionsRef = useRef(null); + const canvasCloseButtonRef = useRef(null); + const canvasSectionRef = useRef(null); + const wasCanvasOpen = useRef(isCanvasOpen); + const shouldFocusCanvasCloseButton = useRef(false); + const displayMode = ChatbotDisplayMode.fullscreen; + + const { open, getInputProps } = useDropzone({ + multiple: true, + // eslint-disable-next-line no-console + onDropAccepted: () => console.log('fileUploaded') + }); + + const generateId = () => { + const id = Date.now() + Math.random(); + return id.toString(); + }; + + const handleSend = useCallback( + (input: string | number) => { + const inputStr = String(input); + const newDate = new Date(); + const newMessages: MessageProps[] = []; + messages.forEach((message) => newMessages.push(message)); + newMessages.push({ + avatar: userAvatar, + avatarProps: { isBordered: true }, + id: generateId(), + name: 'You', + role: 'user', + content: inputStr, + timestamp: `${newDate?.toLocaleDateString()} ${newDate?.toLocaleTimeString()}` + }); + newMessages.push({ + id: generateId(), + name: 'Bot', + role: 'bot', + timestamp: `${newDate?.toLocaleDateString()} ${newDate?.toLocaleTimeString()}`, + isLoading: true + }); + setMessages(newMessages); + setAnnouncement(`Message from You: ${inputStr}. Message from Bot is loading.`); + setIsSendButtonDisabled(true); + + setTimeout(() => { + const loadedMessages: MessageProps[] = []; + newMessages.forEach((message) => loadedMessages.push(message)); + loadedMessages.pop(); + loadedMessages.push({ + id: generateId(), + role: 'bot', + content: 'API response from Bot goes here', + name: 'Bot', + isLoading: false, + actions: { + // eslint-disable-next-line no-console + positive: { onClick: () => console.log('Good response') }, + // eslint-disable-next-line no-console + negative: { onClick: () => console.log('Bad response') }, + // eslint-disable-next-line no-console + copy: { onClick: () => console.log('Copy') }, + // eslint-disable-next-line no-console + download: { onClick: () => console.log('Download') }, + // eslint-disable-next-line no-console + listen: { onClick: () => console.log('Listen') } + }, + timestamp: newDate.toLocaleString() + }); + setMessages(loadedMessages); + setAnnouncement('Message from Bot: API response goes here'); + setIsSendButtonDisabled(false); + }, 5000); + }, + [messages] + ); + + useEffect(() => { + if (messages.length > 3) { + scrollToBottomRef.current?.scrollIntoView(); + } + }, [messages]); + + const onModelSelect = ( + _event: React.MouseEvent | undefined, + value: string | number | undefined + ) => { + setSelectedModel(value as string); + setIsModelSelectOpen(false); + }; + + const openCanvas = () => { + setShowCanvasLabel(true); + setIsCanvasOpen(true); + }; + + const closeCanvasMode = () => { + setShowCanvasLabel(false); + setIsCanvasOpen(false); + window.requestAnimationFrame(() => messageInputRef.current?.focus()); + }; + + const focusCanvasSection = () => { + canvasSectionRef.current?.focus(); + }; + + useEffect(() => { + const wasClosed = !wasCanvasOpen.current; + wasCanvasOpen.current = isCanvasOpen; + + if (isCanvasOpen && wasClosed) { + shouldFocusCanvasCloseButton.current = true; + } else if (!isCanvasOpen) { + shouldFocusCanvasCloseButton.current = false; + } + }, [isCanvasOpen]); + + const onCanvasExpand = () => { + if (shouldFocusCanvasCloseButton.current) { + shouldFocusCanvasCloseButton.current = false; + focusCanvasSection(); + } + }; + + const focusMessageActions = () => { + window.requestAnimationFrame(() => messageActionsRef.current?.focus()); + }; + + const handleGeneratedAiLabelKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + (event.currentTarget as HTMLElement).click(); + } + }; + + const openVersionHistory = () => { + setIsVersionHistoryOpen(true); + const sortedVersions = [...versions].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + if (sortedVersions.length > 0) { + setSelectedVersionId(sortedVersions[0].id); + setPreviewCode(sortedVersions[0].code); + } else { + setSelectedVersionId(null); + setPreviewCode(null); + } + }; + + const closeVersionHistory = () => { + setIsVersionHistoryOpen(false); + setSelectedVersionId(null); + setEditingVersionId(null); + setOpenMenuVersionId(null); + setPreviewCode(null); + }; + + const handleVersionSelect = (versionId: string) => { + setSelectedVersionId(versionId); + const version = versions.find((v) => v.id === versionId); + if (version) { + setPreviewCode(version.code); + } + }; + + const handleRestoreVersion = (versionId: string) => { + const version = versions.find((v) => v.id === versionId); + if (version) { + setVersionToRestore(version); + setIsRestoreModalOpen(true); + setOpenMenuVersionId(null); + } + }; + + const handleRestoreLatestVersion = () => { + const sortedVersions = [...versions].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + if (sortedVersions.length > 0) { + handleRestoreVersion(sortedVersions[0].id); + } + }; + + const handleNameCurrentVersion = () => { + setEditingCanvasTitle(canvasTitle); + setIsEditingCanvasTitle(true); + }; + + const handleSaveCanvasTitle = () => { + const nextTitle = editingCanvasTitle.trim() || 'Edit code'; + setCanvasTitle(nextTitle); + setEditingCanvasTitle(nextTitle); + setIsEditingCanvasTitle(false); + }; + + const handleCancelCanvasTitle = () => { + setEditingCanvasTitle(canvasTitle); + setIsEditingCanvasTitle(false); + }; + + const handleSaveCurrentVersion = () => { + const newVersion: Version = { + id: `v${Date.now()}`, + timestamp: new Date(), + author: 'user', + authorName: 'You', + code, + name: canvasTitle === 'Edit code' ? 'Untitled version' : canvasTitle + }; + setVersions((prev) => [newVersion, ...prev]); + openVersionHistory(); + }; + + const confirmRestore = () => { + if (versionToRestore) { + setCode(versionToRestore.code); + setIsRestoreModalOpen(false); + setVersionToRestore(null); + closeVersionHistory(); + } + }; + + const cancelRestore = () => { + setIsRestoreModalOpen(false); + setVersionToRestore(null); + }; + + const formatModalTimestamp = (timestamp: Date) => + timestamp.toLocaleDateString('en-US', { + month: 'long', + day: 'numeric' + }) + + ' at ' + + timestamp.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true + }); + + const handleStartRename = (versionId: string, currentName: string) => { + setEditingVersionId(versionId); + setEditingName(currentName); + setOpenMenuVersionId(null); + }; + + const handleSaveRename = (versionId: string) => { + setVersions((prev) => prev.map((v) => (v.id === versionId ? { ...v, name: editingName } : v))); + setEditingVersionId(null); + setEditingName(''); + }; + + const handleCancelRename = () => { + setEditingVersionId(null); + setEditingName(''); + }; + + const handleDuplicateVersion = (versionId: string) => { + const version = versions.find((v) => v.id === versionId); + if (version) { + const newVersion: Version = { + ...version, + id: `v${Date.now()}`, + name: `${version.name} (copy)`, + timestamp: new Date() + }; + setVersions((prev) => [newVersion, ...prev]); + } + setOpenMenuVersionId(null); + }; + + const handleMenuToggle = (versionId: string) => { + setOpenMenuVersionId((prev) => (prev === versionId ? null : versionId)); + }; + + const formatTimestamp = (timestamp: Date) => { + const now = new Date(); + const diff = now.getTime() - timestamp.getTime(); + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + + if (minutes < 1) { + return 'Just now'; + } + if (minutes < 60) { + return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; + } + if (hours < 24) { + return `${hours} hour${hours > 1 ? 's' : ''} ago`; + } + return timestamp.toLocaleString(); + }; + + const findMatchingItems = (targetValue: string) => { + const filteredConversations: { [key: string]: Conversation[] } = Object.entries(initialConversations).reduce( + (acc: { [key: string]: Conversation[] }, [key, items]) => { + const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase())); + if (filteredItems.length > 0) { + acc[key] = filteredItems; + } + return acc; + }, + {} + ); + + if (Object.keys(filteredConversations).length === 0) { + return [{ id: '13', noIcon: true, text: 'No results found' }] as unknown as { [key: string]: Conversation[] }; + } + return filteredConversations; + }; + + const attachMenuItems: ReactNode = ( + <> + + }> + Alerts + + }> + Events + + }> + Logs + + + + + } onClick={open}> + Upload from computer + + }> + {`${isCanvasOpen ? 'Disable' : 'Enable'} canvas mode`} + + + + ); + + const customControls = [ + } + aria-label="Undo" + tooltipProps={{ content: 'Undo', aria: 'none' }} + onClick={() => editorRef.current?.trigger('keyboard', 'undo', null)} + />, + } + aria-label="Redo" + tooltipProps={{ content: 'Redo', aria: 'none' }} + onClick={() => editorRef.current?.trigger('keyboard', 'redo', null)} + />, + } + aria-label="Save" + tooltipProps={{ content: 'Save', aria: 'none' }} + onClick={handleSaveCurrentVersion} + /> + ]; + + const selectedVersion = versions.find((v) => v.id === selectedVersionId); + + const canvasTitleHeading = isEditingCanvasTitle ? ( +
{ + event.preventDefault(); + handleSaveCanvasTitle(); + }} + > +
+
+ setEditingCanvasTitle(value)} + aria-label="Canvas title" + autoFocus + onKeyDown={(event) => { + if (event.key === 'Escape') { + event.preventDefault(); + handleCancelCanvasTitle(); + } + }} + /> +
+
+
+
+
+
+
+
+
+ ) : ( + + {canvasTitle} + + ); + + const versionHistoryPanel = ( +
+
+ + + + Version History + + + +
+ +
+ handleVersionSelect(id)} + onSelectableRowChange={(_event, id) => handleVersionSelect(id)} + > + {[...versions] + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .map((version) => ( + + + + {editingVersionId === version.id ? ( + + + setEditingName(value)} + aria-label="Version name" + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSaveRename(version.id); + } + if (e.key === 'Escape') { + handleCancelRename(); + } + }} + /> + + +
+
+ ); + + const panelContent = ( + +
+ + + {canvasTitleHeading} + + + + + + What is canvas mode? + + } + bodyContent={ + <> +
+ This canvas is a collaborative workspace that blends AI-generated content with manual human edits. +
+
Always review AI-generated code prior to use.
+ + } + onShow={() => setIsGeneratedAiPopoverOpen(true)} + onHide={() => setIsGeneratedAiPopoverOpen(false)} + > + + + + + + setIsVersionHistoryMenuOpen(isOpen)} + onSelect={() => setIsVersionHistoryMenuOpen(false)} + popperProps={{ position: 'right' }} + toggle={(toggleRef) => ( + setIsVersionHistoryMenuOpen(!isVersionHistoryMenuOpen)} + aria-label="Version history options" + splitButtonItems={[ + { + event.stopPropagation(); + setIsVersionHistoryMenuOpen(false); + openVersionHistory(); + }} + > + + + + + Version history + + + ]} + /> + )} + > + + + Show full history + + + Restore latest version + + + Name current version + + + Save current version + + + + +
+
+
+
+
+
+ { + editorRef.current = editor; + }} + /> +
+
+
+
+ ); + + // Matches localhost option 2: chatbot hides; canvas + version history side by side + if (isVersionHistoryOpen) { + return ( + <> + + + + {versionToRestore && ( +

+ {`Your current document will revert to the version from ${formatModalTimestamp( + versionToRestore.timestamp + )}.`} +

+ )} +
+ + + + +
+ +
+
+
+
+ + + + {selectedVersion?.name || 'Edit code'} + + + + + + +
+
+
+ +
+
+
+
+
{versionHistoryPanel}
+
+ + ); + } + + return ( + <> + + + + {versionToRestore && ( +

+ {`Your current document will revert to the version from ${formatModalTimestamp( + versionToRestore.timestamp + )}.`} +

+ )} +
+ + + + +
+ + + + +
+ { + setIsDrawerOpen(!isDrawerOpen); + setConversations(initialConversations); + }} + isDrawerOpen={isDrawerOpen} + setIsDrawerOpen={setIsDrawerOpen} + activeItemId="1" + conversations={conversations} + drawerCloseButtonProps={{ 'aria-label': 'Close chat history' }} + onNewChat={() => { + setIsDrawerOpen(!isDrawerOpen); + setMessages([]); + setConversations(initialConversations); + }} + handleTextInputChange={(value: string) => { + if (value === '') { + setConversations(initialConversations); + } + const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value); + setConversations(newConversations); + }} + drawerContent={ + + + +
+ + + setIsDrawerOpen(!isDrawerOpen)} + tooltipContent="Chat history" + menuAriaLabel="Chat history" + /> + {iconLogo} + + + setChatbotVisible(false)} + /> + + + + + {messages.map((message) => ( + ({ + ...attachment, + onClick: openCanvas + }))} + /> + ))} +
+
+
+ + { + if (value === 'canvas') { + if (showCanvasLabel) { + closeCanvasMode(); + } else { + setShowCanvasLabel(true); + setIsCanvasOpen(true); + } + } + setIsAttachMenuOpen(false); + focusMessageActions(); + }, + onAttachMenuToggleClick: () => setIsAttachMenuOpen(!isAttachMenuOpen) + }} + buttonProps={{ + attach: { + innerRef: messageActionsRef, + icon: , + tooltipContent: 'Message actions', + 'aria-label': 'Message actions' + } + }} + additionalActions={ + <> + {showCanvasLabel && ( + + )} + + + } + /> + + +
+
+
+
+ } + /> +
+
+ + ); +}; diff --git a/packages/module/patternfly-docs/patternfly-docs.css.js b/packages/module/patternfly-docs/patternfly-docs.css.js index 2e05d907..2841a490 100644 --- a/packages/module/patternfly-docs/patternfly-docs.css.js +++ b/packages/module/patternfly-docs/patternfly-docs.css.js @@ -13,3 +13,4 @@ import '@patternfly/documentation-framework/global.css'; // Add your extension CSS below import '@patternfly/chatbot/dist/css/main.css'; +import './content/extensions/chatbot/examples/demos/VersionHistory.css';