Skip to content

Commit 6c4cfe6

Browse files
committed
Restructured the project
1 parent b449da0 commit 6c4cfe6

File tree

16 files changed

+777
-729
lines changed

16 files changed

+777
-729
lines changed

src/components/VapiWidget.tsx

Lines changed: 16 additions & 724 deletions
Large diffs are not rendered by default.

src/components/constants.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export const sizeClasses = {
2+
tiny: {
3+
button: 'w-12 h-12',
4+
expanded: 'w-72 h-80',
5+
icon: 'w-5 h-5'
6+
},
7+
compact: {
8+
button: 'px-4 py-3 h-12',
9+
expanded: 'w-96 h-[32rem]',
10+
icon: 'w-5 h-5'
11+
},
12+
full: {
13+
button: 'px-6 py-4 h-14',
14+
expanded: 'w-[28rem] h-[40rem]',
15+
icon: 'w-6 h-6'
16+
}
17+
}
18+
19+
export const radiusClasses = {
20+
none: 'rounded-none',
21+
small: 'rounded-lg',
22+
medium: 'rounded-2xl',
23+
large: 'rounded-3xl'
24+
}
25+
26+
export const buttonRadiusClasses = {
27+
none: 'rounded-none',
28+
small: 'rounded-lg',
29+
medium: 'rounded-2xl',
30+
large: 'rounded-3xl'
31+
}
32+
33+
export const messageRadiusClasses = {
34+
none: 'rounded-none',
35+
small: 'rounded-md', // 6px - subtle rounding
36+
medium: 'rounded-lg', // 8px - moderate rounding
37+
large: 'rounded-xl' // 12px - more rounded but not excessive
38+
}
39+
40+
export const positionClasses = {
41+
'bottom-right': 'bottom-6 right-6',
42+
'bottom-left': 'bottom-6 left-6',
43+
'top-right': 'top-6 right-6',
44+
'top-left': 'top-6 left-6'
45+
}

src/components/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
export { default as AnimatedStatusIcon } from './AnimatedStatusIcon'
22
export { default as VapiWidget } from './VapiWidget'
3-
export { default as ConsentForm } from './ConsentForm'
4-
export type { VapiWidgetProps } from './VapiWidget'
5-
export type { ConsentFormProps } from './ConsentForm'
3+
export type { VapiWidgetProps } from './types'
64
export type { AnimatedStatusIconProps } from './AnimatedStatusIcon'

