Skip to content

Commit

Permalink
Fluent: allow displaying feedback inside activity actions toolbar
Browse files Browse the repository at this point in the history
  • Loading branch information
OEvgeny committed Jan 9, 2025
1 parent cf2b542 commit 17b9981
Show file tree
Hide file tree
Showing 24 changed files with 299 additions and 82 deletions.
61 changes: 61 additions & 0 deletions __tests__/html/fluentTheme/side-by-side.wide.dark.html
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,49 @@
$ pip install numpy matplotlib`
}
], [
{
from:{
role: "bot"
},
id: "a-00001",
type: "message",
text: "Hi! I'm Cody, the devbot. How can I help?",
timestamp: timestamp(),
entities: [
{
...aiMessageEntity,
keywords: ['AIGeneratedContent', 'AllowCopy'],
potentialAction: [
{
"@type": "LikeAction",
actionStatus: "PotentialActionStatus",
target: {
"@type": "EntryPoint",
urlTemplate: "ms-directline://postback?interaction=like"
}
},
{
"@type": "DislikeAction",
actionStatus: "PotentialActionStatus",
result: {
"@type": "Review",
reviewBody: "I don't like it.",
"reviewBody-input": {
"@type": "PropertyValueSpecification",
valueMinLength: 3,
valueName: "reason"
}
},
target: {
"@type": "EntryPoint",
urlTemplate: "ms-directline://postback?interaction=dislike{&reason}"
}
}
]
}
]
}
]];

const leftStore = testHelpers.createStore();
Expand Down Expand Up @@ -726,6 +769,24 @@
await host.snapshot();
await host.sendKeys('ENTER');
await host.snapshot();
},
likeDislike: async sendbox => {
sendbox.focus();
await host.sendShiftTab();
await host.sendKeys('ARROW_UP');
await host.sendKeys('ENTER');
await host.snapshot();
await host.sendKeys('TAB');
await host.snapshot();
await host.sendKeys('ENTER');
await host.snapshot();
await host.sendKeys('TAB');
await host.snapshot();
await host.sendKeys('ENTER');
await host.snapshot();
await host.sendKeys('ESCAPE');
await host.sendKeys('ESCAPE');
await host.snapshot();
}
}));

Expand Down
6 changes: 5 additions & 1 deletion __tests__/html/fluentTheme/side-by-side.wide.dark.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ describe('Fluent theme applied', () => {
test('side by side left - transcript, right - codeblock', () =>
runHTML('fluentTheme/side-by-side.wide.dark?transcript=0&transcript=5&focus=1&focus-preset=viewCode'));
test('side by side left - transcript, right - codeblock dark', () =>
runHTML('fluentTheme/side-by-side.wide.dark?transcript=0&transcript=5&focus=1&focus-preset=viewCode&codeBlockTheme=github-dark-default'));
runHTML(
'fluentTheme/side-by-side.wide.dark?transcript=0&transcript=5&focus=1&focus-preset=viewCode&codeBlockTheme=github-dark-default'
));
test('side by side left - transcript, right - feedback', () =>
runHTML('fluentTheme/side-by-side.wide.dark?transcript=0&transcript=6&focus=1&focus-preset=likeDislike'));
});
});
61 changes: 61 additions & 0 deletions __tests__/html/fluentTheme/side-by-side.wide.html
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,49 @@
$ pip install numpy matplotlib`
}
], [
{
from:{
role: "bot"
},
id: "a-00001",
type: "message",
text: "Hi! I'm Cody, the devbot. How can I help?",
timestamp: timestamp(),
entities: [
{
...aiMessageEntity,
keywords: ['AIGeneratedContent', 'AllowCopy'],
potentialAction: [
{
"@type": "LikeAction",
actionStatus: "PotentialActionStatus",
target: {
"@type": "EntryPoint",
urlTemplate: "ms-directline://postback?interaction=like"
}
},
{
"@type": "DislikeAction",
actionStatus: "PotentialActionStatus",
result: {
"@type": "Review",
reviewBody: "I don't like it.",
"reviewBody-input": {
"@type": "PropertyValueSpecification",
valueMinLength: 3,
valueName: "reason"
}
},
target: {
"@type": "EntryPoint",
urlTemplate: "ms-directline://postback?interaction=dislike{&reason}"
}
}
]
}
]
}
]];

