From c14ad118fd21e41227c96e33714735a93b0f84d7 Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Mon, 2 Nov 2026 10:22:56 -0500 Subject: [PATCH 01/17] feat: add version history pattern demo This adds a new Version History demo showcasing three layout options for displaying version history in canvas mode: - Option 1: Full overlay - version history replaces canvas panel - Option 2: Split-screen - canvas (left) + version history (right) side-by-side - Option 3: Floating panel - 75% overlay with floating background Features include: - Version list with timestamps and AI/user attribution - Rename, restore, and duplicate version actions - Confirmation modal for restore operations - Read-only preview mode in split-screen layout Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.css | 143 ++ .../chatbot/examples/demos/VersionHistory.md | 135 ++ .../chatbot/examples/demos/VersionHistory.tsx | 1199 +++++++++++++++++ 3 files changed, 1477 insertions(+) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.tsx 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..4e00b108 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css @@ -0,0 +1,143 @@ +/* Version history layouts masthead - separate from chatbot */ +.version-history-masthead { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 10000; + padding: var(--pf-t--global--spacer--md); + background-color: var(--pf-t--global--background--color--secondary--default); + border-bottom: 1px solid var(--pf-t--global--border--color--default); +} + +/* Ensure chatbot fills available space below toggle header */ +.version-history-demo .pf-chatbot--fullscreen { + top: 58px !important; + height: calc(100% - 58px) !important; +} + +/* Version History Panel Styles */ +.version-history-panel { + display: flex; + flex-direction: column; + height: 100%; + padding: var(--pf-t--global--spacer--lg); + background-color: var(--pf-t--global--background--color--primary--default); +} + +.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; + overflow: hidden; +} + +.version-history-panel .pf-v6-c-data-list__item.pf-m-selected { + background-color: var(--pf-t--global--background--color--secondary--default); +} + +/* 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; +} + +.version-history-panel .pf-v6-c-data-list__item:hover { + background-color: var(--pf-t--global--background--color--secondary--hover); +} + +/* Make version list items clickable */ +.version-history-panel .pf-v6-c-data-list__item { + cursor: pointer; +} + +/* Option 2: Split-screen layout styles */ +.version-history-split-screen { + display: flex; + position: fixed; + top: 58px; /* Below masthead */ + left: 0; + right: 0; + bottom: 0; +} + +.version-history-split-canvas { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.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; +} + +.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); +} + +.version-history-split-canvas .pf-chatbot__canvas-editor { + flex: 1; + overflow: hidden; + border-radius: var(--pf-t--global--border--radius--medium); +} + +.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); +} + +.version-history-split-panel .version-history-panel { + height: 100%; +} + +/* Option 3: Floating Version History Panel */ +.pf-chatbot__canvas-panel:has(.version-history-floating-panel) { + position: relative; +} + +.version-history-floating-panel { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 75%; + z-index: 100; + background-color: var(--pf-t--global--background--color--floating--default); + box-shadow: var(--pf-t--global--box-shadow--md); +} + +.version-history-floating-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..375ffe27 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -0,0 +1,135 @@ +--- +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, +Modal, +ModalBody, +ModalFooter, +ModalHeader, +Popover, +Select, +SelectList, +SelectOption, +Title, +Tooltip, +ToggleGroup, +ToggleGroupItem, +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 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. + +## 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 + +## Layout options + +This demo provides three layout options for displaying version history, which can be toggled using the selector at the top of the page: + +### Option 1: Full overlay + +The version history panel completely replaces the canvas panel. This option: +- Maximizes space for the version list +- Provides a focused view of version history +- Temporarily hides the code editor until version history is closed + +### Option 2: Split-screen + +The chatbot hides and the screen splits into two panels—canvas on the left and version history on the right. This option: +- Allows side-by-side comparison of versions with the code preview +- Shows a read-only preview of the selected version in the canvas area +- Provides the most comprehensive view for reviewing changes + +### Option 3: Floating panel + +A 75% width floating panel appears over the canvas with a floating background color and medium drop shadow. This option: +- Keeps the canvas partially visible behind the panel +- Uses PatternFly's floating background color token for visual distinction +- Provides a balance between version list visibility and canvas context + +## Version list features + +Each version in the list displays: +- **Version name**: A user-editable name for the version +- **Author**: Who created the version (user or AI bot) +- **Timestamp**: Relative time since the version was created +- **AI label**: A badge indicating AI-generated versions + +Version actions include: +- **Name this version**: Rename a version for easier identification +- **Restore this version**: Revert the canvas to this version's content +- **Duplicate**: Create a copy of the version + +## Demo + +Click the clock icon in the code editor toolbar to open version history. Use the toggle at the top to switch between layout options. + +```js file="./VersionHistory.tsx" isFullscreen + +``` diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.tsx new file mode 100644 index 00000000..614db971 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.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, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + Popover, + Select, + SelectList, + SelectOption, + Title, + Tooltip, + ToggleGroup, + ToggleGroupItem, + 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 { 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/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; +} + +// Version History Types +interface Version { + id: string; + timestamp: Date; + author: 'user' | 'bot'; + authorName: string; + code: string; + name: string; +} + +// Initial versions for demo +const initialVersions: Version[] = [ + { + id: 'v1', + timestamp: new Date(Date.now() - 3600000 * 2), // 2 hours ago + 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), // 1 hour ago + 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), // 30 mins ago + 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 VersionHistory: 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); + // State for version history toggle + const [selectedOption, setSelectedOption] = useState('option1'); + + // Version History State + 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 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, + 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 date = 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: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}` + }); + newMessages.push({ + id: generateId(), + name: 'Bot', + role: 'bot', + timestamp: `${date?.toLocaleDateString()} ${date?.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: { + positive: { onClick: () => console.log('Good response') }, + negative: { onClick: () => console.log('Bad response') }, + copy: { onClick: () => console.log('Copy') }, + download: { onClick: () => console.log('Download') }, + listen: { onClick: () => console.log('Listen') } + }, + timestamp: date.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 handleOptionChange = (event: React.MouseEvent | React.KeyboardEvent | MouseEvent) => { + const id = (event.currentTarget as HTMLElement).id; + setSelectedOption(id); + }; + + // Version History Handlers + const openVersionHistory = () => { + setIsVersionHistoryOpen(true); + setSelectedVersionId(null); + setPreviewCode(null); + + // For Option 2, select the most recent version by default + if (selectedOption === 'option2') { + const sortedVersions = [...versions].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + if (sortedVersions.length > 0) { + setSelectedVersionId(sortedVersions[0].id); + setPreviewCode(sortedVersions[0].code); + } + } + }; + + const closeVersionHistory = () => { + setIsVersionHistoryOpen(false); + setSelectedVersionId(null); + setEditingVersionId(null); + setOpenMenuVersionId(null); + setPreviewCode(null); + }; + + const handleVersionSelect = (versionId: string) => { + setSelectedVersionId(versionId); + + // For Option 2, update the preview code + if (selectedOption === 'option2') { + 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 confirmRestore = () => { + if (versionToRestore) { + setCode(versionToRestore.code); + setIsRestoreModalOpen(false); + setVersionToRestore(null); + closeVersionHistory(); + } + }; + + const cancelRestore = () => { + setIsRestoreModalOpen(false); + setVersionToRestore(null); + }; + + const formatModalTimestamp = (date: Date) => { + return date.toLocaleDateString('en-US', { + month: 'long', + day: 'numeric' + }) + ' at ' + date.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 = (date: Date) => { + const now = new Date(); + const diff = now.getTime() - date.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 date.toLocaleString(); + }; + + const findMatchingItems = (targetValue: string) => { + let 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="Version history" + tooltipProps={{ + content: 'Version history', + aria: 'none' + }} + onClick={openVersionHistory} + /> + ]; + + // Version History Panel component (shared between options) + const versionHistoryPanel = ( +
+
+ + + Version History + + +
+ +
+ + + + {[...versions].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).map((version) => ( + handleVersionSelect(version.id)} + > + + + {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 = ( + + {isVersionHistoryOpen && selectedOption === 'option1' ? ( + // Option 1: Version History Panel - Full overlay + versionHistoryPanel + ) : isVersionHistoryOpen && selectedOption === 'option3' ? ( + // Option 3: Version History Panel - Floating 75% overlay + <> + {/* Normal Canvas Section with Code Editor */} +
+ + + + + Edit code + + + + + + + + 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)} + > + + + + + + +
+
+
+
+
+
+ { + editorRef.current = editor; + }} + /> +
+
+
+ {/* Floating Version History Panel */} +
+ {versionHistoryPanel} +
+ + ) : ( + // Normal Canvas Section with Code Editor +
+ + + + + Edit code + + + + + + + + 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)} + > + + + + + + +
+
+
+
+
+
+ { + editorRef.current = editor; + }} + /> +
+
+
+ )} +
+ ); + + // Option 2: Split-screen layout (Canvas left | Version History right) + if (selectedOption === 'option2' && isVersionHistoryOpen) { + return ( +
+ {/* Restore Version Modal */} + + + + {versionToRestore && ( +

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

+ )} +
+ + + + +
+ + + + {/* Version History Layouts Masthead */} +
+ + + Version history layouts + + + + + + + + + +
+ + {/* Option 2: Split-screen layout */} +
+ {/* Left side: Canvas/Code Editor */} +
+
+
+ + + + + {selectedVersionId ? versions.find(v => v.id === selectedVersionId)?.name || 'Edit code' : 'Edit code'} + + + + + + + +
+
+
+ +
+
+
+
+ + {/* Right side: Version History Panel */} +
+ {versionHistoryPanel} +
+
+
+ ); + } + + return ( +
+ {/* Restore Version Modal */} + + + + {versionToRestore && ( +

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

+ )} +
+ + + + +
+ + + + {/* Version History Layouts Masthead */} +
+ + + Version history layouts + + + + + + + + + +
+ + +
+ { + 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 + }))} + /> + ))} +
+
+
+ + { + console.log('selected', value); + 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 && ( + + )} + + + } + /> + + +
+
+
+
+ } + /> +
+
+
+ ); +}; From 8edb3d46fffbbac1156db1e809f894e5b611b8a1 Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Mon, 2 Nov 2026 10:34:16 -0500 Subject: [PATCH 02/17] fix: add eslint-disable comments for console.log statements Co-authored-by: Cursor --- .../extensions/chatbot/examples/demos/VersionHistory.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.tsx index 614db971..e926568d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.tsx @@ -246,6 +246,7 @@ export const VersionHistory: FunctionComponent = () => { const { open, getInputProps } = useDropzone({ multiple: true, + // eslint-disable-next-line no-console onDropAccepted: () => console.log('fileUploaded') }); @@ -291,10 +292,15 @@ export const VersionHistory: FunctionComponent = () => { 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: date.toLocaleString() From 417c6a15cc09504c63f37f45e9e9dc7cc32bde8e Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Mon, 2 Nov 2026 10:51:52 -0500 Subject: [PATCH 03/17] refactor: split version history into 3 separate demos - VersionHistoryOverlay: Full overlay that replaces the canvas panel - VersionHistorySplitScreen: Side-by-side canvas preview and version list - VersionHistoryFloating: 75% floating panel over the canvas Each demo is now self-contained and can be viewed independently without the need for a toggle switch. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.css | 8 +- .../chatbot/examples/demos/VersionHistory.md | 55 +- .../examples/demos/VersionHistoryFloating.tsx | 967 ++++++++++++++++++ ...nHistory.tsx => VersionHistoryOverlay.tsx} | 583 ++++------- .../demos/VersionHistorySplitScreen.tsx | 427 ++++++++ 5 files changed, 1600 insertions(+), 440 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx rename packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/{VersionHistory.tsx => VersionHistoryOverlay.tsx} (60%) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx 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 index 4e00b108..f58d7421 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css @@ -59,10 +59,16 @@ } /* Option 2: Split-screen layout styles */ +.version-history-split-screen-demo { + display: flex; + height: 100vh; + width: 100%; +} + .version-history-split-screen { display: flex; position: fixed; - top: 58px; /* Below masthead */ + top: 58px; left: 0; right: 0; bottom: 0; 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 index 375ffe27..22a035e1 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -32,8 +32,6 @@ SelectList, SelectOption, Title, Tooltip, -ToggleGroup, -ToggleGroupItem, TextInput, DataList, DataListItem, @@ -88,31 +86,6 @@ Version history is useful when: - Users need to compare different iterations of AI-generated content - Teams collaborate on AI-assisted documents or code -## Layout options - -This demo provides three layout options for displaying version history, which can be toggled using the selector at the top of the page: - -### Option 1: Full overlay - -The version history panel completely replaces the canvas panel. This option: -- Maximizes space for the version list -- Provides a focused view of version history -- Temporarily hides the code editor until version history is closed - -### Option 2: Split-screen - -The chatbot hides and the screen splits into two panels—canvas on the left and version history on the right. This option: -- Allows side-by-side comparison of versions with the code preview -- Shows a read-only preview of the selected version in the canvas area -- Provides the most comprehensive view for reviewing changes - -### Option 3: Floating panel - -A 75% width floating panel appears over the canvas with a floating background color and medium drop shadow. This option: -- Keeps the canvas partially visible behind the panel -- Uses PatternFly's floating background color token for visual distinction -- Provides a balance between version list visibility and canvas context - ## Version list features Each version in the list displays: @@ -126,10 +99,32 @@ Version actions include: - **Restore this version**: Revert the canvas to this version's content - **Duplicate**: Create a copy of the version -## Demo +## Demos + +### Full overlay + +The version history panel completely replaces the canvas panel. This option maximizes space for the version list and provides a focused view of version history. The code editor is temporarily hidden until version history is closed. + +Click the clock icon in the code editor toolbar to open version history. + +```js file="./VersionHistoryOverlay.tsx" isFullscreen + +``` + +### Split-screen + +The screen splits into two panels—a read-only code preview on the left and version history on the right. This option allows side-by-side comparison of versions and provides the most comprehensive view for reviewing changes. Select different versions to preview their content. + +```js file="./VersionHistorySplitScreen.tsx" isFullscreen + +``` + +### Floating panel + +A 75% width floating panel appears over the canvas using PatternFly's floating background color token and medium drop shadow. This option keeps the canvas partially visible behind the panel and provides a balance between version list visibility and canvas context. -Click the clock icon in the code editor toolbar to open version history. Use the toggle at the top to switch between layout options. +Click the clock icon in the code editor toolbar to open version history. -```js file="./VersionHistory.tsx" isFullscreen +```js file="./VersionHistoryFloating.tsx" isFullscreen ``` diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx new file mode 100644 index 00000000..2cd01727 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx @@ -0,0 +1,967 @@ +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, + 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 { 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/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 VersionHistoryFloating: 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 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); + setSelectedVersionId(null); + }; + + const closeVersionHistory = () => { + setIsVersionHistoryOpen(false); + setSelectedVersionId(null); + setEditingVersionId(null); + setOpenMenuVersionId(null); + }; + + const handleVersionSelect = (versionId: string) => { + setSelectedVersionId(versionId); + }; + + const handleRestoreVersion = (versionId: string) => { + const version = versions.find((v) => v.id === versionId); + if (version) { + setVersionToRestore(version); + setIsRestoreModalOpen(true); + setOpenMenuVersionId(null); + } + }; + + const confirmRestore = () => { + if (versionToRestore) { + setCode(versionToRestore.code); + setIsRestoreModalOpen(false); + setVersionToRestore(null); + closeVersionHistory(); + } + }; + + const cancelRestore = () => { + setIsRestoreModalOpen(false); + setVersionToRestore(null); + }; + + const formatModalTimestamp = (timestamp: Date) => { + return ( + 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="Version history" + tooltipProps={{ content: 'Version history', aria: 'none' }} + onClick={openVersionHistory} + /> + ]; + + // Version History Floating Panel + const versionHistoryPanel = ( +
+
+ + + + Version History + + + +
+ +
+ + + + {[...versions] + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .map((version) => ( + handleVersionSelect(version.id)} + > + + + {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 = ( + +
+ + + + + Edit code + + + + + + + + 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)} + > + + + + + + +
+
+
+
+
+
+ { + editorRef.current = editor; + }} + /> +
+
+
+ {/* Floating Version History Panel - 75% overlay */} + {isVersionHistoryOpen &&
{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/content/extensions/chatbot/examples/demos/VersionHistory.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx similarity index 60% rename from packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.tsx rename to packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx index e926568d..5ecbb251 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx @@ -26,8 +26,6 @@ import { SelectOption, Title, Tooltip, - ToggleGroup, - ToggleGroupItem, TextInput, DataList, DataListItem, @@ -132,7 +130,6 @@ interface CanvasEditor { trigger: (source: string, handlerId: string, payload?: unknown) => void; } -// Version History Types interface Version { id: string; timestamp: Date; @@ -142,11 +139,10 @@ interface Version { name: string; } -// Initial versions for demo const initialVersions: Version[] = [ { id: 'v1', - timestamp: new Date(Date.now() - 3600000 * 2), // 2 hours ago + timestamp: new Date(Date.now() - 3600000 * 2), author: 'bot', authorName: 'Bot', code: `apiVersion: v1 @@ -160,7 +156,7 @@ data: }, { id: 'v2', - timestamp: new Date(Date.now() - 3600000), // 1 hour ago + timestamp: new Date(Date.now() - 3600000), author: 'user', authorName: 'You', code: `apiVersion: v1 @@ -177,7 +173,7 @@ data: }, { id: 'v3', - timestamp: new Date(Date.now() - 1800000), // 30 mins ago + timestamp: new Date(Date.now() - 1800000), author: 'bot', authorName: 'Bot', code: `apiVersion: v1 @@ -203,7 +199,7 @@ const iconLogo = ( ); -export const VersionHistory: FunctionComponent = () => { +export const VersionHistoryOverlay: FunctionComponent = () => { const [messages, setMessages] = useState(initialMessages); const [announcement, setAnnouncement] = useState(); const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false); @@ -219,10 +215,6 @@ export const VersionHistory: FunctionComponent = () => { initialConversations ); const [code, setCode] = useState(sampleCode); - // State for version history toggle - const [selectedOption, setSelectedOption] = useState('option1'); - - // Version History State const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(false); const [versions, setVersions] = useState(initialVersions); const [selectedVersionId, setSelectedVersionId] = useState(null); @@ -231,8 +223,7 @@ export const VersionHistory: FunctionComponent = () => { const [openMenuVersionId, setOpenMenuVersionId] = useState(null); const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false); const [versionToRestore, setVersionToRestore] = useState(null); - const [previewCode, setPreviewCode] = useState(null); - + const scrollToBottomRef = useRef(null); const historyRef = useRef(null); const editorRef = useRef(null); @@ -258,7 +249,7 @@ export const VersionHistory: FunctionComponent = () => { const handleSend = useCallback( (input: string | number) => { const inputStr = String(input); - const date = new Date(); + const newDate = new Date(); const newMessages: MessageProps[] = []; messages.forEach((message) => newMessages.push(message)); newMessages.push({ @@ -268,13 +259,13 @@ export const VersionHistory: FunctionComponent = () => { name: 'You', role: 'user', content: inputStr, - timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}` + timestamp: `${newDate?.toLocaleDateString()} ${newDate?.toLocaleTimeString()}` }); newMessages.push({ id: generateId(), name: 'Bot', role: 'bot', - timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`, + timestamp: `${newDate?.toLocaleDateString()} ${newDate?.toLocaleTimeString()}`, isLoading: true }); setMessages(newMessages); @@ -303,7 +294,7 @@ export const VersionHistory: FunctionComponent = () => { // eslint-disable-next-line no-console listen: { onClick: () => console.log('Listen') } }, - timestamp: date.toLocaleString() + timestamp: newDate.toLocaleString() }); setMessages(loadedMessages); setAnnouncement('Message from Bot: API response goes here'); @@ -371,25 +362,9 @@ export const VersionHistory: FunctionComponent = () => { } }; - const handleOptionChange = (event: React.MouseEvent | React.KeyboardEvent | MouseEvent) => { - const id = (event.currentTarget as HTMLElement).id; - setSelectedOption(id); - }; - - // Version History Handlers const openVersionHistory = () => { setIsVersionHistoryOpen(true); setSelectedVersionId(null); - setPreviewCode(null); - - // For Option 2, select the most recent version by default - if (selectedOption === 'option2') { - const sortedVersions = [...versions].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); - if (sortedVersions.length > 0) { - setSelectedVersionId(sortedVersions[0].id); - setPreviewCode(sortedVersions[0].code); - } - } }; const closeVersionHistory = () => { @@ -397,23 +372,14 @@ export const VersionHistory: FunctionComponent = () => { setSelectedVersionId(null); setEditingVersionId(null); setOpenMenuVersionId(null); - setPreviewCode(null); }; const handleVersionSelect = (versionId: string) => { setSelectedVersionId(versionId); - - // For Option 2, update the preview code - if (selectedOption === 'option2') { - 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); + const version = versions.find((v) => v.id === versionId); if (version) { setVersionToRestore(version); setIsRestoreModalOpen(true); @@ -435,15 +401,19 @@ export const VersionHistory: FunctionComponent = () => { setVersionToRestore(null); }; - const formatModalTimestamp = (date: Date) => { - return date.toLocaleDateString('en-US', { - month: 'long', - day: 'numeric' - }) + ' at ' + date.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true - }); + const formatModalTimestamp = (timestamp: Date) => { + return ( + 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) => { @@ -453,9 +423,7 @@ export const VersionHistory: FunctionComponent = () => { }; const handleSaveRename = (versionId: string) => { - setVersions(prev => prev.map(v => - v.id === versionId ? { ...v, name: editingName } : v - )); + setVersions((prev) => prev.map((v) => (v.id === versionId ? { ...v, name: editingName } : v))); setEditingVersionId(null); setEditingName(''); }; @@ -466,7 +434,7 @@ export const VersionHistory: FunctionComponent = () => { }; const handleDuplicateVersion = (versionId: string) => { - const version = versions.find(v => v.id === versionId); + const version = versions.find((v) => v.id === versionId); if (version) { const newVersion: Version = { ...version, @@ -474,29 +442,29 @@ export const VersionHistory: FunctionComponent = () => { name: `${version.name} (copy)`, timestamp: new Date() }; - setVersions(prev => [newVersion, ...prev]); + setVersions((prev) => [newVersion, ...prev]); } setOpenMenuVersionId(null); }; const handleMenuToggle = (versionId: string) => { - setOpenMenuVersionId(prev => prev === versionId ? null : versionId); + setOpenMenuVersionId((prev) => (prev === versionId ? null : versionId)); }; - const formatTimestamp = (date: Date) => { + const formatTimestamp = (timestamp: Date) => { const now = new Date(); - const diff = now.getTime() - date.getTime(); + 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 date.toLocaleString(); + return timestamp.toLocaleString(); }; const findMatchingItems = (targetValue: string) => { - let filteredConversations: { [key: string]: Conversation[] } = Object.entries(initialConversations).reduce( + 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) { @@ -543,163 +511,172 @@ export const VersionHistory: FunctionComponent = () => { key="undo" icon={} aria-label="Undo" - tooltipProps={{ - content: 'Undo', - aria: 'none' - }} + tooltipProps={{ content: 'Undo', aria: 'none' }} onClick={() => editorRef.current?.trigger('keyboard', 'undo', null)} />, } aria-label="Redo" - tooltipProps={{ - content: 'Redo', - aria: 'none' - }} + tooltipProps={{ content: 'Redo', aria: 'none' }} onClick={() => editorRef.current?.trigger('keyboard', 'redo', null)} />, } aria-label="Version history" - tooltipProps={{ - content: 'Version history', - aria: 'none' - }} + tooltipProps={{ content: 'Version history', aria: 'none' }} onClick={openVersionHistory} /> ]; - // Version History Panel component (shared between options) + // Version History Panel - Full Overlay (Option 1) const versionHistoryPanel = (
- Version History + + Version History + -
- +
- {[...versions].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).map((version) => ( - handleVersionSelect(version.id)} - > - - - {editingVersionId === version.id ? ( - - - setEditingName(value)} - aria-label="Version name" - autoFocus - onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveRename(version.id); - if (e.key === 'Escape') handleCancelRename(); - }} - /> - - -
- - -
-
- { - editorRef.current = editor; - }} - /> -
-
- - {/* Floating Version History Panel */} -
- {versionHistoryPanel} -
- ) : ( - // Normal Canvas Section with Code Editor
{ bodyContent={ <>
- This canvas is a collaborative workspace that blends AI-generated content with manual human edits. + This canvas is a collaborative workspace that blends AI-generated content with manual human + edits.
Always review AI-generated code prior to use.
@@ -900,108 +784,8 @@ export const VersionHistory: FunctionComponent = () => { ); - // Option 2: Split-screen layout (Canvas left | Version History right) - if (selectedOption === 'option2' && isVersionHistoryOpen) { - return ( -
- {/* Restore Version Modal */} - - - - {versionToRestore && ( -

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

- )} -
- - - - -
- - - - {/* Version History Layouts Masthead */} -
- - - Version history layouts - - - - - - - - - -
- - {/* Option 2: Split-screen layout */} -
- {/* Left side: Canvas/Code Editor */} -
-
-
- - - - - {selectedVersionId ? versions.find(v => v.id === selectedVersionId)?.name || 'Edit code' : 'Edit code'} - - - - - - - -
-
-
- -
-
-
-
- - {/* Right side: Version History Panel */} -
- {versionHistoryPanel} -
-
-
- ); - } - return ( -
- {/* Restore Version Modal */} + <> { {versionToRestore && ( -

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

+

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

)}
@@ -1027,22 +813,6 @@ export const VersionHistory: FunctionComponent = () => {
- - {/* Version History Layouts Masthead */} -
- - - Version history layouts - - - - - - - - - -
@@ -1127,7 +897,6 @@ export const VersionHistory: FunctionComponent = () => { attachMenuItems, onAttachMenuOnOpenChangeKeys: ['Escape', 'Tab'], onAttachMenuSelect: (_ev, value) => { - console.log('selected', value); if (value === 'canvas') { if (showCanvasLabel) { closeCanvasMode(); @@ -1152,11 +921,7 @@ export const VersionHistory: FunctionComponent = () => { additionalActions={ <> {showCanvasLabel && ( -
-
+ ); }; 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..3254cb1a --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx @@ -0,0 +1,427 @@ +import React, { useState, FunctionComponent } from 'react'; +import { + Button, + Dropdown, + DropdownItem, + DropdownList, + Flex, + FlexItem, + Label, + MenuToggle, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + Title, + TextInput, + DataList, + DataListItem, + DataListItemRow, + DataListItemCells, + DataListCell, + DataListAction, + Content, + ContentVariants +} from '@patternfly/react-core'; +import { CodeEditor, Language } from '@patternfly/react-code-editor'; +import { RhUiAiInfoIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-info-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 { RhMicronsCloseIcon } from '@patternfly/react-icons/dist/esm/icons/rh-microns-close-icon'; +import '@patternfly/react-core/dist/styles/base.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? +`; + +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' + } +]; + +export const VersionHistorySplitScreen: FunctionComponent = () => { + const [code, setCode] = useState(sampleCode); + const [versions, setVersions] = useState(initialVersions); + const [selectedVersionId, setSelectedVersionId] = useState(initialVersions[2].id); + 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(initialVersions[2].code); + + 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 confirmRestore = () => { + if (versionToRestore) { + setCode(versionToRestore.code); + setIsRestoreModalOpen(false); + setVersionToRestore(null); + } + }; + + const cancelRestore = () => { + setIsRestoreModalOpen(false); + setVersionToRestore(null); + }; + + const formatModalTimestamp = (timestamp: Date) => { + return ( + 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 selectedVersion = versions.find((v) => v.id === selectedVersionId); + + const versionHistoryPanel = ( +
+
+ + + + Version History + + + +
+ +
+ + + + {[...versions] + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .map((version) => ( + handleVersionSelect(version.id)} + > + + + {editingVersionId === version.id ? ( + + + setEditingName(value)} + aria-label="Version name" + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveRename(version.id); + if (e.key === 'Escape') handleCancelRename(); + }} + /> + + +
+
+ ); + + return ( + <> + + + + {versionToRestore && ( +

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

+ )} +
+ + + + +
+ +
+ {/* Left side: Canvas/Code Editor */} +
+
+
+ + + + + {selectedVersion?.name || 'Edit code'} + + + + + + + +
+
+
+ +
+
+
+
+ + {/* Right side: Version History Panel */} +
{versionHistoryPanel}
+
+ + ); +}; From 17a48ab470521193b8b9d92f3d76353390729471 Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Mon, 2 Nov 2026 11:11:23 -0500 Subject: [PATCH 04/17] fix: load version history CSS in docs and fix panel layout The VersionHistory.css import from example TSX files was not bundled by the docs framework, so the panel collapsed and hid the version list. Import the CSS from patternfly-docs.css.js and make the overlay panel fill the canvas drawer height. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.css | 38 +-- .../examples/demos/VersionHistoryFloating.tsx | 236 +++++++++-------- .../examples/demos/VersionHistoryOverlay.tsx | 240 +++++++++--------- .../demos/VersionHistorySplitScreen.tsx | 237 +++++++++-------- .../patternfly-docs/patternfly-docs.css.js | 1 + 5 files changed, 353 insertions(+), 399 deletions(-) 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 index f58d7421..8e252b4d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css @@ -1,28 +1,13 @@ -/* Version history layouts masthead - separate from chatbot */ -.version-history-masthead { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 10000; - padding: var(--pf-t--global--spacer--md); - background-color: var(--pf-t--global--background--color--secondary--default); - border-bottom: 1px solid var(--pf-t--global--border--color--default); -} - -/* Ensure chatbot fills available space below toggle header */ -.version-history-demo .pf-chatbot--fullscreen { - top: 58px !important; - height: calc(100% - 58px) !important; -} - /* 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 { @@ -33,7 +18,8 @@ .version-history-content { flex: 1; - overflow: hidden; + min-height: 0; + overflow: auto; } .version-history-panel .pf-v6-c-data-list__item.pf-m-selected { @@ -63,15 +49,7 @@ display: flex; height: 100vh; width: 100%; -} - -.version-history-split-screen { - display: flex; - position: fixed; - top: 58px; - left: 0; - right: 0; - bottom: 0; + background-color: var(--pf-t--global--background--color--secondary--default); } .version-history-split-canvas { @@ -79,6 +57,7 @@ display: flex; flex-direction: column; overflow: hidden; + min-width: 0; } .version-history-split-canvas .pf-chatbot__canvas-section { @@ -105,12 +84,14 @@ 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 { @@ -122,6 +103,7 @@ 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 { diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx index 2cd01727..c6996a84 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx @@ -552,134 +552,124 @@ export const VersionHistoryFloating: FunctionComponent = () => {
- - - - {[...versions] - .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) - .map((version) => ( - handleVersionSelect(version.id)} - > - - - {editingVersionId === version.id ? ( - - - setEditingName(value)} - aria-label="Version name" - autoFocus - onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveRename(version.id); - if (e.key === 'Escape') handleCancelRename(); - }} - /> - - -
); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx index 5ecbb251..1c342442 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx @@ -552,134 +552,124 @@ export const VersionHistoryOverlay: FunctionComponent = () => {
- - - - {[...versions] - .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) - .map((version) => ( - handleVersionSelect(version.id)} - > - - - {editingVersionId === version.id ? ( - - - setEditingName(value)} - aria-label="Version name" - autoFocus - onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveRename(version.id); - if (e.key === 'Escape') handleCancelRename(); - }} - /> - - -
); @@ -694,7 +684,9 @@ export const VersionHistoryOverlay: FunctionComponent = () => { className="pf-chatbot__canvas-panel" > {isVersionHistoryOpen ? ( - versionHistoryPanel +
+ {versionHistoryPanel} +
) : (
{
- - - - {[...versions] - .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) - .map((version) => ( - handleVersionSelect(version.id)} - > - - - {editingVersionId === version.id ? ( - - - setEditingName(value)} - aria-label="Version name" - autoFocus - onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveRename(version.id); - if (e.key === 'Escape') handleCancelRename(); - }} - /> - - -
); 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'; From 1812167acad4687ec6355f98eed049bf37f073bc Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Mon, 2 Nov 2026 13:01:37 -0500 Subject: [PATCH 05/17] fix: add close button to split-screen version history panel Restores the X close control in the panel header so users can dismiss version history and reopen it from the code editor toolbar. Co-authored-by: Cursor --- .../demos/VersionHistorySplitScreen.tsx | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) 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 index e9510695..81ecc2eb 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx @@ -23,11 +23,13 @@ import { Content, ContentVariants } from '@patternfly/react-core'; -import { CodeEditor, Language } from '@patternfly/react-code-editor'; +import { CodeEditor, CodeEditorControl, Language } from '@patternfly/react-code-editor'; import { RhUiAiInfoIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-info-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 { RhMicronsCloseIcon } from '@patternfly/react-icons/dist/esm/icons/rh-microns-close-icon'; +import { RhUiHistoryIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-history-icon'; import '@patternfly/react-core/dist/styles/base.css'; import '@patternfly/chatbot/dist/css/main.css'; import './VersionHistory.css'; @@ -111,7 +113,25 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { const [openMenuVersionId, setOpenMenuVersionId] = useState(null); const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false); const [versionToRestore, setVersionToRestore] = useState(null); - const [previewCode, setPreviewCode] = useState(initialVersions[2].code); + const [previewCode, setPreviewCode] = useState(initialVersions[2].code); + const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(true); + + 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); + } + }; + + const closeVersionHistory = () => { + setIsVersionHistoryOpen(false); + setSelectedVersionId(null); + setEditingVersionId(null); + setOpenMenuVersionId(null); + setPreviewCode(null); + }; const handleVersionSelect = (versionId: string) => { setSelectedVersionId(versionId); @@ -207,6 +227,16 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { const selectedVersion = versions.find((v) => v.id === selectedVersionId); + const historyControl = ( + } + aria-label="Version history" + tooltipProps={{ content: 'Version history', aria: 'none' }} + onClick={openVersionHistory} + /> + ); + const versionHistoryPanel = (
@@ -216,6 +246,14 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { Version History + +
@@ -382,12 +420,12 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { - {selectedVersion?.name || 'Edit code'} + {isVersionHistoryOpen ? selectedVersion?.name || 'Edit code' : 'Edit code'} @@ -399,9 +437,13 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { isFullHeight isLineNumbersVisible isLanguageLabelVisible - isReadOnly - code={previewCode} + isReadOnly={isVersionHistoryOpen} + isCopyEnabled={!isVersionHistoryOpen} + isDownloadEnabled={!isVersionHistoryOpen} + customControls={isVersionHistoryOpen ? undefined : [historyControl]} + code={isVersionHistoryOpen ? previewCode || code : code} language={Language.yaml} + onCodeChange={setCode} />
@@ -409,7 +451,7 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { {/* Right side: Version History Panel */} -
{versionHistoryPanel}
+ {isVersionHistoryOpen &&
{versionHistoryPanel}
} ); From d49879c6c560eb6495ec606ac6217a4583037a49 Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Tue, 3 Nov 2026 15:35:19 -0500 Subject: [PATCH 06/17] refactor: keep only split-screen and partial overlay demos Remove the full overlay version history demo so the PR focuses on the two preferred layout options. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.md | 12 +- .../examples/demos/VersionHistoryOverlay.tsx | 962 ------------------ 2 files changed, 1 insertion(+), 973 deletions(-) delete mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx 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 index 22a035e1..015bb447 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -101,16 +101,6 @@ Version actions include: ## Demos -### Full overlay - -The version history panel completely replaces the canvas panel. This option maximizes space for the version list and provides a focused view of version history. The code editor is temporarily hidden until version history is closed. - -Click the clock icon in the code editor toolbar to open version history. - -```js file="./VersionHistoryOverlay.tsx" isFullscreen - -``` - ### Split-screen The screen splits into two panels—a read-only code preview on the left and version history on the right. This option allows side-by-side comparison of versions and provides the most comprehensive view for reviewing changes. Select different versions to preview their content. @@ -119,7 +109,7 @@ The screen splits into two panels—a read-only code preview on the left and ver ``` -### Floating panel +### Partial overlay A 75% width floating panel appears over the canvas using PatternFly's floating background color token and medium drop shadow. This option keeps the canvas partially visible behind the panel and provides a balance between version list visibility and canvas context. diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx deleted file mode 100644 index 1c342442..00000000 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryOverlay.tsx +++ /dev/null @@ -1,962 +0,0 @@ -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, - 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 { 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/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 VersionHistoryOverlay: 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 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); - setSelectedVersionId(null); - }; - - const closeVersionHistory = () => { - setIsVersionHistoryOpen(false); - setSelectedVersionId(null); - setEditingVersionId(null); - setOpenMenuVersionId(null); - }; - - const handleVersionSelect = (versionId: string) => { - setSelectedVersionId(versionId); - }; - - const handleRestoreVersion = (versionId: string) => { - const version = versions.find((v) => v.id === versionId); - if (version) { - setVersionToRestore(version); - setIsRestoreModalOpen(true); - setOpenMenuVersionId(null); - } - }; - - const confirmRestore = () => { - if (versionToRestore) { - setCode(versionToRestore.code); - setIsRestoreModalOpen(false); - setVersionToRestore(null); - closeVersionHistory(); - } - }; - - const cancelRestore = () => { - setIsRestoreModalOpen(false); - setVersionToRestore(null); - }; - - const formatModalTimestamp = (timestamp: Date) => { - return ( - 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="Version history" - tooltipProps={{ content: 'Version history', aria: 'none' }} - onClick={openVersionHistory} - /> - ]; - - // Version History Panel - Full Overlay (Option 1) - const versionHistoryPanel = ( -
-
- - - - Version History - - - -
- -
- - {[...versions] - .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) - .map((version) => ( - handleVersionSelect(version.id)} - > - - - {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 = ( - - {isVersionHistoryOpen ? ( -
- {versionHistoryPanel} -
- ) : ( -
- - - - - Edit code - - - - - - - - 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)} - > - - - - - - -
-
-
-
-
-
- { - editorRef.current = editor; - }} - /> -
-
-
- )} -
- ); - - 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 && ( - - )} - - - } - /> - - -
-
-
-
- } - /> -
-
- - ); -}; From 8e4c20de6fcd42f0fbbaa92305371eb2b4e21843 Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Tue, 3 Nov 2026 15:37:33 -0500 Subject: [PATCH 07/17] fix: move split-screen version history trigger to canvas header Place the history icon button in the canvas panel header instead of the code editor toolbar. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.md | 2 ++ .../demos/VersionHistorySplitScreen.tsx | 26 ++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) 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 index 015bb447..7e8b07dd 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -105,6 +105,8 @@ Version actions include: The screen splits into two panels—a read-only code preview on the left and version history on the right. This option allows side-by-side comparison of versions and provides the most comprehensive view for reviewing changes. Select different versions to preview their content. +Close version history with the X in the panel header, then reopen it with the history icon button in the canvas panel header. + ```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 index 81ecc2eb..bd4e550d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx @@ -13,6 +13,7 @@ import { ModalFooter, ModalHeader, Title, + Tooltip, TextInput, DataList, DataListItem, @@ -23,7 +24,7 @@ import { Content, ContentVariants } from '@patternfly/react-core'; -import { CodeEditor, CodeEditorControl, Language } from '@patternfly/react-code-editor'; +import { CodeEditor, Language } from '@patternfly/react-code-editor'; import { RhUiAiInfoIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-info-icon'; import { CheckIcon } from '@patternfly/react-icons/dist/esm/icons/check-icon'; import { TimesIcon } from '@patternfly/react-icons/dist/esm/icons/times-icon'; @@ -227,16 +228,6 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { const selectedVersion = versions.find((v) => v.id === selectedVersionId); - const historyControl = ( - } - aria-label="Version history" - tooltipProps={{ content: 'Version history', aria: 'none' }} - onClick={openVersionHistory} - /> - ); - const versionHistoryPanel = (
@@ -429,6 +420,18 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { + {!isVersionHistoryOpen && ( + + +
@@ -440,7 +443,6 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { isReadOnly={isVersionHistoryOpen} isCopyEnabled={!isVersionHistoryOpen} isDownloadEnabled={!isVersionHistoryOpen} - customControls={isVersionHistoryOpen ? undefined : [historyControl]} code={isVersionHistoryOpen ? previewCode || code : code} language={Language.yaml} onCodeChange={setCode} From f8230797a620f8e226e372f0bab872a16a4072ef Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Tue, 3 Nov 2026 15:42:18 -0500 Subject: [PATCH 08/17] fix: rebuild split-screen demo with chatbot and header trigger Restore the fullscreen chatbot + canvas default view, place the version history trigger in the canvas header, and show the split-screen layout only while version history is open. Update docs and PR description to remove the full overlay option. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.md | 4 +- .../demos/VersionHistorySplitScreen.tsx | 712 ++++++++++++++++-- 2 files changed, 653 insertions(+), 63 deletions(-) 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 index 7e8b07dd..e7e05a21 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -103,9 +103,7 @@ Version actions include: ### Split-screen -The screen splits into two panels—a read-only code preview on the left and version history on the right. This option allows side-by-side comparison of versions and provides the most comprehensive view for reviewing changes. Select different versions to preview their content. - -Close version history with the X in the panel header, then reopen it with the history icon button in the canvas panel header. +Starts with the fullscreen chatbot and canvas. Open version history with the history icon button in the canvas panel header. The chatbot hides and the screen splits into two panels—a read-only code preview on the left and version history on the right. Select different versions to preview their content. Close version history with the X in the panel header 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 index bd4e550d..ad6b41e3 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx @@ -1,6 +1,13 @@ -import React, { useState, FunctionComponent } from 'react'; +import React, { useState, useRef, useCallback, useEffect, FunctionComponent, ReactNode } from 'react'; import { + Brand, Button, + Divider, + Drawer, + DrawerContent, + DrawerContentBody, + DrawerHead, + DrawerPanelContent, Dropdown, DropdownItem, DropdownList, @@ -12,6 +19,10 @@ import { ModalBody, ModalFooter, ModalHeader, + Popover, + Select, + SelectList, + SelectOption, Title, Tooltip, TextInput, @@ -24,13 +35,41 @@ import { Content, ContentVariants } from '@patternfly/react-core'; -import { CodeEditor, Language } from '@patternfly/react-code-editor'; +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 { 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 { RhUiHistoryIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-history-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/chatbot/dist/css/main.css'; import './VersionHistory.css'; @@ -43,6 +82,53 @@ 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; @@ -105,17 +191,176 @@ data: } ]; +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(initialVersions[2].id); + 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(initialVersions[2].code); - const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(true); + const [previewCode, setPreviewCode] = useState(null); + + 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); @@ -123,6 +368,9 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { if (sortedVersions.length > 0) { setSelectedVersionId(sortedVersions[0].id); setPreviewCode(sortedVersions[0].code); + } else { + setSelectedVersionId(null); + setPreviewCode(null); } }; @@ -156,6 +404,7 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { setCode(versionToRestore.code); setIsRestoreModalOpen(false); setVersionToRestore(null); + closeVersionHistory(); } }; @@ -226,8 +475,69 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { 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)} + /> + ]; + const selectedVersion = versions.find((v) => v.id === selectedVersionId); + // Version History Panel const versionHistoryPanel = (
@@ -371,6 +681,191 @@ export const VersionHistorySplitScreen: FunctionComponent = () => {
); + const panelContent = ( + +
+ + + + + + Edit code + + + + + + + + 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)} + > + + + +
+ + + +
+ + + + +
+
+ { + editorRef.current = editor; + }} + /> +
+
+
+ + ); + + // Split-screen layout when version history is open (chatbot hides) + if (isVersionHistoryOpen) { + return ( + <> + + + + {versionToRestore && ( +

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

+ )} +
+ + + + +
+ +
+
+
+
+ + + + + {selectedVersion?.name || 'Edit code'} + + + + + + + +
+
+
+ +
+
+
+
+
{versionHistoryPanel}
+
+ + ); + } + return ( <> { -
- {/* Left side: Canvas/Code Editor */} -
-
-
-
+ + +
+ + + 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 && ( + + )} + + + } + /> + + +
+
+
+ + } + />
- - {/* Right side: Version History Panel */} - {isVersionHistoryOpen &&
{versionHistoryPanel}
} -
+ ); }; From 716e0d07ee04ca7ea172de3f5abf941bf01b1bb0 Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Tue, 3 Nov 2026 15:46:43 -0500 Subject: [PATCH 09/17] fix: restore split-screen to match localhost option 2 layout Keep the default chatbot + canvas view identical to the local prototype. Only relocate the version history trigger from the code editor toolbar into the canvas header DrawerActions next to close. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.md | 2 +- .../demos/VersionHistorySplitScreen.tsx | 159 ++++++++---------- 2 files changed, 73 insertions(+), 88 deletions(-) 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 index e7e05a21..e4134d66 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -103,7 +103,7 @@ Version actions include: ### Split-screen -Starts with the fullscreen chatbot and canvas. Open version history with the history icon button in the canvas panel header. The chatbot hides and the screen splits into two panels—a read-only code preview on the left and version history on the right. Select different versions to preview their content. Close version history with the X in the panel header to return to the chatbot and canvas. +Matches the canvas split-screen layout: chatbot and canvas side by side by default. Open version history with the history icon in the canvas panel header (next to close). The chatbot hides and the screen splits into a read-only code preview on the left and version history on the right. Close with the X 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 index ad6b41e3..d4ef6147 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx @@ -4,6 +4,7 @@ import { Button, Divider, Drawer, + DrawerActions, DrawerContent, DrawerContentBody, DrawerHead, @@ -537,7 +538,6 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { const selectedVersion = versions.find((v) => v.id === selectedVersionId); - // Version History Panel const versionHistoryPanel = (
@@ -699,77 +699,67 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { id="chatbot-canvas-section" > - - - - - Edit code - - - - - - - - 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)} + + + + Edit code + + + + + + + + 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)} + > +
- - - -
- - + Generated with AI + + + + + +
+
@@ -794,7 +784,7 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { ); - // Split-screen layout when version history is open (chatbot hides) + // Matches localhost option 2: chatbot hides; canvas + version history side by side if (isVersionHistoryOpen) { return ( <> @@ -828,22 +818,17 @@ export const VersionHistorySplitScreen: FunctionComponent = () => {
- - - - - {selectedVersion?.name || 'Edit code'} - - - - - - + + + + {selectedVersion?.name || 'Edit code'} + + + + +
From 6aaceb9373992d0da8ef4066cd57a9a3eb2943ba Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Wed, 4 Nov 2026 11:38:17 -0500 Subject: [PATCH 10/17] refactor: remove partial overlay version history demo Keep only the split-screen demo and related documentation. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.css | 20 - .../chatbot/examples/demos/VersionHistory.md | 10 - .../examples/demos/VersionHistoryFloating.tsx | 957 ------------------ 3 files changed, 987 deletions(-) delete mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx 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 index 8e252b4d..77d08501 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css @@ -109,23 +109,3 @@ .version-history-split-panel .version-history-panel { height: 100%; } - -/* Option 3: Floating Version History Panel */ -.pf-chatbot__canvas-panel:has(.version-history-floating-panel) { - position: relative; -} - -.version-history-floating-panel { - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 75%; - z-index: 100; - background-color: var(--pf-t--global--background--color--floating--default); - box-shadow: var(--pf-t--global--box-shadow--md); -} - -.version-history-floating-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 index e4134d66..2ccc51de 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -108,13 +108,3 @@ Matches the canvas split-screen layout: chatbot and canvas side by side by defau ```js file="./VersionHistorySplitScreen.tsx" isFullscreen ``` - -### Partial overlay - -A 75% width floating panel appears over the canvas using PatternFly's floating background color token and medium drop shadow. This option keeps the canvas partially visible behind the panel and provides a balance between version list visibility and canvas context. - -Click the clock icon in the code editor toolbar to open version history. - -```js file="./VersionHistoryFloating.tsx" isFullscreen - -``` diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx deleted file mode 100644 index c6996a84..00000000 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistoryFloating.tsx +++ /dev/null @@ -1,957 +0,0 @@ -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, - 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 { 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/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 VersionHistoryFloating: 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 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); - setSelectedVersionId(null); - }; - - const closeVersionHistory = () => { - setIsVersionHistoryOpen(false); - setSelectedVersionId(null); - setEditingVersionId(null); - setOpenMenuVersionId(null); - }; - - const handleVersionSelect = (versionId: string) => { - setSelectedVersionId(versionId); - }; - - const handleRestoreVersion = (versionId: string) => { - const version = versions.find((v) => v.id === versionId); - if (version) { - setVersionToRestore(version); - setIsRestoreModalOpen(true); - setOpenMenuVersionId(null); - } - }; - - const confirmRestore = () => { - if (versionToRestore) { - setCode(versionToRestore.code); - setIsRestoreModalOpen(false); - setVersionToRestore(null); - closeVersionHistory(); - } - }; - - const cancelRestore = () => { - setIsRestoreModalOpen(false); - setVersionToRestore(null); - }; - - const formatModalTimestamp = (timestamp: Date) => { - return ( - 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="Version history" - tooltipProps={{ content: 'Version history', aria: 'none' }} - onClick={openVersionHistory} - /> - ]; - - // Version History Floating Panel - const versionHistoryPanel = ( -
-
- - - - Version History - - - -
- -
- - {[...versions] - .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) - .map((version) => ( - handleVersionSelect(version.id)} - > - - - {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 = ( - -
- - - - - Edit code - - - - - - - - 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)} - > - - - - - - -
-
-
-
-
-
- { - editorRef.current = editor; - }} - /> -
-
-
- {/* Floating Version History Panel - 75% overlay */} - {isVersionHistoryOpen &&
{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 && ( - - )} - - - } - /> - - -
-
-
-
- } - /> -
-
- - ); -}; From 4cffa4a10812b9b7f234e88dccedc593e7110e15 Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Wed, 4 Nov 2026 12:09:10 -0500 Subject: [PATCH 11/17] feat: add split menu toggle trigger for version history demo Preserve the previous icon-button trigger as SplitScreenV1. Replace the header control with a MenuToggle split action labeled Version history, including a dropdown of upcoming actions. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.md | 10 +- .../demos/VersionHistorySplitScreen.tsx | 57 +- .../demos/VersionHistorySplitScreenV1.tsx | 1037 +++++++++++++++++ 3 files changed, 1095 insertions(+), 9 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreenV1.tsx 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 index 2ccc51de..6b222aaa 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -103,8 +103,16 @@ Version actions include: ### Split-screen -Matches the canvas split-screen layout: chatbot and canvas side by side by default. Open version history with the history icon in the canvas panel header (next to close). The chatbot hides and the screen splits into a read-only code preview on the left and version history on the right. Close with the X to return to the chatbot and canvas. +Matches the canvas split-screen layout: chatbot and canvas side by side by default. Open version history with the **Version history** split button in the canvas panel header (next to close)—click the main button or **Show full history** in the dropdown. The chatbot hides and the screen splits into a read-only code preview on the left and version history on the right. Close with the X to return to the chatbot and canvas. ```js file="./VersionHistorySplitScreen.tsx" isFullscreen ``` + +### Split-screen (v1) + +Previous trigger: a plain history icon button in the canvas header. Kept for comparison. + +```js file="./VersionHistorySplitScreenV1.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 index d4ef6147..de67e955 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx @@ -16,6 +16,7 @@ import { FlexItem, Label, MenuToggle, + MenuToggleAction, Modal, ModalBody, ModalFooter, @@ -224,6 +225,7 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false); const [versionToRestore, setVersionToRestore] = useState(null); const [previewCode, setPreviewCode] = useState(null); + const [isVersionHistoryMenuOpen, setIsVersionHistoryMenuOpen] = useState(false); const scrollToBottomRef = useRef(null); const historyRef = useRef(null); @@ -741,14 +743,53 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { - -
+ +
+ + {[...versions] + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .map((version) => ( + handleVersionSelect(version.id)} + > + + + {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 = ( + +
+ + + + + Edit code + + + + + + + + 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)} + > + + + + + + +
+ + +
+
+ { + 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 && ( + + )} + + + } + /> + + +
+
+
+
+ } + /> +
+
+ + ); +}; From 43a4631fdea3459174f226e50bbaa4e345243b07 Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Thu, 5 Nov 2026 12:01:53 -0500 Subject: [PATCH 12/17] feat: wire version history split toggle menu actions and polish demo Complete restore/name/save flows, restore editor toolbar actions, use default split MenuToggle styling with md spacer, and fix DataList clickable row selection/hover. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.css | 17 +-- .../demos/VersionHistorySplitScreen.tsx | 135 ++++++++++++++++-- 2 files changed, 125 insertions(+), 27 deletions(-) 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 index 77d08501..b8147051 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.css @@ -22,10 +22,6 @@ overflow: auto; } -.version-history-panel .pf-v6-c-data-list__item.pf-m-selected { - background-color: var(--pf-t--global--background--color--secondary--default); -} - /* Remove top border from first data list item */ .version-history-panel .pf-v6-c-data-list { border-top: none; @@ -35,15 +31,6 @@ border-block-start: none; } -.version-history-panel .pf-v6-c-data-list__item:hover { - background-color: var(--pf-t--global--background--color--secondary--hover); -} - -/* Make version list items clickable */ -.version-history-panel .pf-v6-c-data-list__item { - cursor: pointer; -} - /* Option 2: Split-screen layout styles */ .version-history-split-screen-demo { display: flex; @@ -78,6 +65,10 @@ flex-shrink: 0; } +.version-history-split-canvas .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; 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 index de67e955..e8ff36e1 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx @@ -59,6 +59,7 @@ import { RhUiImageFillIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui- 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'; @@ -73,6 +74,7 @@ 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'; @@ -226,6 +228,9 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { 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); @@ -402,6 +407,43 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { } }; + 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); @@ -535,11 +577,65 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { 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 = (
@@ -561,7 +657,13 @@ export const VersionHistorySplitScreen: FunctionComponent = () => {
- + handleVersionSelect(id)} + onSelectableRowChange={(_event, id) => handleVersionSelect(id)} + > {[...versions] .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) .map((version) => ( @@ -569,9 +671,6 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { key={version.id} id={version.id} aria-labelledby={`version-${version.id}`} - isExpanded={selectedVersionId === version.id} - className={selectedVersionId === version.id ? 'pf-m-selected' : ''} - onClick={() => handleVersionSelect(version.id)} > { ) : ( - + {version.name} @@ -702,11 +805,7 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { > - - - Edit code - - + {canvasTitleHeading} { toggle={(toggleRef) => ( setIsVersionHistoryMenuOpen(!isVersionHistoryMenuOpen)} aria-label="Version history options" @@ -785,9 +883,15 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { Show full history - Restore latest version - Name current version - Make a copy + + Restore latest version + + + Name current version + + + Save current version + @@ -879,6 +983,9 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { isLineNumbersVisible isLanguageLabelVisible isReadOnly + isCopyEnabled + isDownloadEnabled + downloadFileName="canvas-mode" code={previewCode || code} language={Language.yaml} /> From 84a0265ead08eca9e55b884efe55cfe1f0542062 Mon Sep 17 00:00:00 2001 From: Kayla Chumley Date: Thu, 5 Nov 2026 12:21:49 -0500 Subject: [PATCH 13/17] fix: restore version history demo imports and resolve lint errors Add missing MenuToggleAction/RhUiSaveIcon docs imports so the Surge demo no longer throws, and fix eslint/prettier issues in both demos. Co-authored-by: Cursor --- .../chatbot/examples/demos/VersionHistory.md | 2 + .../demos/VersionHistorySplitScreen.tsx | 68 +++++++++++-------- .../demos/VersionHistorySplitScreenV1.tsx | 62 ++++++++++------- 3 files changed, 81 insertions(+), 51 deletions(-) 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 index 6b222aaa..5f913e2d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistory.md @@ -22,6 +22,7 @@ Flex, FlexItem, Label, MenuToggle, +MenuToggleAction, Modal, ModalBody, ModalFooter, @@ -62,6 +63,7 @@ import RhUiImageFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-imag 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'; 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 index e8ff36e1..04f65f8d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/VersionHistorySplitScreen.tsx @@ -458,20 +458,17 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { setVersionToRestore(null); }; - const formatModalTimestamp = (timestamp: Date) => { - return ( - timestamp.toLocaleDateString('en-US', { - month: 'long', - day: 'numeric' - }) + - ' at ' + - timestamp.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true - }) - ); - }; + 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); @@ -514,9 +511,15 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { 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`; + 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(); }; @@ -667,11 +670,7 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { {[...versions] .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) .map((version) => ( - + { aria-label="Version name" autoFocus onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveRename(version.id); - if (e.key === 'Escape') handleCancelRename(); + if (e.key === 'Enter') { + handleSaveRename(version.id); + } + if (e.key === 'Escape') { + handleCancelRename(); + } }} /> @@ -719,7 +722,10 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { - + {version.author === 'bot' ? 'Bot' : 'You'} · {formatTimestamp(version.timestamp)} @@ -945,7 +951,8 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { {versionToRestore && (

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

)}
@@ -1013,7 +1020,8 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { {versionToRestore && (

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

)}
@@ -1136,7 +1144,11 @@ export const VersionHistorySplitScreen: FunctionComponent = () => { additionalActions={ <> {showCanvasLabel && ( -
@@ -614,7 +621,10 @@ export const VersionHistorySplitScreenV1: FunctionComponent = () => {
- + {version.author === 'bot' ? 'Bot' : 'You'} · {formatTimestamp(version.timestamp)} @@ -800,7 +810,8 @@ export const VersionHistorySplitScreenV1: FunctionComponent = () => { {versionToRestore && (

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

)}
@@ -865,7 +876,8 @@ export const VersionHistorySplitScreenV1: FunctionComponent = () => { {versionToRestore && (

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

)}
@@ -988,7 +1000,11 @@ export const VersionHistorySplitScreenV1: FunctionComponent = () => { additionalActions={ <> {showCanvasLabel && ( -
- - -
-
- { - 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 && ( - - )} - - - } - /> - - -
-
-
-
- } - /> -
-
- - ); -};