src/components/types.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
export interface VapiWidgetProps {
2+
publicKey: string
3+
4+
// Vapi Configuration - Generic support for all vapi.start() patterns
5+
vapiConfig: any // This gets passed directly to vapi.start()
6+
7+
// API Configuration
8+
apiUrl?: string // Optional custom API URL for chat
9+
10+
// Layout & Position
11+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
12+
size?: 'tiny' | 'compact' | 'full'
13+
radius?: 'none' | 'small' | 'medium' | 'large'
14+
15+
// Mode & Theme
16+
mode?: 'voice' | 'chat' | 'hybrid'
17+
theme?: 'light' | 'dark'
18+
19+
// Colors
20+
baseColor?: string
21+
accentColor?: string
22+
buttonBaseColor?: string
23+
buttonAccentColor?: string
24+
25+
// Text & Labels
26+
mainLabel?: string
27+
startButtonText?: string
28+
endButtonText?: string
29+
30+
// Empty State Messages
31+
emptyVoiceMessage?: string
32+
emptyVoiceActiveMessage?: string
33+
emptyChatMessage?: string
34+
emptyHybridMessage?: string
35+
36+
// Legal & Consent
37+
requireConsent?: boolean
38+
termsContent?: string
39+
localStorageKey?: string
40+
41+
// Transcript
42+
showTranscript?: boolean
43+
44+
// Legacy props (for backwards compatibility)
45+
primaryColor?: string
46+
47+
// Event handlers
48+
onCallStart?: () => void
49+
onCallEnd?: () => void
50+
onMessage?: (message: any) => void
51+
onError?: (error: Error) => void
52+
}
53+
54+
export interface ColorScheme {
55+
baseColor: string
56+
accentColor: string
57+
buttonBaseColor: string
58+
buttonAccentColor: string
59+
}
60+
61+
export interface StyleConfig {
62+
size: 'tiny' | 'compact' | 'full'
63+
radius: 'none' | 'small' | 'medium' | 'large'
64+
theme: 'light' | 'dark'
65+
}
66+
67+
export interface VolumeIndicatorProps {
68+
volumeLevel: number
69+
isCallActive: boolean
70+
isSpeaking: boolean
71+
theme: 'light' | 'dark'
72+
}
73+
74+
export interface FloatingButtonProps {
75+
isCallActive: boolean
76+
connectionStatus: 'disconnected' | 'connecting' | 'connected'
77+
isSpeaking: boolean
78+
isTyping: boolean
79+
onClick: () => void
80+
onToggleCall?: () => void
81+
mainLabel: string
82+
colors: ColorScheme
83+
styles: StyleConfig
84+
mode: 'voice' | 'chat' | 'hybrid'
85+
}
86+
87+
export interface WidgetHeaderProps {
88+
mode: 'voice' | 'chat' | 'hybrid'
89+
connectionStatus: 'disconnected' | 'connecting' | 'connected'
90+
isCallActive: boolean
91+
isSpeaking: boolean
92+
isTyping: boolean
93+
hasActiveConversation: boolean
94+
mainLabel: string
95+
onClose: () => void
96+
onReset: () => void
97+
colors: ColorScheme
98+
styles: StyleConfig
99+
}
100+
101+
export interface ConversationMessageProps {
102+
role: 'user' | 'assistant'
103+
content: string
104+
colors: ColorScheme
105+
styles: StyleConfig
106+
isLoading?: boolean
107+
}
108+
109+
export interface MarkdownMessageProps {
110+
content: string
111+
isLoading?: boolean
112+
role: 'user' | 'assistant'
113+
}
114+
115+
export interface EmptyConversationProps {
116+
mode: 'voice' | 'chat' | 'hybrid'
117+
isCallActive: boolean
118+
theme: 'light' | 'dark'
119+
emptyVoiceMessage: string
120+
emptyVoiceActiveMessage: string
121+
emptyChatMessage: string
122+
emptyHybridMessage: string
123+
}
124+
125+
export interface VoiceControlsProps {
126+
isCallActive: boolean
127+
connectionStatus: 'disconnected' | 'connecting' | 'connected'
128+
isAvailable: boolean
129+
onToggleCall: () => void
130+
startButtonText: string
131+
endButtonText: string
132+
colors: ColorScheme
133+
}
134+
135+
export interface ChatControlsProps {
136+
chatInput: string
137+
isAvailable: boolean
138+
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
139+
onSendMessage: () => void
140+
colors: ColorScheme
141+
styles: StyleConfig
142+
inputRef?: React.RefObject<HTMLInputElement>
143+
}
144+
145+
export interface HybridControlsProps {
146+
chatInput: string
147+
isCallActive: boolean
148+
connectionStatus: 'disconnected' | 'connecting' | 'connected'
149+
isChatAvailable: boolean
150+
isVoiceAvailable: boolean
151+
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
152+
onSendMessage: () => void
153+
onToggleCall: () => void
154+
colors: ColorScheme
155+
styles: StyleConfig
156+
inputRef?: React.RefObject<HTMLInputElement>
157+
}
File renamed without changes.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react'
2+
import AnimatedStatusIcon from '../AnimatedStatusIcon'
3+
import { FloatingButtonProps } from '../types'
4+
import { sizeClasses, buttonRadiusClasses } from '../constants'
5+
6+
const FloatingButton: React.FC<FloatingButtonProps> = ({
7+
isCallActive,
8+
connectionStatus,
9+
isSpeaking,
10+
isTyping,
11+
onClick,
12+
onToggleCall,
13+
mainLabel,
14+
colors,
15+
styles,
16+
mode
17+
}) => {
18+
// Special handling for tiny voice mode
19+
const isTinyVoice = mode === 'voice' && styles.size === 'tiny'
20+
const handleClick = () => {
21+
if (isTinyVoice && onToggleCall) {
22+
onToggleCall()
23+
} else {
24+
onClick()
25+
}
26+
}
27+
28+
// Larger size and glow effect for tiny voice mode when active
29+
const buttonClass = isTinyVoice && isCallActive
30+
? 'w-20 h-20' // Larger size for active tiny voice
31+
: sizeClasses[styles.size].button
32+
33+
return (
34+
<div
35+
className={`${buttonClass} ${buttonRadiusClasses[styles.radius]} shadow-lg cursor-pointer transition-all duration-300 hover:scale-105 hover:-translate-y-1 hover:shadow-xl flex items-center justify-center relative ${
36+
isTinyVoice && isCallActive ? 'animate-glow' : ''
37+
}`}
38+
style={{
39+
backgroundColor: isCallActive && isTinyVoice ? '#ef4444' : colors.buttonBaseColor
40+
}}
41+
onClick={handleClick}
42+
>
43+
<div className="flex items-center space-x-2">
44+
<AnimatedStatusIcon
45+
size={isTinyVoice && isCallActive ? 48 : styles.size === 'tiny' ? 24 : 28}
46+
connectionStatus={connectionStatus}
47+
isCallActive={isCallActive}
48+
isSpeaking={isSpeaking}
49+
isTyping={isTyping}
50+
baseColor={colors.accentColor}
51+
colors={colors.accentColor}
52+
/>
53+
54+
{(styles.size === 'compact' || styles.size === 'full') && !isTinyVoice && (
55+
<span className={`${styles.size === 'full' ? 'text-sm' : 'text-sm'} font-medium`} style={{ color: colors.buttonAccentColor }}>
56+
{mainLabel}
57+
</span>
58+
)}
59+
</div>
60+
</div>
61+
)
62+
}
63+
64+
export default FloatingButton
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React from 'react'
2+
import { XIcon, ArrowsClockwiseIcon } from '@phosphor-icons/react'
3+
import AnimatedStatusIcon from '../AnimatedStatusIcon'
4+
import { WidgetHeaderProps } from '../types'
5+
6+
const WidgetHeader: React.FC<WidgetHeaderProps> = ({
7+
mode,
8+
connectionStatus,
9+
isCallActive,
10+
isSpeaking,
11+
isTyping,
12+
hasActiveConversation,
13+
mainLabel,
14+
onClose,
15+
onReset,
16+
colors,
17+
styles
18+
}) => {
19+
// Determine the status message based on current state
20+
const getStatusMessage = () => {
21+
if (connectionStatus === 'connecting') return 'Connecting...'
22+
23+
if (isCallActive) {
24+
return isSpeaking ? 'Assistant Speaking...' : 'Listening...'
25+
}
26+
27+
if (isTyping) return 'Assistant is typing...'
28+
29+
// If there's an active conversation, show appropriate status
30+
if (hasActiveConversation) {
31+
if (mode === 'chat') return 'Chat active'
32+
if (mode === 'hybrid') return 'Ready to assist'
33+
return 'Connected'
34+
}
35+
36+
// No conversation yet - show how to start
37+
if (mode === 'voice') return 'Click the microphone to start'
38+
if (mode === 'chat') return 'Type a message below'
39+
return 'Choose voice or text'
40+
}
41+
42+
return (
43+
<div
44+
className={`relative z-10 p-4 flex items-center justify-between border-b ${
45+
styles.theme === 'dark'
46+
? 'text-white border-gray-800 shadow-lg'
47+
: 'text-gray-900 border-gray-200 shadow-sm'
48+
}`}
49+
style={{ backgroundColor: colors.baseColor }}
50+
>
51+
<div className="flex items-center space-x-3">
52+
<AnimatedStatusIcon
53+
size={40}
54+
connectionStatus={connectionStatus}
55+
isCallActive={isCallActive}
56+
isSpeaking={isSpeaking}
57+
isTyping={isTyping}
58+
baseColor={colors.accentColor}
59+
colors={colors.accentColor}
60+
/>
61+
62+
<div>
63+
<div className="font-medium">{mainLabel}</div>
64+
<div className={`text-sm ${
65+
styles.theme === 'dark' ? 'text-gray-300' : 'text-gray-600'
66+
}`}>
67+
{getStatusMessage()}
68+
</div>
69+
</div>
70+
</div>
71+
<div className="flex items-center space-x-2">
72+
<button
73+
onClick={onReset}
74+
className={`w-8 h-8 rounded-full flex items-center justify-center transition-all ${
75+
styles.theme === 'dark'
76+
? 'bg-white bg-opacity-10 hover:bg-opacity-20 text-gray-300'
77+
: 'bg-black bg-opacity-5 hover:bg-opacity-10 text-gray-600'
78+
}`}
79+
title="Reset conversation"
80+
>
81+
<ArrowsClockwiseIcon size={16} weight="bold" />
82+
</button>
83+
<button
84+
onClick={onClose}
85+
className={`w-8 h-8 rounded-full flex items-center justify-center transition-all ${
86+
styles.theme === 'dark'
87+
? 'bg-white bg-opacity-10 hover:bg-opacity-20 text-gray-300'
88+
: 'bg-black bg-opacity-5 hover:bg-opacity-10 text-gray-600'
89+
}`}
90+
>
91+
<XIcon size={16} weight="bold" />
92+
</button>
93+
</div>
94+
</div>
95+
)
96+
}
97+
98+
export default WidgetHeader

0 commit comments

Comments
 (0)