Skip to content
This repository has been archived by the owner on Feb 5, 2025. It is now read-only.

Commit

Permalink
feat: update voice widget
Browse files Browse the repository at this point in the history
  • Loading branch information
sofly committed Jan 7, 2025
1 parent 83d71fc commit 02cc078
Show file tree
Hide file tree
Showing 22 changed files with 1,010 additions and 136 deletions.
86 changes: 60 additions & 26 deletions examples/live-chat/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,62 @@
import { ChatConfig } from '@voiceflow/react-chat';
import type { ChatWidgetSettings } from '@voiceflow/react-chat/build/types';

const IMAGE = 'https://picsum.photos/seed/1/200/300';
const AVATAR = 'https://picsum.photos/seed/1/80/80';

export const ASSISTANT: ChatWidgetSettings = {
type: 'chat',
type: 'voice',
chat: {
voiceOutput: true,
voiceInput: true,
voiceOutput: true,
renderMode: 'widget',
headerImage: { enabled: true, url: IMAGE },
agentImage: { enabled: true, url: AVATAR },
banner: { enabled: true, title: 'Your AI Agent', description: 'How can I help you today?' },
placeholderText: 'Type a message...',
aiDisclaimer: { enabled: true, text: 'This is AI!!!' },
headerImage: {
enabled: true,
},
agentImage: {
enabled: true,
},
banner: {
enabled: true,
title: 'Your AI agent',
description: 'How can I help you today?',
},
placeholderText: 'Message...',
aiDisclaimer: {
enabled: true,
text: 'Generated by AI, double-check for accuracy.',
},
handoffToAgentImageURL: '',
},
common: {
fontFamily: 'UCity Pro',
launcher: {
text: 'Your text',
type: 'both',
},
poweredBy: true,
footerLink: {
enabled: true,
},
position: 'right',
sideSpacing: '20px',
bottomSpacing: '20px',
primaryColor: {
color: '#397DFF',
palette: {
50: '#E7F5FD',
100: '#C6E4FB',
200: '#A2D2FA',
300: '#87BFFB',
400: '#659FFD',
500: '#397DFF',
600: '#2F68DB',
700: '#264EB4',
800: '#1C368E',
900: '#0F1E61',
},
},
persistence: 'localStorage',
},
voice: {
renderMode: 'compact',
renderMode: 'full',
content: {
callToActionText: 'How can I help you?',
startButtonText: 'Start a call',
Expand All @@ -26,25 +65,20 @@ export const ASSISTANT: ChatWidgetSettings = {
endButtonText: 'End',
},
},
common: {
fontFamily: 'UCity Pro',
launcher: { type: 'icon' },
poweredBy: true,
footerLink: { enabled: true, text: 'Privacy', url: 'https://voiceflow.com' },
position: 'right',
sideSpacing: '30px',
bottomSpacing: '30px',
primaryColor: {
color: 'blue',
palette: { 50: '', 100: '', 200: '', 300: '', 400: '', 500: '', 600: '', 700: '', 800: '', 900: '' },
},
persistence: 'LOCAL_STORAGE',
},

stylesheet: '',
extensions: [],
};

export const CONFIG = ChatConfig.parse({
verify: { projectID: import.meta.env.VF_PROJECT_ID },
url: import.meta.env.VF_RUNTIME_URL ?? undefined,
versionID: import.meta.env.VF_VERSION_ID,
verify: {
projectID: import.meta.env.VF_PROJECT_ID,
versionID: import.meta.env.VF_VERSION_ID,
},
voice: {
url: import.meta.env.VF_RUNTIME_API_URL,
accessToken: import.meta.env.VF_ACCESS_TOKEN,
},
});
4 changes: 4 additions & 0 deletions examples/live-chat/types/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

interface ImportMetaEnv {
readonly VF_PROJECT_ID: string;
readonly VF_RUNTIME_URL: string;
readonly VF_VERSION_ID: string;
readonly VF_ACCESS_TOKEN: string;
readonly VF_RUNTIME_API_URL: string;
}

interface ImportMeta {
Expand Down
31 changes: 27 additions & 4 deletions packages/chat/src/components/Launcher/LauncherWithLabel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Button } from '@/components/Button';
import { ClassName } from '@/constants';

import { ChevronIcon } from '../ChevronIcon';
import { DEFAULT_ICON } from '../constant';
import { PhoneIcon } from '../PhoneIcon';
import { closeChevron, imageIconStyle, imageIconWrapper, launcherLabelStyles, launcherStyles } from './styles.css';

export interface LauncherProps {
Expand All @@ -31,18 +33,39 @@ export interface LauncherProps {
* A callback that will be executed when the button is clicked.
*/
onClick: MouseEventHandler<HTMLButtonElement>;

/**
* Flag to use the default phone icon.
*/
isVoice?: boolean;

/**
* Flag to use image.
*/
withIcon?: boolean;
}

/**
* A floating action button used to launch the chat widget.
*
* @see {@link https://voiceflow.github.io/react-chat/?path=/story/components-launcher--default}
*/
export const LauncherWithLabel: React.FC<LauncherProps> = ({ image, isOpen, label, onClick }) => {
export const LauncherWithLabel: React.FC<LauncherProps> = ({ isVoice, withIcon, image, isOpen, label, onClick }) => {
const showDefaultPhoneIcon = !image && isVoice;

return (
<Button className={clsx(launcherStyles({ isOpen, noImage: !image }), ClassName.LAUNCHER)} onClick={onClick}>
<div className={imageIconWrapper({ isOpen, noImage: !image })}>
{image && <img src={image} className={clsx(imageIconStyle({ isOpen }))} alt="open chat" />}
<Button className={clsx(launcherStyles({ isOpen, noImage: !withIcon }), ClassName.LAUNCHER)} onClick={onClick}>
<div className={imageIconWrapper({ isOpen, noImage: !withIcon })}>
{withIcon && (
<>
{showDefaultPhoneIcon && <PhoneIcon className={clsx(imageIconStyle({ isOpen }))} fill="white" />}

{!showDefaultPhoneIcon && (
<img src={image ?? DEFAULT_ICON} className={clsx(imageIconStyle({ isOpen }))} alt="open chat" />
)}
</>
)}

<ChevronIcon className={clsx(closeChevron({ isOpen }))} />
</div>
<div className={launcherLabelStyles}>{label}</div>
Expand Down
12 changes: 12 additions & 0 deletions packages/chat/src/components/Launcher/PhoneIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { SVGProps } from 'react';

export const PhoneIcon = (props: SVGProps<SVGSVGElement>) => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.0395 4.75C12.0395 4.33579 12.3753 4 12.7895 4C14.6333 4 16.4802 4.70453 17.8878 6.11216C19.2955 7.51979 20 9.36674 20 11.2105C20 11.6247 19.6642 11.9605 19.25 11.9605C18.8358 11.9605 18.5 11.6247 18.5 11.2105C18.5 9.74848 17.9423 8.28797 16.8272 7.17282C15.712 6.05767 14.2515 5.5 12.7895 5.5C12.3753 5.5 12.0395 5.16421 12.0395 4.75ZM5.74394 6.20973C6.11074 5.84293 6.60824 5.63686 7.12698 5.63686C7.64573 5.63686 8.14323 5.84293 8.51003 6.20974L10.0515 7.75118C10.9723 8.67199 10.9723 10.1649 10.0515 11.0857L9.19857 11.9386C9.78708 13.199 10.801 14.2129 12.0614 14.8014L12.9143 13.9485C13.3565 13.5063 13.9562 13.2579 14.5815 13.2579C15.2069 13.2579 15.8066 13.5063 16.2488 13.9485L17.7903 15.49C18.5541 16.2538 18.5541 17.4922 17.7903 18.2561L17.1 18.9463C16.4261 19.6202 15.5123 19.9992 14.5593 20C11.758 20.0024 9.07084 18.8907 7.09006 16.9099C5.10929 14.9291 3.99757 12.2419 4 9.44068C4.00083 8.48763 4.3798 7.57386 5.05371 6.89996L5.74394 6.20973C5.74394 6.20973 5.74393 6.20974 5.74394 6.20973ZM7.12698 7.13686C7.00606 7.13686 6.8901 7.18489 6.8046 7.27039L6.11436 7.96062C5.72144 8.35354 5.50049 8.88631 5.5 9.44198C5.49792 11.8449 6.45157 14.1501 8.15072 15.8493C9.84988 17.5484 12.155 18.5021 14.558 18.5C15.1137 18.4995 15.6465 18.2786 16.0394 17.8856L16.7296 17.1954C16.9077 17.0174 16.9077 16.7287 16.7296 16.5506L15.1882 15.0092C15.0273 14.8483 14.8091 14.7579 14.5815 14.7579C14.354 14.7579 14.1358 14.8483 13.9749 15.0092L12.7665 16.2176C12.5589 16.4252 12.2492 16.4926 11.9741 16.39C9.95531 15.6371 8.3629 14.0447 7.61001 12.0259C7.50742 11.7508 7.57479 11.4411 7.7824 11.2335L8.99079 10.0251C9.32581 9.69005 9.32581 9.14686 8.99079 8.81183L7.44937 7.27039C7.36387 7.18489 7.2479 7.13686 7.12698 7.13686ZM14.5536 9.44637C14.0714 8.96411 13.4367 8.72876 12.7997 8.73741C12.3855 8.74304 12.0452 8.41184 12.0395 7.99766C12.0339 7.58349 12.3651 7.24317 12.7793 7.23755C13.8 7.22369 14.8311 7.60255 15.6143 8.38571C16.3974 9.16887 16.7763 10.2 16.7624 11.2207C16.7568 11.6349 16.4165 11.9661 16.0023 11.9605C15.5881 11.9548 15.2569 11.6145 15.2626 11.2003C15.2712 10.5633 15.0359 9.92862 14.5536 9.44637Z"
fill="currentColor"
/>
</svg>
);
1 change: 1 addition & 0 deletions packages/chat/src/components/Launcher/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_ICON = 'https://cdn.voiceflow.com/widget-next/message.png';
37 changes: 28 additions & 9 deletions packages/chat/src/components/Launcher/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { ClassName } from '@/constants';

import { Button } from '../Button';
import { ChevronIcon } from './ChevronIcon';
import { DEFAULT_ICON } from './constant';
import { LauncherWithLabel } from './LauncherWithLabel';
import { PhoneIcon } from './PhoneIcon';
import {
closeChevron,
closeIconStyles,
Expand All @@ -18,7 +20,7 @@ import {
launcherStyles,
} from './styles.css';

export const DEFAULT_ICON = 'https://cdn.voiceflow.com/widget-next/message.png';
export { DEFAULT_ICON };

export interface LauncherProps {
/**
Expand Down Expand Up @@ -48,39 +50,56 @@ export interface LauncherProps {
* A callback that will be executed when the button is clicked.
*/
onClick: MouseEventHandler<HTMLDivElement | HTMLButtonElement>;

/**
* Flag to use the default phone icon.
*/
isVoice?: boolean;
}

/**
* A floating action button used to launch the chat widget.
*
* @see {@link https://voiceflow.github.io/react-chat/?path=/story/components-launcher--default}
*/
export const Launcher: React.FC<LauncherProps> = ({ image, type, isOpen, label, onClick }) => {
export const Launcher: React.FC<LauncherProps> = ({ image, type, isVoice, isOpen, label, onClick }) => {
const withIcon = type !== 'label';
const withLabel = type !== 'icon' && !!label?.length;

if (withLabel) {
return (
<LauncherWithLabel
image={withIcon ? (image ?? DEFAULT_ICON) : undefined}
isOpen={isOpen}
image={image}
label={label}
isOpen={isOpen}
onClick={onClick}
isVoice={isVoice}
withIcon={withIcon}
/>
);
}

const showDefaultPhoneIcon = !image && isVoice;

return (
<div className={launcherContainer} onClick={onClick}>
<Button className={clsx(ClassName.LAUNCHER, launcherStyles({ isOpen }))}>
<div className={iconContainer({ isOpen, withIcon })}>
<ChevronIcon className={clsx(closeChevron({ isOpen }), closeIconStyles())} />
{withIcon && (
<img
src={image ?? DEFAULT_ICON}
className={clsx(imageStyles({ isOpen }), launcherIconStyles({}))}
alt="open chat"
/>
<>
{showDefaultPhoneIcon && (
<PhoneIcon className={clsx(imageStyles({ isOpen }), launcherIconStyles({}))} fill="white" />
)}

{!showDefaultPhoneIcon && (
<img
src={image ?? DEFAULT_ICON}
className={clsx(imageStyles({ isOpen }), launcherIconStyles({}))}
alt="open chat"
/>
)}
</>
)}
</div>
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';

import { COLORS } from '@/styles/colors';
import { THEME } from '@/styles/colors.css';
import { hideTextOverflow } from '@/styles/font';
import { SIZES } from '@/styles/sizes';
import { transition } from '@/styles/transitions';

export const footerLinksContainer = style({
color: COLORS.NEUTRAL_DARK[100],
fontFamily: THEME.fontFamily,
fontSize: '12px',
lineHeight: '17px',
width: '100%',
minHeight: 20,
padding: '10px 0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
export const smallStyle = style({});

export const footerLinksContainer = recipe({
base: {
color: COLORS.NEUTRAL_DARK[100],
fontFamily: THEME.fontFamily,
width: '100%',
padding: '10px 0',
fontSize: '12px',
lineHeight: '17px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
},
variants: {
small: {
true: {
fontSize: '11px',
lineHeight: '15px',
},
false: {},
},
},
});

export const separator = style({
Expand All @@ -30,8 +43,8 @@ export const separator = style({
export const extraLinkStyles = style({
color: COLORS.NEUTRAL_DARK[100],
fontFamily: THEME.fontFamily,
fontSize: '12px',
lineHeight: '17px',
fontSize: 'inherit',
lineHeight: 'inherit',
textDecorationColor: 'transparent',
transition: transition(['color', 'text-decoration-color']),
selectors: {
Expand Down Expand Up @@ -67,8 +80,8 @@ export const voiceflowLink = style({
export const footerNote = style({
color: COLORS.NEUTRAL_DARK[100],
fontFamily: THEME.fontFamily,
fontSize: '12px',
lineHeight: '17px',
fontSize: 'inherit',
lineHeight: 'inherit',
textDecorationColor: 'transparent',
transition: transition(['color', 'text-decoration-color']),
...hideTextOverflow(),
Expand Down
18 changes: 14 additions & 4 deletions packages/chat/src/components/NewFooter/BottomLinks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,36 @@ import { extraLinkStyles, footerLinksContainer, footerNote, separator, voiceflow
const VOICEFLOW_URL = 'https://www.voiceflow.com/';

interface IBottomLinks {
isSmall?: boolean;
className?: string;
extraLinkUrl?: string;
showPoweredBy?: boolean;
extraLinkText?: string;
extraLinkUrl?: string;
className?: string;
}

export const BottomLinks: React.FC<IBottomLinks> = ({ showPoweredBy, extraLinkText, extraLinkUrl, className }) => {
export const BottomLinks: React.FC<IBottomLinks> = ({
isSmall = false,
className,
extraLinkUrl,
extraLinkText,
showPoweredBy,
}) => {
const showExtraLink = extraLinkText && extraLinkUrl;

return (
<div className={clsx(footerLinksContainer, className)}>
<div className={clsx(footerLinksContainer({ small: isSmall }), className)}>
{showPoweredBy && (
<div>
<a href={VOICEFLOW_URL} target="_blank" rel="noreferrer" className={voiceflowLink}>
Powered by Voiceflow
</a>
</div>
)}

{showPoweredBy && showExtraLink && <div className={separator} />}

{extraLinkText && !extraLinkUrl && <div className={footerNote}>{extraLinkText}</div>}

{showExtraLink && (
<a href={extraLinkUrl} target="_blank" className={extraLinkStyles}>
{extraLinkText}
Expand Down
Loading

0 comments on commit 02cc078

Please sign in to comment.