const leftStore = testHelpers.createStore();
Expand Down Expand Up @@ -709,6 +752,24 @@
await host.snapshot();
await host.sendKeys('ENTER');
await host.snapshot();
},
likeDislike: async sendbox => {
sendbox.focus();
await host.sendShiftTab();
await host.sendKeys('ARROW_UP');
await host.sendKeys('ENTER');
await host.snapshot();
await host.sendKeys('TAB');
await host.snapshot();
await host.sendKeys('ENTER');
await host.snapshot();
await host.sendKeys('TAB');
await host.snapshot();
await host.sendKeys('ENTER');
await host.snapshot();
await host.sendKeys('ESCAPE');
await host.sendKeys('ESCAPE');
await host.snapshot();
}
}));

Expand Down
6 changes: 5 additions & 1 deletion __tests__/html/fluentTheme/side-by-side.wide.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ describe('Fluent theme applied', () => {
test('side by side left - transcript, right - codeblock', () =>
runHTML('fluentTheme/side-by-side.wide?transcript=0&transcript=5&focus=1&focus-preset=viewCode'));
test('side by side left - transcript, right - codeblock dark', () =>
runHTML('fluentTheme/side-by-side.wide?transcript=0&transcript=5&focus=1&focus-preset=viewCode&codeBlockTheme=github-dark-default'));
runHTML(
'fluentTheme/side-by-side.wide?transcript=0&transcript=5&focus=1&focus-preset=viewCode&codeBlockTheme=github-dark-default'
));
test('side by side left - transcript, right - feedback', () =>
runHTML('fluentTheme/side-by-side.wide?transcript=0&transcript=6&focus=1&focus-preset=likeDislike'));
});
12 changes: 12 additions & 0 deletions packages/api/src/StyleOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,18 @@ type StyleOptions = {
* New in 4.19.0.
*/
codeBlockTheme?: 'github-light-default' | 'github-dark-default';

/**
* Feedback actions placement
*
* - `'activity-actions'` - place feedback buttons inside activity actions
* - `'activity-status'` - place feedback buttons inside activity status
*
* @default 'activity-status'
*
* New in 4.19.0.
*/
feedbackActionsPlacement?: 'activity-actions' | 'activity-status';
};

// StrictStyleOptions is only used internally in Web Chat and for simplifying our code:
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/defaultStyleOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,9 @@ const DEFAULT_OPTIONS: Required<StyleOptions> = {
borderAnimationColor2: '#4DD3FF',
borderAnimationColor3: '#2B8DD8',

codeBlockTheme: 'github-light-default' as const
codeBlockTheme: 'github-light-default' as const,

feedbackActionsPlacement: 'activity-status' as const
};

export default DEFAULT_OPTIONS;
53 changes: 53 additions & 0 deletions packages/component/src/Activity/ActivityFeedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { hooks } from 'botframework-webchat-api';
import { getOrgSchemaMessage, OrgSchemaAction, parseAction, WebChatActivity } from 'botframework-webchat-core';
import cx from 'classnames';
import React, { memo, useMemo } from 'react';

import Feedback from './private/Feedback';
import dereferenceBlankNodes from '../Utils/JSONLinkedData/dereferenceBlankNodes';

const { useStyleOptions } = hooks;

type ActivityFeedbackProps = Readonly<{
activity: WebChatActivity;
placement: 'activity-status' | 'activity-actions';
}>;

function ActivityFeedback({ activity, placement }: ActivityFeedbackProps) {
const [{ feedbackActionsPlacement }] = useStyleOptions();

const graph = useMemo(() => dereferenceBlankNodes(activity.entities || []), [activity.entities]);

const messageThing = useMemo(() => getOrgSchemaMessage(graph), [graph]);

const feedbackActions = useMemo<ReadonlySet<OrgSchemaAction> | undefined>(() => {
try {
const reactActions = (messageThing?.potentialAction || []).filter(
({ '@type': type }) => type === 'LikeAction' || type === 'DislikeAction'
);

if (reactActions.length) {
return Object.freeze(new Set(reactActions));
}

const voteActions = graph.filter(({ type }) => type === 'https://schema.org/VoteAction').map(parseAction);

if (voteActions.length) {
return Object.freeze(new Set(voteActions));
}
} catch {
// Intentionally left blank.
}
}, [graph, messageThing?.potentialAction]);

return feedbackActions?.size && placement === feedbackActionsPlacement ? (
<Feedback
actions={feedbackActions}
className={cx({
'webchat__thumb-button--large': feedbackActionsPlacement === 'activity-actions'
})}
/>
) : null;
}

