diff --git a/.eslintrc.json b/.eslintrc.json index d96089ba3..23c466e7b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -95,6 +95,18 @@ "react-hooks/exhaustive-deps": "warn", "react/no-unescaped-entities": ["error", { "forbid": [">", "}"] }], "spaced-comment": "error", - "use-isnan": "error" + "use-isnan": "error", + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "react-jss", + "importNames": ["createUseStyles"], + "message": "Do not import { createUseStyles } from 'react-jss'; Use { createVaStyles, defaultTheme } from '../VirtualAssistantTheme' instead!" + } + ] + } + ] } } diff --git a/.github/workflows/extensions.yml b/.github/workflows/extensions.yml index 3e27da6a0..87ba2f781 100644 --- a/.github/workflows/extensions.yml +++ b/.github/workflows/extensions.yml @@ -14,3 +14,27 @@ jobs: with: project-url: https://github.com/orgs/patternfly/projects/7 github-token: ${{ secrets.GH_PROJECTS }} + label-issue: + runs-on: ubuntu-latest + steps: + - name: Team Membership Checker + # You may pin to the exact commit or the version. + # uses: TheModdingInquisition/actions-team-membership@a69636a92bc927f32c3910baac06bacc949c984c + uses: TheModdingInquisition/actions-team-membership@v1.0 + with: + # Repository token. GitHub Action token is used by default(recommended). But you can also use the other token(e.g. personal access token). + token: ${{ secrets.GH_READ_ORG_TOKEN }} + # The team to check for. + team: 'frequent-flyers' + # The organization of the team to check for. Defaults to the context organization. + organization: 'patternfly' + # If the action should exit if the user is not part of the team. + exit: true + + - name: Add label if user is a team member + run: | + curl -X POST \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/labels \ + -d '{"labels":["PF Team"]}' diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 04f9ddfe4..91d5aeb58 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -17,10 +17,10 @@ jobs: git checkout tmp - run: | - git rev-parse origin/main + git rev-parse origin/v5 git rev-parse HEAD - git rev-parse origin/main..HEAD - git log origin/main..HEAD --format="%b" + git rev-parse origin/v5..HEAD + git log origin/v5..HEAD --format="%b" # Yes, we really want to checkout the PR # Injected by generate-workflows.js @@ -52,4 +52,4 @@ jobs: name: a11y tests - run: node .github/upload-preview.js packages/module/coverage name: Upload a11y report - if: always() \ No newline at end of file + if: always() diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index c4abb4b77..f5aa01ed1 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -3,7 +3,7 @@ on: push: # Sequence of patterns matched against refs/tags tags: - - v5.* + - v2.* jobs: build-and-promote: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eee03e2d4..548736a28 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ on: push: branches: - main - - v4 + - v5 jobs: call-build-lint-test-workflow: uses: ./.github/workflows/build-lint-test.yml diff --git a/README.md b/README.md index 56a418004..b3fb451f4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This repo contains React Virtual assistant implementation. ``` import * as React from 'react'; import { Text } from '@patternfly/react-core'; -import { createUseStyles } from 'react-jss'; +import { createVaStyles } from '../VirtualAssistantTheme'; // do not forget to export your component's interface // always place the component's interface above the component itself in the code @@ -23,7 +23,7 @@ export interface MyComponentProps { text: String; } -const useStyles = createUseStyles({ +const useStyles = createVaStyles({ myText: { fontFamily: 'monospace', fontSize: 'var(--pf-v5-global--icon--FontSize--md)', diff --git a/cypress/component/Citations.cy.tsx b/cypress/component/Citations.cy.tsx new file mode 100644 index 000000000..ad008b084 --- /dev/null +++ b/cypress/component/Citations.cy.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import Citations, { CitationsProps } from '../../packages/module/src/Citations'; + +const testCitations: CitationsProps['citations'] = [ + { + id: 'citation-1', + title: 'Understanding PatternFly Layouts', + content: 'Content explaining the use of various layouts in PatternFly.', + ouiaId: 'citation-1', + }, + { + id: 'citation-2', + title: 'PatternFly Design Guidelines', + content: 'Content about design guidelines.', + ouiaId: 'citation-2', + }, + { + id: 'citation-3', + title: 'Accessibility in PatternFly', + content: 'Content about accessibility practices.', + ouiaId: 'citation-3', + }, +]; + +describe('Citations', () => { + it('renders expandable section with citations', () => { + cy.mount(); + + cy.get('[data-ouia-component-id="test-citations-expandable-section"]').should('have.length', 1); + cy.get('[data-ouia-component-id="test-citations-accordion"]').should('not.be.visible'); + }); + + it('expands and displays citations on toggle', () => { + cy.mount(); + + cy.get('[data-ouia-component-id="test-citations-expandable-section"] button').first().click(); + cy.get('[data-ouia-component-id="test-citations-accordion"]').should('be.visible'); + + cy.get('[data-ouia-component-id="citation-1-toggle"]').should('have.length', 1); + cy.get('[data-ouia-component-id="citation-2-toggle"]').should('have.length', 1); + cy.get('[data-ouia-component-id="citation-3-toggle"]').should('have.length', 1); + }); + + it('toggles citations within the accordion', () => { + cy.mount(); + + cy.get('[data-ouia-component-id="test-citations-expandable-section"] button').first().click(); + + cy.get('[data-ouia-component-id="citation-1-toggle"]').click(); + cy.get('[data-ouia-component-id="citation-1-content"]').should('be.visible'); + + cy.get('[data-ouia-component-id="citation-2-toggle"]').click(); + cy.get('[data-ouia-component-id="citation-1-content"]').should('be.visible'); + cy.get('[data-ouia-component-id="citation-1-toggle"]').click(); + cy.get('[data-ouia-component-id="citation-1-content"]').should('not.be.visible'); + cy.get('[data-ouia-component-id="citation-2-content"]').should('be.visible'); + }); +}); \ No newline at end of file diff --git a/cypress/component/LoadingMessage.cy.tsx b/cypress/component/LoadingMessage.cy.tsx index a2f28c4e1..9a35c2f47 100644 --- a/cypress/component/LoadingMessage.cy.tsx +++ b/cypress/component/LoadingMessage.cy.tsx @@ -1,17 +1,32 @@ import React from 'react'; -import LoadingMessage from '../../packages/module/src/LoadingMessage'; -import GrinIcon from '@patternfly/react-icons/dist/js/icons/grin-icon'; +import { ThemeProvider } from 'react-jss'; +import GrinIcon from '@patternfly/react-icons/dist/dynamic/icons/grin-icon'; +import LoadingMessage from '@patternfly/virtual-assistant/src/LoadingMessage'; +import { VirtualAssistantProvider } from '@patternfly/virtual-assistant/src/VirtualAssistantContext'; +import { defaultTheme } from '@patternfly/virtual-assistant/src/VirtualAssistantTheme'; describe('LoadingMessage', () => { it('renders default loading message', () => { - cy.mount(); + cy.mount( + + + + + + ); cy.get('[data-test-id="assistant-loading-icon"]').should('have.length', 1); cy.get('[data-test-id="assistant-loading-dots"]').should('have.length', 1); }) it('renders custom loading message', () => { - cy.mount(); + cy.mount( + + + + + + ); cy.get('[data-test-id="assistant-loading-icon"]').should('have.length', 1); cy.get('[data-test-id="assistant-loading-dots"]').should('have.length', 1); diff --git a/cypress/component/VirtualAssistant.cy.tsx b/cypress/component/VirtualAssistant.cy.tsx index 0ec9221fc..4c68c46b1 100644 --- a/cypress/component/VirtualAssistant.cy.tsx +++ b/cypress/component/VirtualAssistant.cy.tsx @@ -1,34 +1,49 @@ import React from 'react'; +import { CardHeader } from '@patternfly/react-core'; import VirtualAssistant from '../../packages/module/src/VirtualAssistant'; -describe('VirtualAssistant', () => { +describe('VirtualAssistant', () => { it('renders virtual assistant body', () => { cy.mount(); - - cy.get('[data-test-id="assistant-title"]').first().should('contain', 'Virtual Assistant'); - cy.get('[data-test-id="assistant-text-input"]').first().should('have.attr', 'placeholder', 'Type a message...'); + + cy.get('[data-ouia-component-id="VirtualAssistant-title"]').first().should('contain', 'Virtual Assistant'); + cy.get('[data-test-id="assistant-text-input"]').first().should('have.attr', 'placeholder', 'Send a message...'); cy.get('[data-test-id="assistant-send-button"]').first().should('not.be.disabled'); - }) + }); it('renders a customized title and placeholder', () => { cy.mount(); - - cy.get('[data-test-id="assistant-title"]').should('contain', 'PatternFly assistant'); + + cy.get('[data-ouia-component-id="VirtualAssistant-title"]').should('contain', 'PatternFly assistant'); cy.get('[data-test-id="assistant-text-input"]').should('have.attr', 'placeholder', 'You can ask anything in here.'); cy.get('[data-test-id="assistant-send-button"]').should('not.be.disabled'); - }) + }); it('listens to messages', () => { cy.mount(); - + cy.get('[data-test-id="assistant-text-input"]').type('my message'); cy.get('[data-test-id="assistant-send-button"]').click(); cy.get('@change').should('have.been.called'); cy.get('@send').should('have.been.called'); - }) + }); it('renders header with disabled send button', () => { cy.mount(); + cy.get('[data-test-id="assistant-send-button"]').should('be.disabled'); - }) -}) \ No newline at end of file + }); + + it('renders in full-page mode', () => { + cy.mount(); + + cy.get('[data-ouia-component-id="VirtualAssistant-title"]').should('contain', 'Full Page Assistant'); + cy.get('.fullPage').should('exist'); + }); + + it('renders a custom header', () => { + cy.mount(Custom Header} />); + + cy.get('[data-ouia-component-id="custom-header"]').should('contain', 'Custom Header'); + }); +}); \ No newline at end of file diff --git a/cypress/component/VirtualAssistantAction.cy.tsx b/cypress/component/VirtualAssistantAction.cy.tsx index 5422a1f71..57102e36e 100644 --- a/cypress/component/VirtualAssistantAction.cy.tsx +++ b/cypress/component/VirtualAssistantAction.cy.tsx @@ -1,12 +1,18 @@ import React from 'react'; +import { ThemeProvider } from 'react-jss'; import VirtualAssistantAction from '@patternfly/virtual-assistant/src/VirtualAssistantAction'; import { AngleDownIcon } from '@patternfly/react-icons'; +import { defaultTheme } from '@patternfly/virtual-assistant/src/VirtualAssistantTheme'; describe('VirtualAssistantAction', () => { it('renders assistant action', () => { - cy.mount( - - ); + cy.mount( + + + + + + ); cy.get('[aria-label="Minimize virtual assistant"]').click(); cy.get('@action').should('have.been.called'); cy.get('.pf-v5-svg').should('exist'); diff --git a/cypress/e2e/VirtualAssistant.spec.cy.ts b/cypress/e2e/VirtualAssistant.spec.cy.ts index 1539f8ed9..24d9b7818 100644 --- a/cypress/e2e/VirtualAssistant.spec.cy.ts +++ b/cypress/e2e/VirtualAssistant.spec.cy.ts @@ -5,8 +5,8 @@ describe('Test the Virtual assistant docs page', () => { cy.wait(1000); cy.get('[data-test-id="assistant-example-message"]').should('contain', 'Last received message: '); - cy.get('[data-test-id="assistant-text-input"]').eq(2).type('my message'); - cy.get('[data-test-id="assistant-send-button"]').eq(2).click({ force: true }); + cy.get('[data-test-id="assistant-text-input"]').eq(5).type('my message'); + cy.get('[data-test-id="assistant-send-button"]').eq(5).click({ force: true }); cy.get('[data-test-id="assistant-example-message"]').should('contain', 'Last received message: my message'); }) diff --git a/packages/module/generate-index.js b/packages/module/generate-index.js index f2d29ef65..38fedf5d1 100644 --- a/packages/module/generate-index.js +++ b/packages/module/generate-index.js @@ -20,7 +20,7 @@ async function generateIndex(files) { files.forEach(file => { const name = file.replace('/index.ts', '').split('/').pop(); - stream.write(`\nexport { default as ${name} } from './${name}';\n`); + name !== 'VirtualAssistantContext' && stream.write(`\nexport { default as ${name} } from './${name}';\n`); stream.write(`export * from './${name}';\n`); }); stream.end(); diff --git a/packages/module/package.json b/packages/module/package.json index a151dba0d..2aa2fa142 100644 --- a/packages/module/package.json +++ b/packages/module/package.json @@ -27,8 +27,7 @@ }, "homepage": "https://github.com/patternfly/virtual-assistant#readme", "publishConfig": { - "access": "public", - "tag": "prerelease" + "access": "public" }, "dependencies": { "@patternfly/react-core": "^5.1.2", diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/about-virtual-assistant.md b/packages/module/patternfly-docs/content/extensions/virtual-assistant/about-virtual-assistant.md index 778060800..75502fcc2 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/about-virtual-assistant.md +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/about-virtual-assistant.md @@ -9,6 +9,36 @@ Virtual assistant lives in its own package [`@patternfly/virtual-assistant`](htt # Virtual assistant -The virtual assistant extension contains implementation of the react virtual assistant. +The Virtual Assistant extension provides UI components for building a virtual assistant layout, leveraging the design system foundations of PatternFly. These components are React-based and contain no additional logic. + +You can take advantage of either the default appearance provided or make advanced customizations through the component's API to make the virtual assistant meet your specific needs. + + + +## Getting started + +To use this extension in your project, ensure that you have the necessary packages installed and always use the latest versions: + +``` +// package.json + +"dependencies": { + "@patternfly/patternfly": "^5.3.1", + "@patternfly/react-core": "^5.3.4", + "@patternfly/react-styles": "^5.3.1", + "@patternfly/virtual-assistant": "1.0.0-prerelease.9", +}, +``` +### Importing stylesheets +If you haven't done so already, import the required stylesheets in your application: + +``` +import '@patternfly/react-core/dist/styles/base.css'; +import '@patternfly/patternfly/patternfly-addons.css'; +``` + +--- + +## Contributions and feedback If you notice a bug or have a suggestion for the virtual assistant, feel free to file an issue in our [GitHub repository](https://github.com/patternfly/virtual-assistant/issues)! Please make sure to check if there is already a pre-existing issue before creating a new issue. diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/AssistantMessageNoRadiusExample.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/AssistantMessageNoRadiusExample.tsx new file mode 100644 index 000000000..666781e27 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/AssistantMessageNoRadiusExample.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import VirtualAssistant from '@patternfly/virtual-assistant/dist/dynamic/VirtualAssistant'; +import AssistantMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/AssistantMessageEntry'; + +const noRadiusTheme = { + global: { + borderRadiusBubble: '0' + }, + components: { + VirtualAssistant: { + card: { + borderRadius: '0', + }, + textArea: { + borderRadius: "0", + } + }, + AssistantMessageEntry: { + label: { + borderRadius: '0' + } + } + } +} + +export const BasicExample: React.FunctionComponent = () => ( + + {console.log('This is an example of onClick event')} } }, { title: "Option #2" }, { title: "Option #3" } ]} + > + How may I help you today? Do you have some question for me? + + +); diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/AssistantMessageWithDropdown.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/AssistantMessageWithDropdown.tsx new file mode 100644 index 000000000..5850808ed --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/AssistantMessageWithDropdown.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import VirtualAssistant from '@patternfly/virtual-assistant/dist/dynamic/VirtualAssistant'; +import AssistantMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/AssistantMessageEntry'; + +export const AssistantMessage: React.FunctionComponent = () => ( + + {console.log('This is an example of onClick event')} } } ] } + } + > + Here are a few things I can help you with. Select an option below or type in your questions. + + +); diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistant.md b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistant.md index 06c5fb0c1..4adb2d20f 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistant.md +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistant.md @@ -10,7 +10,7 @@ id: Virtual assistant source: react # If you use typescript, the name of the interface to display props for # These are found through the sourceProps function provided in patternfly-docs.source.js -propComponents: ['VirtualAssistant'] +propComponents: ['VirtualAssistant', 'VirtualAssistantHeader', 'VirtualAssistantAction', 'SystemMessageEntry', 'LoadingMessage', 'ConversationAlert', 'AssistantMessageEntry', 'UserMessageEntry', 'Citation', 'Citations'] sourceLink: https://github.com/patternfly/virtual-assistant/blob/main/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistant.md --- @@ -18,11 +18,12 @@ import VirtualAssistant from '@patternfly/virtual-assistant/dist/dynamic/Virtual import VirtualAssistantAction from '@patternfly/virtual-assistant/dist/dynamic/VirtualAssistantAction'; import SystemMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/SystemMessageEntry'; import LoadingMessage from '@patternfly/virtual-assistant/dist/dynamic/LoadingMessage'; -import { GrinIcon } from '@patternfly/react-icons'; -import { AngleDownIcon } from '@patternfly/react-icons'; +import EllipsisVIcon from '@patternfly/react-icons/dist/dynamic/icons/ellipsis-v-icon'; +import { GrinIcon, AngleDownIcon, UserIcon } from '@patternfly/react-icons'; import ConversationAlert from '@patternfly/virtual-assistant/dist/dynamic/ConversationAlert'; import AssistantMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/AssistantMessageEntry'; import UserMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/UserMessageEntry'; +import Citations from '@patternfly/virtual-assistant/dist/dynamic/Citations'; The **virtual assistant** component renders body of the virtual assistant window. @@ -34,6 +35,32 @@ A blank example of the virtual assistant body. ``` +### Full page example + +You can make the assistant body use whole available space with the `isFullPage` property. + +```js file="./VirtualAssistantFullPageExample.tsx" + +``` + +### Using custom actions + +Custom actions can be added to the assistant body using the `actions` property. + + +```js file="./VirtualAssistantWithActions.tsx" + +``` + +### Using custom header + +You can override the default header layout using the `header` property accepting any React node. It is recommended to use the original `CardHeader` component as a wrapper for your custom header. The default virtual assistant header component is also exported as `VirtualAssistantHeader` component. + + +```js file="./VirtualAssistantCustomHeaderExample.tsx" + +``` + ### Customizing input title and placeholder You can configure a custom title and placeholder input value using `title` and `inputPlaceholder` props. @@ -59,15 +86,6 @@ Disabling the send button using `isSendButtonDisabled` prevents it from being c ``` -### Using custom actions - -Custom actions can be added to the assistant body using the `actions` property. - - -```js file="./VirtualAssistantWithActions.tsx" - -``` - ### Conversation Alert You can configure a custom title and variant input value using `title` and `variant` props. @@ -110,10 +128,34 @@ This is an example of a message sent by assistant with follow-up options. Follow ``` +### Assistant Message with dropdown options + +This is an example of a message sent by assistant with dropdown options. Follow-up options are defined within `dropdown` property. + +```js file="./AssistantMessageWithDropdown.tsx" + +``` + ### User Message -This is an example of a message sent by user. Additionally, it allows for the use of a custom icon through the `icon` property. +This is an example of a message sent by user. ```js file="./UserMessage.tsx" ``` + +### Adding citations + +You can use the citations component to render an accordion of sources as a part of the assistant response. + +```js file="./VirtualAssistantCitationsExample.tsx" + +``` + +### Using custom theme + +In case you need to customize the look and feel of your virtual assistant, you can use your custom JSS theme and pass it through the `theme` property to the virtual assistant component. It will be merged with the default theme, which is also exported as `defaultTheme`. + +```js file="./AssistantMessageNoRadiusExample.tsx" + +``` diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistantCitationsExample.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistantCitationsExample.tsx new file mode 100644 index 000000000..6d23a7a4f --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistantCitationsExample.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import VirtualAssistant from '@patternfly/virtual-assistant/dist/dynamic/VirtualAssistant'; +import AssistantMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/AssistantMessageEntry'; +import UserMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/UserMessageEntry'; +import Citations from '@patternfly/virtual-assistant/dist/dynamic/Citations'; + +const citations = [ + { + id: 'citation-1', + title: 'Using Flex Layout in PatternFly', + content: 'Flex layout is a powerful utility in PatternFly that allows you to easily create responsive, flexible grid layouts. It provides various options for direction, alignment, and spacing of grid items.', + ouiaId: 'ouia-citation-1', + }, + { + id: 'citation-2', + title: 'PatternFly Grid System', + content: 'The PatternFly grid system is built on a flexible 12-column layout, providing consistency and alignment for different screen sizes and device types. It helps in building complex layouts with ease.', + ouiaId: 'ouia-citation-2', + }, + { + id: 'citation-3', + title: 'Understanding PatternFly Spacing', + content: 'PatternFly provides a comprehensive set of spacing utilities to help control the margin and padding around elements. These utilities follow a consistent scale to ensure visual rhythm and harmony.', + ouiaId: 'ouia-citation-3', + }, + { + id: 'citation-4', + title: 'PatternFly Vertical Navigation', + content: 'Vertical navigation in PatternFly is a crucial layout component for creating side navigation menus. It supports collapsible sections, icons, and badges, making it ideal for hierarchical structures.', + ouiaId: 'ouia-citation-4', + }, + { + id: 'citation-5', + title: 'Responsive Design with PatternFly', + content: 'PatternFly ensures responsiveness across different devices by using fluid grids, flexible layouts, and media queries. It allows developers to build layouts that adapt to various screen sizes seamlessly.', + ouiaId: 'ouia-citation-5', + }, +]; + +export const BasicExample: React.FunctionComponent = () => ( + + Hello, where can find information about PatternFly layouts? + + Here are resources you may find useful + + + +); diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistantCustomHeaderExample.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistantCustomHeaderExample.tsx new file mode 100644 index 000000000..8249cf1dc --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistantCustomHeaderExample.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import VirtualAssistant from '@patternfly/virtual-assistant/dist/dynamic/VirtualAssistant'; +import { CardHeader, Dropdown, DropdownItem, DropdownList, MenuToggle, MenuToggleElement } from '@patternfly/react-core'; +import EllipsisVIcon from '@patternfly/react-icons/dist/dynamic/icons/ellipsis-v-icon'; +import UserIcon from '@patternfly/react-icons/dist/dynamic/icons/user-icon'; + +export const BasicExample: React.FunctionComponent = () => { + const [ isOpen, setIsOpen ] = React.useState(false); + + const headerActions = ( + <> + ) => ( + setIsOpen(!isOpen)} + variant="plain" + aria-label="Card header images and actions example kebab toggle" + > + + )} + isOpen={isOpen} + onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} + > + + Action + Action 2 + + + + ); + + return ( + + My custom header + + } /> + )} diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistantFullPageExample.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistantFullPageExample.tsx new file mode 100644 index 000000000..33ff51c23 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/VirtualAssistant/VirtualAssistantFullPageExample.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import VirtualAssistant from '@patternfly/virtual-assistant/dist/dynamic/VirtualAssistant'; + +export const BasicExample: React.FunctionComponent = () => ( + +); diff --git a/packages/module/release.config.js b/packages/module/release.config.js index 87f67383a..9c599501e 100644 --- a/packages/module/release.config.js +++ b/packages/module/release.config.js @@ -1,5 +1,5 @@ module.exports = { - branches: [ 'do-not-delete', { name: 'main', channel: 'prerelease', prerelease: 'prerelease' } ], + branches: [ 'do-not-delete', { name: 'v5', channel: 'prerelease-v1' } ], analyzeCommits: { preset: 'angular' }, @@ -10,5 +10,5 @@ module.exports = { '@semantic-release/npm' ], tagFormat: 'prerelease-v${version}', - dryRun: false -}; \ No newline at end of file + dryRun: true +}; diff --git a/packages/module/src/AssistantMessageEntry/AssistantMessageEntry.tsx b/packages/module/src/AssistantMessageEntry/AssistantMessageEntry.tsx index b1989ec1e..cebaff0e8 100644 --- a/packages/module/src/AssistantMessageEntry/AssistantMessageEntry.tsx +++ b/packages/module/src/AssistantMessageEntry/AssistantMessageEntry.tsx @@ -1,60 +1,173 @@ import React, { PropsWithChildren } from 'react'; -import { Icon, Label, Split, SplitItem, TextContent, LabelProps } from '@patternfly/react-core'; -import { createUseStyles } from 'react-jss'; +import { Dropdown, DropdownItem, DropdownList, Icon, Label, MenuToggle, MenuToggleElement, Split, SplitItem, TextContent, LabelProps, DropdownItemProps, DropdownProps } from '@patternfly/react-core'; import classnames from "clsx"; +import { useVirtualAssistantContext } from '../VirtualAssistantContext'; +import { createVaStyles } from '../VirtualAssistantTheme'; -import RobotIcon from '@patternfly/react-icons/dist/js/icons/robot-icon'; - -const useStyles = createUseStyles({ +const useStyles = createVaStyles((theme) => ({ chatbot: { marginRight: "40px", }, bubble: { - borderRadius: "14px", + borderRadius: theme.global.borderRadiusBubble, padding: "var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md) var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md)", maxWidth: "100%", wordWrap: "break-word", + }, + dropdownBlock: { + marginTop: "var(--pf-v5-global--spacer--sm)" + }, + label: { + backgroundColor: theme.global.colors.background100, + "--pf-v5-c-label--BorderRadius": theme.components.AssistantMessageEntry.label.borderRadius, + "--pf-v5-c-label__content--before--BorderColor": theme.global.colors.primary, + "--pf-v5-c-label--PaddingBottom": ".3rem", + "--pf-v5-c-label--PaddingRight": "1rem", + "--pf-v5-c-label--PaddingLeft": "1rem", + "--pf-v5-c-label--PaddingTop": ".3rem", + }, + activeOption: { + background: theme.global.colors.primary, + pointerEvents: "none", + "--pf-v5-c-label__content--before--BorderColor": theme.global.colors.primary, + "--pf-v5-c-label--m-outline__content--link--hover--before--BorderColor": theme.global.colors.primary, + "--pf-v5-c-label__content--link--focus--before--BorderColor": theme.global.colors.primary, + "& .pf-v5-c-label__content": { + color: theme.global.colors.background100, + }, + }, + inactiveOption: { + background: theme.global.colors.backgroundPrimaryInactive, + opacity: "0.6", + pointerEvents: "none", + "--pf-v5-c-label__content--before--BorderColor": `${theme.global.colors.backgroundPrimaryInactive} !important`, + "& .pf-v5-c-label__content": { + color: theme.global.colors.primaryInactive, + }, } -}) +})) interface AssistantMessageEntryProps { + /** message title for the assistant */ + title?: React.ReactNode; options?: { title: React.ReactNode; - props?: LabelProps - }[], + props?: LabelProps; + }[]; icon?: React.ComponentType; + dropdown?: { + items: { label: React.ReactNode; props?: DropdownItemProps; }[]; + dropdownProps?: DropdownProps; + }; } -export const AssistantMessageEntry = ({ children, options, icon: IconComponent = RobotIcon }: PropsWithChildren) => { +export const AssistantMessageEntry = ({ + children, + options, + title = 'Virtual Assistant', + icon: IconComponent, + dropdown, +}: PropsWithChildren) => { + const [ selectedOptionIndex, setSelectedOptionIndex ] = React.useState(); + const [ isOpen, setIsOpen ] = React.useState(false); + const [ selected, setSelected ] = React.useState(); + const { assistantIcon: AssistantIcon } = useVirtualAssistantContext(); const classes = useStyles(); + + const handleOptionClick = (event: React.MouseEvent, index: number, customOnClick?: (event: React.MouseEvent) => void) => { + setSelectedOptionIndex(index); + if (customOnClick) { + customOnClick(event); + } + }; + + const handleDropdownClick = (event: React.MouseEvent, index: number, customOnClick?: (event: React.MouseEvent) => void) => { + if (customOnClick) { + customOnClick(event); + } + }; + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const onSelect = (_event: React.MouseEvent | undefined, value: string | number | undefined) => { + setSelected(value); + setIsOpen(false); + }; + return (
- + {IconComponent ? : } - - - {children} + + + {title} +
+ + {children} + + {dropdown ? ( +
+ ) => ( + + {selected ? selected : "A few things I can help you with" } + + )}> + + {dropdown.items.map((option, key) => { + const { onClick: customOnClick, ...dropdownProps } = option.props || {}; + return ( + handleDropdownClick(event, key, customOnClick)}> + {option.label} + + ); + })} + + +
+ ) : null} +
- {options ? ( + {options ? ( - - {options.map((option, index) => ( - - ))} + + {options.map((option, index) => { + const { onClick: customOnClick, ...restProps } = option.props || {}; + return ( + + ); + })} - ) : null - } + ) : null}
); }; -export default AssistantMessageEntry; \ No newline at end of file +export default AssistantMessageEntry; diff --git a/packages/module/src/Citation/Citation.tsx b/packages/module/src/Citation/Citation.tsx new file mode 100644 index 000000000..c0579e66d --- /dev/null +++ b/packages/module/src/Citation/Citation.tsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import { + AccordionItem, + AccordionToggle, + AccordionContent, + AccordionItemProps, +} from '@patternfly/react-core'; + +export interface CitationProps extends AccordionItemProps { + /** Citation ID passed to the toggle */ + id?: string; + /** Citation title */ + title: React.ReactNode; + /** Citation content */ + content: React.ReactNode; + /** Citation OUIA ID */ + ouiaId?: string; +} + +export const Citation:React.FunctionComponent = ({ + id, + title, + content, + ouiaId = 'Citation', + ...props +}: CitationProps) => { + const [ expanded, setExpanded ] = useState(false); + return ( + + setExpanded(!expanded)} + isExpanded={expanded} + > + {title} + + + { expanded ? content : null } + + + ); +}; + +export default Citation; diff --git a/packages/module/src/Citation/index.ts b/packages/module/src/Citation/index.ts new file mode 100644 index 000000000..e95b35087 --- /dev/null +++ b/packages/module/src/Citation/index.ts @@ -0,0 +1,3 @@ +export { default } from './Citation'; + +export * from './Citation'; \ No newline at end of file diff --git a/packages/module/src/Citations/Citations.tsx b/packages/module/src/Citations/Citations.tsx new file mode 100644 index 000000000..8c5406b1d --- /dev/null +++ b/packages/module/src/Citations/Citations.tsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react'; +import { + Accordion, + AccordionProps, + ExpandableSection, + ExpandableSectionProps, +} from '@patternfly/react-core'; +import Citation, { CitationProps } from '../Citation'; + +export interface CitationsProps extends Omit { + /** Citations to be displayed in an accordion */ + citations: CitationProps[]; + /** Citations OUIA ID */ + ouiaId?: string; + /** Citations accordion props */ + accordionProps?: AccordionProps; +} + +export const Citations:React.FunctionComponent = ({ + toggleText = "Citations", + citations, + ouiaId, + accordionProps, + ...props +}: CitationsProps) => { + const [ expanded, setExpanded ] = useState(false); + return ( + setExpanded(!expanded)} + toggleText={toggleText} + {...props} + > + {citations.map((citation, index) => )} + + ); +}; + +export default Citations; diff --git a/packages/module/src/Citations/index.ts b/packages/module/src/Citations/index.ts new file mode 100644 index 000000000..bae731575 --- /dev/null +++ b/packages/module/src/Citations/index.ts @@ -0,0 +1,3 @@ +export { default } from './Citations'; + +export * from './Citations'; \ No newline at end of file diff --git a/packages/module/src/ConversationAlert/ConversationAlert.tsx b/packages/module/src/ConversationAlert/ConversationAlert.tsx index f25d36fdf..d3f897d02 100644 --- a/packages/module/src/ConversationAlert/ConversationAlert.tsx +++ b/packages/module/src/ConversationAlert/ConversationAlert.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Alert, TextContent } from '@patternfly/react-core'; - -import { createUseStyles } from 'react-jss'; +import { createVaStyles } from '../VirtualAssistantTheme'; export interface ConversationAlertProps { /** Content for conversation alert */ @@ -11,7 +10,7 @@ export interface ConversationAlertProps { children?: React.ReactNode; } -const useStyles = createUseStyles({ +const useStyles = createVaStyles({ banner: { paddingTop: "0", paddingBottom: "var(--pf-v5-global--spacer--md)", diff --git a/packages/module/src/LoadingMessage/LoadingMessage.tsx b/packages/module/src/LoadingMessage/LoadingMessage.tsx index ded2532b6..f0348f515 100644 --- a/packages/module/src/LoadingMessage/LoadingMessage.tsx +++ b/packages/module/src/LoadingMessage/LoadingMessage.tsx @@ -1,17 +1,17 @@ import React from 'react'; import { Icon, Split, SplitItem } from '@patternfly/react-core'; -import { createUseStyles } from 'react-jss'; import classnames from "clsx"; -import RobotIcon from '@patternfly/react-icons/dist/js/icons/robot-icon'; +import { useVirtualAssistantContext } from '../VirtualAssistantContext'; +import { createVaStyles } from '../VirtualAssistantTheme'; -const useStyles = createUseStyles({ +const useStyles = createVaStyles((theme) => ({ chatbot: { marginBottom: "var(--pf-v5-global--spacer--md)", marginRight: "40px", }, bubble: { - borderRadius: "14px", + borderRadius: theme.global.borderRadiusBubble, padding: "var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md) var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md)", maxWidth: "100%", wordWrap: "break-word", @@ -56,23 +56,25 @@ const useStyles = createUseStyles({ }, } } -}) +})) export interface LoadingMessageProps { + /** Icon component to display in the loading message */ icon?: React.ComponentType; } -export const LoadingMessage: React.FunctionComponent = ({ icon: IconComponent = RobotIcon }) => { +export const LoadingMessage: React.FunctionComponent = ({ icon: IconComponent }: LoadingMessageProps) => { + const { assistantIcon: AssistantIcon } = useVirtualAssistantContext(); const classes = useStyles(); return ( - + {IconComponent ? : } - -
+ +
diff --git a/packages/module/src/SystemMessageEntry/SystemMessageEntry.tsx b/packages/module/src/SystemMessageEntry/SystemMessageEntry.tsx index b0e0c413c..59217cca9 100644 --- a/packages/module/src/SystemMessageEntry/SystemMessageEntry.tsx +++ b/packages/module/src/SystemMessageEntry/SystemMessageEntry.tsx @@ -1,20 +1,20 @@ import React from 'react'; import { Text, TextContent, TextVariants } from '@patternfly/react-core'; -import { createUseStyles } from 'react-jss'; +import { createVaStyles } from '../VirtualAssistantTheme'; export interface SystemMessageEntryProps { /** Message rendered within the system message entry */ children: React.ReactNode; } -const useStyles = createUseStyles({ +const useStyles = createVaStyles({ systemMessageText: { paddingBottom: "var(--pf-v5-global--spacer--md)", textAlign: "center", } }) -export const SystemMessageEntry: React.FunctionComponent = (props) => { +export const SystemMessageEntry: React.FunctionComponent = (props: SystemMessageEntryProps) => { const classes = useStyles(); return ( diff --git a/packages/module/src/UserMessageEntry/UserMessageEntry.tsx b/packages/module/src/UserMessageEntry/UserMessageEntry.tsx index 5939f9088..2803489ef 100644 --- a/packages/module/src/UserMessageEntry/UserMessageEntry.tsx +++ b/packages/module/src/UserMessageEntry/UserMessageEntry.tsx @@ -1,45 +1,39 @@ import React, { PropsWithChildren } from 'react'; -import { Icon, Split, SplitItem, TextContent } from '@patternfly/react-core'; -import OutlinedUserIcon from '@patternfly/react-icons/dist/js/icons/outlined-user-icon'; -import { createUseStyles } from 'react-jss'; -import classnames from "clsx"; +import { Split, SplitItem, TextContent } from '@patternfly/react-core'; +import clsx from "clsx"; +import { createVaStyles } from '../VirtualAssistantTheme'; -const useStyles = createUseStyles({ +const useStyles = createVaStyles((theme) => ({ user: { margin: "0 0 12px 40px", }, bubbleUser: { - border: "1px solid var(--pf-v5-global--BackgroundColor--dark-400)", - borderRadius: "14px", + backgroundColor: theme.global.colors.primary, + borderRadius: theme.global.borderRadiusBubble, + color: "#fff", padding: "var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md) var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md)", maxWidth: "100%", wordWrap: "break-word", } -}) +})); interface UserMessageEntryProps { + /** User message entry icon */ icon?: React.ComponentType; } -const UserMessageEntry = ({ children, icon: IconComponent = OutlinedUserIcon }: PropsWithChildren) => { +const UserMessageEntry = ({ children }: PropsWithChildren) => { const classes = useStyles(); return ( - <> - - - - {children} - - - - - - - - - + + + + {children} + + + ); }; -export default UserMessageEntry; \ No newline at end of file +export default UserMessageEntry; diff --git a/packages/module/src/VirtualAssistant/VirtualAssistant.test.tsx b/packages/module/src/VirtualAssistant/VirtualAssistant.test.tsx index 16d78cd34..6d4cdf035 100644 --- a/packages/module/src/VirtualAssistant/VirtualAssistant.test.tsx +++ b/packages/module/src/VirtualAssistant/VirtualAssistant.test.tsx @@ -37,6 +37,14 @@ describe('VirtualAssistant', () => { expect(screen.getByText('I am the message')).toBeTruthy(); }); + it('should use custom icon', () => { + const MyIcon: React.FunctionComponent = () => FakeIcon; + render(); + expect(screen.getByText('FakeIcon')).toBeTruthy(); + }); + it('should listen to message changes', async () => { const listener = jest.fn(); render((target: T, ...sources: Partial[]): T { + if (!sources.length) {return target;} + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + const sourceValue = source[key]; + if (isObject(sourceValue) && isObject(target[key])) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + target[key] = deepMerge(target[key] as any, sourceValue as any); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (target as any)[key] = sourceValue; + } + } + } + } + // To avoid cross-modification of themes when using multiple instances on the same screen, a new object has to be returned here. + return deepMerge({ ...target }, ...sources); +} + +const useStyles = createVaStyles((theme) => ({ card: { - width: "350px", - height: "550px", + width: "400px", + height: "600px", overflow: "hidden", + borderRadius: theme.components.VirtualAssistant.card.borderRadius, "@media screen and (max-width: 768px)": { height: "420px", width: "100%", }, - }, - cardHeader: { - background: "var(--pf-v5-global--BackgroundColor--dark-400)", - "& .pf-v5-c-button.pf-m-plain": { - color: "var(--pf-v5-global--Color--light-100)", - paddingLeft: "0", - paddingRight: "0", + "&.fullPage": { + width: "100%", + height: "100%", + borderRadius: "0", } }, - cardTitle: { - color: "var(--pf-v5-global--Color--light-100)", - }, cardBody: { - backgroundColor: "var(--pf-v5-global--BackgroundColor--100)", + backgroundColor: theme.global.colors.background100, paddingLeft: "var(--pf-v5-global--spacer--md)", paddingRight: "var(--pf-v5-global--spacer--md)", paddingTop: "var(--pf-v5-global--spacer--lg)", overflowY: "scroll", - "&::-webkit-scrollbar": "display: none", + "&::-webkit-scrollbar": { + display: "none" + } }, cardFooter: { - padding: "0", - }, - inputGroup: { - height: "60px", + padding: "10px", + paddingBottom: "16px", + "& :focus-visible": { + outline: "none", + }, + "& .pf-v5-c-button.pf-m-disabled": { + color: "transparent !important", + }, + "& .pf-v5-c-button.pf-m-plain": { + "--pf-v5-c-button--disabled--Color": "transparent", + color: theme.global.colors.primary, + }, + "& .pf-v5-c-form-control": { + "--pf-v5-c-form-control--after--BorderBottomWidth": "0", + }, + "& .pf-v5-svg": { + width: "27px", + height: "27px", + } }, textArea: { resize: "none", - } -}) + backgroundColor: theme.global.colors.background200, + borderRadius: theme.components.VirtualAssistant.textArea.borderRadius, + color: theme.global.colors.light100, + paddingRight: "50px", + paddingLeft: "20px", + }, + sendButton: { + position: "absolute", + bottom: "22px", + right: "14px", + }, +})); -export interface VirtualAssistantProps { +export interface VirtualAssistantProps extends Omit { /** Messages rendered within the assistant */ children?: React.ReactNode; - /** Header title for the assistant */ - title?: React.ReactNode; /** Input's placeholder for the assistant */ inputPlaceholder?: string; /** Input's content */ message?: string; - /** Header actions of the assistant */ - actions?: React.ReactNode; /** Input's content change */ onChangeMessage?: (event: React.ChangeEvent, value: string) => void; /** Fire when clicking the Send (Plane) icon */ @@ -73,18 +118,30 @@ export interface VirtualAssistantProps { isInputDisabled?: boolean; /** Disables the send button */ isSendButtonDisabled?: boolean; + /** Expands the assistant to fill the entire page */ + isFullPage?: boolean; + /** Allows to overwrite the default header with a custom one */ + header?: React.ReactNode; + /** VirtualAssistant OUIA ID */ + ouiaId?: string; + /** VirtualAssistant theme */ + theme?: DefaultTheme; } -export const VirtualAssistant: React.FunctionComponent = ({ +const VirtualAssistantImplementation: React.FunctionComponent = ({ children, - title = 'Virtual Assistant', - inputPlaceholder = 'Type a message...', + inputPlaceholder = 'Send a message...', message = '', actions, onChangeMessage, onSendMessage, isInputDisabled = false, isSendButtonDisabled = false, + title, + icon = RobotIcon, + isFullPage = false, + header = null, + ouiaId = 'VirtualAssistant' }: VirtualAssistantProps) => { const classes = useStyles(); @@ -101,19 +158,21 @@ export const VirtualAssistant: React.FunctionComponent = }; return ( - - - - {title} - - - - {children} - - - + + + { header ?? } + + {children} + + +