Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ The simplest way to add the widget to your website:
| `consentTitle` | `string` | `"Terms and conditions"` | Consent form title |
| `consentContent` | `string` | _(default message)_ | Terms & conditions content |
| `consentStorageKey` | `string` | `"vapi_widget_consent"` | Key for storing consent |
| **Localization** | | | |
| `labels` | `Partial<WidgetLabels>` | _(English)_ | Built-in UI strings to override — see [Localization](#localization) |

### Event Callbacks

Expand Down Expand Up @@ -301,6 +303,77 @@ Use this approach if your environment doesn't support custom elements or for bet
/>
```

## Localization

Everything the widget says can be set from outside. Content-like strings have
their own props (`title`, `ctaTitle`, `chatPlaceholder`, `chatEmptyMessage`,
`chatFirstMessage`, `chatEndMessage`, `consentTitle`, `consentContent`,
`startButtonText`, `endButtonText`, ...). The remaining UI strings — the
header's status line, button labels and tooltips — are grouped in the
`labels` prop. Pass any subset; keys you leave out keep their English default.

| Key | Default | Where it shows |
| ------------------- | --------------------------------- | -------------------------------------------------------------------- |
| `connecting` | `'Connecting...'` | Status line while a call connects; voice button; hybrid call tooltip |
| `assistantSpeaking` | `'Assistant Speaking...'` | Status line during a call |
| `listening` | `'Listening...'` | Status line during a call |
| `assistantTyping` | `'Assistant is typing...'` | Status line while a reply streams |
| `chatActive` | `'Chat active'` | Status line, chat mode, once the conversation has started |
| `readyToAssist` | `'Ready to assist'` | Status line, hybrid mode, once the conversation has started |
| `connected` | `'Connected'` | Status line, voice mode, once the conversation has started |
| `voiceIdle` | `'Click the microphone to start'` | Status line, voice mode, before the first call |
| `chatIdle` | `'Type a message below'` | Status line, chat mode, before the first message |
| `hybridIdle` | `'Choose voice or text'` | Status line, hybrid mode, before the first interaction |
| `endChat` | `'End Chat'` | Header button (chat mode) |
| `resetConversation` | `'Reset conversation'` | Header reset button tooltip |
| `close` | `'Close'` | Header close button tooltip; close button after a chat has ended |
| `startNewChat` | `'Start new chat'` | Button after a chat has ended |
| `consentAccept` | `'Accept'` | Consent form |
| `consentCancel` | `'Cancel'` | Consent form |
| `sendMessage` | `'Send message'` | Send button tooltip |
| `muteMicrophone` | `'Mute microphone'` | Mute button tooltip during a call |
| `unmuteMicrophone` | `'Unmute microphone'` | Mute button tooltip during a call |
| `startVoiceCall` | `'Start voice call'` | Hybrid call button tooltip |
| `stopVoiceCall` | `'Stop voice call'` | Hybrid call button tooltip |

React:

```tsx
<VapiWidget
publicKey="your-public-key"
assistantId="your-assistant-id"
mode="chat"
title="Chat mit uns"
chatPlaceholder="Ihre Frage …"
consentRequired
consentTitle="Hinweis zum Chat"
consentContent="Ihre Nachrichten werden an unseren Dienstleister übermittelt."
labels={{
chatIdle: 'Nachricht eingeben',
chatActive: 'Chat aktiv',
assistantTyping: 'Der Assistent schreibt …',
endChat: 'Beenden',
consentAccept: 'Akzeptieren',
consentCancel: 'Abbrechen',
}}
/>
```

Custom element — `labels` is a JSON attribute, like `assistant-overrides`:

```html
<vapi-widget
public-key="your-public-key"
assistant-id="your-assistant-id"
mode="chat"
title="Chat mit uns"
labels='{"chatIdle":"Nachricht eingeben","endChat":"Beenden","consentAccept":"Akzeptieren","consentCancel":"Abbrechen"}'
></vapi-widget>
```

The data-attribute embed takes the same object as `data-labels`. The
`WidgetLabels` type is exported, so a translation table can be typed.

## Development

### Setup
Expand Down
19 changes: 16 additions & 3 deletions src/components/VapiWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { useVapiWidget } from '../hooks';

import { VapiWidgetProps, ColorScheme, StyleConfig } from './types';

import { sizeStyles, radiusStyles, positionStyles } from './constants';
import {
sizeStyles,
radiusStyles,
positionStyles,
defaultLabels,
} from './constants';

import ConsentForm from './widget/ConsentForm';
import FloatingButton from './widget/FloatingButton';
Expand Down Expand Up @@ -72,6 +77,8 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
termsContent = 'By clicking "Agree," and each time I interact with this AI agent, I consent to the recording, storage, and sharing of my communications with third-party service providers, and as otherwise described in our Terms of Service.', // deprecated
consentStorageKey,
localStorageKey = 'vapi_widget_consent', // deprecated
// Localization
labels,
// Event handlers
onVoiceStart,
onCallStart, // deprecated
Expand Down Expand Up @@ -141,6 +148,7 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
const effectiveChatPlaceholder = chatPlaceholder ?? 'Type your message...';
const effectiveChatEndMessage =
chatEndMessage ?? 'This chat has ended. Thank you.';
const effectiveLabels = { ...defaultLabels, ...labels };

const vapi = useVapiWidget({
mode,
Expand Down Expand Up @@ -400,13 +408,13 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
color: colors.ctaButtonTextColor,
}}
>
Start new chat
{effectiveLabels.startNewChat}
</button>
<button
onClick={handleCloseWidget}
className={`px-3 py-1.5 rounded-md ${styles.theme === 'dark' ? 'bg-gray-800 text-gray-100' : 'bg-gray-100 text-gray-800'}`}
>
Close
{effectiveLabels.close}
</button>
</div>
</div>
Expand Down Expand Up @@ -491,6 +499,7 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
startButtonText={effectiveStartButtonText}
endButtonText={effectiveEndButtonText}
colors={colors}
labels={effectiveLabels}
/>
);
}
Expand All @@ -506,6 +515,7 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
styles={styles}
inputRef={inputRef}
placeholder={effectiveChatPlaceholder}
labels={effectiveLabels}
/>
);
}
Expand All @@ -527,6 +537,7 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
styles={styles}
inputRef={inputRef}
placeholder={effectiveChatPlaceholder}
labels={effectiveLabels}
/>
);
}
Expand All @@ -545,6 +556,7 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
colors={colors}
styles={styles}
radius={radius}
labels={effectiveLabels}
/>
);
}
Expand All @@ -565,6 +577,7 @@ const VapiWidget: React.FC<VapiWidgetProps> = ({
showEndChatButton={!showEndScreen}
colors={colors}
styles={styles}
labels={effectiveLabels}
/>

{/* Conversation Area */}
Expand Down
28 changes: 28 additions & 0 deletions src/components/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
import type { WidgetLabels } from './types';

// English defaults for every UI string the widget renders on its own. The
// `labels` prop overrides any subset of them (see README, "Localization").
export const defaultLabels: WidgetLabels = {
connecting: 'Connecting...',
assistantSpeaking: 'Assistant Speaking...',
listening: 'Listening...',
assistantTyping: 'Assistant is typing...',
chatActive: 'Chat active',
readyToAssist: 'Ready to assist',
connected: 'Connected',
voiceIdle: 'Click the microphone to start',
chatIdle: 'Type a message below',
hybridIdle: 'Choose voice or text',
endChat: 'End Chat',
resetConversation: 'Reset conversation',
close: 'Close',
startNewChat: 'Start new chat',
consentAccept: 'Accept',
consentCancel: 'Cancel',
sendMessage: 'Send message',
muteMicrophone: 'Mute microphone',
unmuteMicrophone: 'Unmute microphone',
startVoiceCall: 'Start voice call',
stopVoiceCall: 'Stop voice call',
};

export const sizeClasses = {
tiny: {
button: 'w-12 h-12',
Expand Down
2 changes: 1 addition & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { default as AnimatedStatusIcon } from './AnimatedStatusIcon';
export { default as VapiWidget } from './VapiWidget';

export type { VapiWidgetProps } from './types';
export type { VapiWidgetProps, WidgetLabels } from './types';
export type { AnimatedStatusIconProps } from './AnimatedStatusIcon';
58 changes: 58 additions & 0 deletions src/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,53 @@
/**
* Every piece of UI text the widget renders on its own — status line,
* button labels and tooltips — as opposed to content that already has a
* dedicated prop (`title`, `chatPlaceholder`, `chatEmptyMessage`,
* `consentTitle`, `consentContent`, `chatEndMessage`, ...). Pass any subset
* through the `labels` prop to localize the widget; keys you leave out keep
* their English default.
*/
export interface WidgetLabels {
// Header status line
/** While a voice call is being established. Also the voice button text and the hybrid call button tooltip. */
connecting: string;
assistantSpeaking: string;
listening: string;
assistantTyping: string;
/** Chat mode, once the conversation has started */
chatActive: string;
/** Hybrid mode, once the conversation has started */
readyToAssist: string;
/** Voice mode, once the conversation has started */
connected: string;
/** Voice mode, before the first call */
voiceIdle: string;
/** Chat mode, before the first message */
chatIdle: string;
/** Hybrid mode, before the first interaction */
hybridIdle: string;

// Header controls
endChat: string;
/** Tooltip of the reset button */
resetConversation: string;
/** Tooltip of the close button, and the close button after a chat has ended */
close: string;

// After a chat has ended
startNewChat: string;

// Consent form buttons
consentAccept: string;
consentCancel: string;

// Control tooltips
sendMessage: string;
muteMicrophone: string;
unmuteMicrophone: string;
startVoiceCall: string;
stopVoiceCall: string;
}

export interface VapiWidgetProps {
// API Configuration
apiUrl?: string;
Expand Down Expand Up @@ -58,6 +108,10 @@ export interface VapiWidgetProps {
consentContent?: string;
consentStorageKey?: string;

// Localization
/** Overrides for the widget's built-in UI strings; see `WidgetLabels` */
labels?: Partial<WidgetLabels>;

// Event handlers
onVoiceStart?: () => void;
onVoiceEnd?: () => void;
Expand Down Expand Up @@ -161,6 +215,7 @@ export interface WidgetHeaderProps {
showEndChatButton?: boolean;
colors: ColorScheme;
styles: StyleConfig;
labels: WidgetLabels;
}

export interface ConversationMessageProps {
Expand Down Expand Up @@ -197,6 +252,7 @@ export interface VoiceControlsProps {
startButtonText: string;
endButtonText: string;
colors: ColorScheme;
labels: WidgetLabels;
}

export interface ChatControlsProps {
Expand All @@ -208,6 +264,7 @@ export interface ChatControlsProps {
styles: StyleConfig;
inputRef?: React.RefObject<HTMLInputElement>;
placeholder?: string;
labels: WidgetLabels;
}

export interface HybridControlsProps {
Expand All @@ -225,4 +282,5 @@ export interface HybridControlsProps {
styles: StyleConfig;
inputRef?: React.RefObject<HTMLInputElement>;
placeholder?: string;
labels: WidgetLabels;
}
7 changes: 5 additions & 2 deletions src/components/widget/ConsentForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import type { WidgetLabels } from '../types';

interface ColorScheme {
baseColor: string;
Expand Down Expand Up @@ -28,6 +29,7 @@ export interface ConsentFormProps {
colors: ColorScheme;
styles: StyleConfig;
radius: 'none' | 'small' | 'medium' | 'large';
labels: Pick<WidgetLabels, 'consentAccept' | 'consentCancel'>;
}

const ConsentForm: React.FC<ConsentFormProps> = ({
Expand All @@ -38,6 +40,7 @@ const ConsentForm: React.FC<ConsentFormProps> = ({
colors,
styles,
radius,
labels,
}) => {
// Use the configured base color and derive other colors based on theme
const isDark = styles.theme === 'dark';
Expand Down Expand Up @@ -154,14 +157,14 @@ const ConsentForm: React.FC<ConsentFormProps> = ({
onClick={onCancel}
style={cancelButtonStyle}
>
Cancel
{labels.consentCancel}
</button>
<button
className="consent-accept-button"
onClick={onAccept}
style={acceptButtonStyle}
>
Accept
{labels.consentAccept}
</button>
</div>
</div>
Expand Down
Loading