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}
+
+
+
-
-
+
+
+
+
);
};
+export const VirtualAssistant: React.FunctionComponent = ({ theme, ...rest }: VirtualAssistantProps) => {
+ const vaTheme = useMemo(() => deepMerge({ ...defaultTheme }, theme ?? {}), [ theme ])
+ return (
+
+
+
+ )};
+
export default VirtualAssistant;
diff --git a/packages/module/src/VirtualAssistant/__snapshots__/VirtualAssistant.test.tsx.snap b/packages/module/src/VirtualAssistant/__snapshots__/VirtualAssistant.test.tsx.snap
index 05b140308..abd017e60 100644
--- a/packages/module/src/VirtualAssistant/__snapshots__/VirtualAssistant.test.tsx.snap
+++ b/packages/module/src/VirtualAssistant/__snapshots__/VirtualAssistant.test.tsx.snap
@@ -3,24 +3,49 @@
exports[`VirtualAssistant should render assistant 1`] = `
diff --git a/packages/module/src/VirtualAssistantAction/VirtualAssistantAction.tsx b/packages/module/src/VirtualAssistantAction/VirtualAssistantAction.tsx
index 746087cf9..25881a7a1 100644
--- a/packages/module/src/VirtualAssistantAction/VirtualAssistantAction.tsx
+++ b/packages/module/src/VirtualAssistantAction/VirtualAssistantAction.tsx
@@ -1,13 +1,13 @@
import * as React from 'react';
import { Button, ButtonProps, ButtonVariant } from '@patternfly/react-core';
-import { createUseStyles } from 'react-jss';
-import classnames from "clsx";
+import clsx from "clsx";
+import { createVaStyles } from '../VirtualAssistantTheme';
-const useStyles = createUseStyles({
+const useStyles = createVaStyles((theme) => ({
button: {
- color: "var(--pf-v5-global--Color--light-100)"
+ color: theme.global.colors.light100
}
-});
+}));
export type VirtualAssistantActionProps = ButtonProps;
@@ -16,9 +16,9 @@ export const VirtualAssistantAction: React.FunctionComponent = ({
variant = ButtonVariant.plain,
className,
...otherProps
-}) => {
+}: VirtualAssistantActionProps) => {
const classes = useStyles();
- return
+ return
}
export default VirtualAssistantAction;
diff --git a/packages/module/src/VirtualAssistantContext/VirtualAssistantContext.tsx b/packages/module/src/VirtualAssistantContext/VirtualAssistantContext.tsx
new file mode 100644
index 000000000..ca4a0874b
--- /dev/null
+++ b/packages/module/src/VirtualAssistantContext/VirtualAssistantContext.tsx
@@ -0,0 +1,25 @@
+import React, { createContext, useContext, ReactNode } from 'react';
+
+interface VirtualAssistantContextValues {
+ /** Default icon for assistant's messages */
+ assistantIcon: React.ComponentType;
+}
+
+const VirtualAssistantContext = createContext(undefined);
+
+export const VirtualAssistantProvider: React.FC<{
+ assistantIcon: React.ComponentType;
+ children: ReactNode
+}> = ({ assistantIcon, children }) => (
+
+ {children}
+
+);
+
+export const useVirtualAssistantContext = (): VirtualAssistantContextValues => {
+ const context = useContext(VirtualAssistantContext);
+ if (context === undefined) {
+ throw new Error('useAssistantContext must be used within an AssistantProvider');
+ }
+ return context;
+};
diff --git a/packages/module/src/VirtualAssistantContext/index.ts b/packages/module/src/VirtualAssistantContext/index.ts
new file mode 100644
index 000000000..c332e07f0
--- /dev/null
+++ b/packages/module/src/VirtualAssistantContext/index.ts
@@ -0,0 +1 @@
+export * from './VirtualAssistantContext';
diff --git a/packages/module/src/VirtualAssistantHeader/VirtualAssistantHeader.tsx b/packages/module/src/VirtualAssistantHeader/VirtualAssistantHeader.tsx
new file mode 100644
index 000000000..30dca6bb8
--- /dev/null
+++ b/packages/module/src/VirtualAssistantHeader/VirtualAssistantHeader.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { CardHeader, CardHeaderProps, Flex, Icon } from '@patternfly/react-core';
+import RobotIcon from '@patternfly/react-icons/dist/js/icons/robot-icon';
+import { createVaStyles } from '../VirtualAssistantTheme';
+
+export interface VirtualAssistantHeaderProps extends Omit {
+ /** Header title for the assistant */
+ title?: React.ReactNode;
+ /** Virtual assistant icon */
+ icon?: React.ComponentType;
+ /** Header actions of the assistant */
+ actions?: React.ReactNode;
+ /** Custom ouiaId */
+ ouiaId?: string;
+};
+
+const useStyles = createVaStyles((theme) => ({
+ cardHeader: {
+ background: theme.components.VirtualAssistantHeader.background,
+ boxShadow: "0px 3px 5px 0px rgba(0,0,0,0.40) !important",
+ height: "74px",
+ marginBottom: "6px",
+ "&:first-child": {
+ paddingBlockStart: "10px",
+ paddingInlineEnd: "10px",
+ },
+ "& .pf-v5-c-button.pf-m-plain": {
+ color: theme.global.colors.light100,
+ paddingLeft: "0",
+ paddingRight: "0",
+ "& .pf-v5-svg": {
+ width: ".8em",
+ height: ".8em",
+ verticalAlign: "1em",
+ }
+ }
+ },
+ cardTitle: {
+ alignSelf: "center",
+ color: theme.global.colors.light100,
+ fontSize: "var(--pf-v5-global--FontSize--lg)",
+ fontWeight: "400",
+ lineHeight: "27px",
+ paddingLeft: "var(--pf-v5-global--spacer--sm)",
+ },
+ titleIcon: {
+ marginLeft: "5px",
+ marginTop: "4px",
+ fontSize: "28px",
+ color: theme.global.colors.primary,
+ },
+ titleIconWrapper: {
+ display: "block",
+ float: "left",
+ width: "38px",
+ height: "38px",
+ background: theme.global.colors.background100,
+ borderRadius: "20px",
+ marginRight: "7px",
+ },
+}));
+
+export const VirtualAssistantHeader: React.FC = ({
+ title = 'Virtual Assistant',
+ icon: VAIcon = RobotIcon,
+ actions,
+ ouiaId = 'VirtualAssistant'
+}: VirtualAssistantHeaderProps) => {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+ );
+};
+
+export default VirtualAssistantHeader;
diff --git a/packages/module/src/VirtualAssistantHeader/index.ts b/packages/module/src/VirtualAssistantHeader/index.ts
new file mode 100644
index 000000000..4c88107ba
--- /dev/null
+++ b/packages/module/src/VirtualAssistantHeader/index.ts
@@ -0,0 +1,2 @@
+export { default } from './VirtualAssistantHeader';
+export * from './VirtualAssistantHeader';
diff --git a/packages/module/src/VirtualAssistantTheme/VirtualAssistantTheme.tsx b/packages/module/src/VirtualAssistantTheme/VirtualAssistantTheme.tsx
new file mode 100644
index 000000000..14988b24a
--- /dev/null
+++ b/packages/module/src/VirtualAssistantTheme/VirtualAssistantTheme.tsx
@@ -0,0 +1,41 @@
+// eslint-disable-next-line no-restricted-imports
+import { createUseStyles, Styles } from "react-jss";
+
+export function createVaStyles(styles: Styles | ((theme: Theme) => Styles)) {
+ return createUseStyles(styles)
+}
+
+export const defaultTheme = {
+ global: {
+ colors: {
+ primary: 'var(--pf-v5-global--danger-color--100)',
+ primaryInactive: 'var(--pf-v5-c-label--m-red__content--Color)',
+ light100: 'var(--pf-v5-global--Color--light-100)',
+ backgroundPrimaryInactive: 'var(--pf-v5-c-label--m-red--BackgroundColor)',
+ background100: 'var(--pf-v5-global--BackgroundColor--100)',
+ background200: 'var(--pf-v5-global--BackgroundColor--200)',
+
+ },
+ borderRadiusBubble: '14px'
+ },
+ components: {
+ VirtualAssistant: {
+ card: {
+ borderRadius: '20px',
+ },
+ textArea: {
+ borderRadius: "50px",
+ }
+ },
+ VirtualAssistantHeader: {
+ background: 'linear-gradient(180deg, #C9190B 0%, #A30000 100%, #3D0000 100.01%)'
+ },
+ AssistantMessageEntry: {
+ label: {
+ borderRadius: 'var(--pf-v5-global--BorderRadius--lg)'
+ }
+ }
+ }
+}
+
+export default defaultTheme;
diff --git a/packages/module/src/VirtualAssistantTheme/index.ts b/packages/module/src/VirtualAssistantTheme/index.ts
new file mode 100644
index 000000000..4cc565647
--- /dev/null
+++ b/packages/module/src/VirtualAssistantTheme/index.ts
@@ -0,0 +1,2 @@
+export { default } from './VirtualAssistantTheme';
+export * from './VirtualAssistantTheme';
diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts
index a87ea99fa..2fcff9397 100644
--- a/packages/module/src/index.ts
+++ b/packages/module/src/index.ts
@@ -1,8 +1,15 @@
// this file is autogenerated by generate-index.js, modifying it manually will have no effect
+export * from './VirtualAssistantContext';
export { default as AssistantMessageEntry } from './AssistantMessageEntry';
export * from './AssistantMessageEntry';
+export { default as Citation } from './Citation';
+export * from './Citation';
+
+export { default as Citations } from './Citations';
+export * from './Citations';
+
export { default as ConversationAlert } from './ConversationAlert';
export * from './ConversationAlert';
@@ -20,3 +27,6 @@ export * from './VirtualAssistant';
export { default as VirtualAssistantAction } from './VirtualAssistantAction';
export * from './VirtualAssistantAction';
+
+export { default as VirtualAssistantHeader } from './VirtualAssistantHeader';
+export * from './VirtualAssistantHeader';