export default memo(ActivityFeedback);
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import { type OrgSchemaAction } from 'botframework-webchat-core';
import React, { Fragment, memo, useEffect, useState, type PropsWithChildren } from 'react';
import { useRefFrom } from 'use-ref-from';

import FeedbackVoteButton from './private/VoteButton';
import FeedbackVoteButton from './VoteButton';

const { usePonyfill, usePostActivity } = hooks;

type Props = Readonly<
PropsWithChildren<{
actions: ReadonlySet<OrgSchemaAction>;
className?: string | undefined;
}>
>;

const DEBOUNCE_TIMEOUT = 500;

const Feedback = memo(({ actions }: Props) => {
const Feedback = memo(({ actions, className }: Props) => {
const [{ clearTimeout, setTimeout }] = usePonyfill();
const [selectedAction, setSelectedAction] = useState<OrgSchemaAction | undefined>();
const postActivity = usePostActivity();
Expand Down Expand Up @@ -46,6 +47,7 @@ const Feedback = memo(({ actions }: Props) => {
{Array.from(actions).map((action, index) => (
<FeedbackVoteButton
action={action}
className={className}
key={action['@id'] || index}
onClick={setSelectedAction}
pressed={selectedAction === action}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { memo } from 'react';

import ThumbDislike16Filled from './icons/ThumbDislike16Filled';
import ThumbDislike16Regular from './icons/ThumbDislike16Regular';
import ThumbLike16Filled from './icons/ThumbLike16Filled';
import ThumbLike16Regular from './icons/ThumbLike16Regular';
import ThumbDislike16Filled from './ThumbDislike16Filled';
import ThumbDislike16Regular from './ThumbDislike16Regular';
import ThumbLike16Filled from './ThumbLike16Filled';
import ThumbLike16Regular from './ThumbLike16Regular';

type Props = Readonly<{
className?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ import classNames from 'classnames';
import React, { memo } from 'react';

import ThumbButtonImage from './ThumbButton.Image';
import useStyleSet from '../../../../hooks/useStyleSet';
import useStyleSet from '../../hooks/useStyleSet';

const { useLocalizer } = hooks;

type Props = Readonly<{
className?: string | undefined;
direction: 'down' | 'up';
onClick?: () => void;
pressed?: boolean;
}>;

const ThumbButton = memo(({ direction, onClick, pressed }: Props) => {
const ThumbButton = memo(({ className, direction, onClick, pressed }: Props) => {
const [{ thumbButton }] = useStyleSet();
const localize = useLocalizer();

Expand All @@ -26,6 +27,7 @@ const ThumbButton = memo(({ direction, onClick, pressed }: Props) => {
className={classNames(
'webchat__thumb-button',
{ 'webchat__thumb-button--is-pressed': pressed },
className,
thumbButton + ''
)}
onClick={onClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { useRefFrom } from 'use-ref-from';
import ThumbsButton from './ThumbButton';

type Props = Readonly<{
className?: string;
action: OrgSchemaAction;
onClick?: (action: OrgSchemaAction) => void;
pressed: boolean;
}>;

const FeedbackVoteButton = memo(({ action, onClick, pressed }: Props) => {
const FeedbackVoteButton = memo(({ action, className, onClick, pressed }: Props) => {
const onClickRef = useRefFrom(onClick);
const voteActionRef = useRefFrom(action);

Expand All @@ -28,7 +29,7 @@ const FeedbackVoteButton = memo(({ action, onClick, pressed }: Props) => {

const handleClick = useCallback(() => onClickRef.current?.(voteActionRef.current), [onClickRef, voteActionRef]);

return <ThumbsButton direction={direction} onClick={handleClick} pressed={pressed} />;
return <ThumbsButton className={className} direction={direction} onClick={handleClick} pressed={pressed} />;
});

FeedbackVoteButton.displayName = 'FeedbackVoteButton';
Expand Down
Loading

0 comments on commit 17b9981

Please sign in to comment.