@afncdelacru/brady-chat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +1405 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1374 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -0
- package/src/index.ts +4 -0
- package/src/lib/BradyChatContext.tsx +203 -0
- package/src/lib/EnhancedBradyChat.tsx +590 -0
- package/src/lib/ImageWithFallback.tsx +28 -0
- package/src/lib/InfoRequestForm.tsx +115 -0
- package/src/lib/LeadershipCallForm.tsx +161 -0
- package/src/lib/ModePromptTree.tsx +277 -0
- package/src/lib/PersonalizedOverviewForm.tsx +132 -0
- package/src/lib/QuickSuggestions.tsx +33 -0
- package/src/lib/api/afnBradyApi.ts +48 -0
- package/src/lib/api/bradyHealth.ts +13 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { motion } from 'motion/react';
|
|
5
|
+
|
|
6
|
+
interface InfoRequestFormProps {
|
|
7
|
+
onSubmit: (data: { firstName: string; lastName: string; email: string; phone?: string }) => void;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function InfoRequestForm({ onSubmit, onCancel }: InfoRequestFormProps) {
|
|
12
|
+
const [formData, setFormData] = useState({
|
|
13
|
+
firstName: '',
|
|
14
|
+
lastName: '',
|
|
15
|
+
email: '',
|
|
16
|
+
phone: '',
|
|
17
|
+
});
|
|
18
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
19
|
+
|
|
20
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
const newErrors: Record<string, string> = {};
|
|
23
|
+
|
|
24
|
+
if (!formData.firstName.trim()) {
|
|
25
|
+
newErrors.firstName = 'First name is required';
|
|
26
|
+
}
|
|
27
|
+
if (!formData.lastName.trim()) {
|
|
28
|
+
newErrors.lastName = 'Last name is required';
|
|
29
|
+
}
|
|
30
|
+
if (!formData.email.trim()) {
|
|
31
|
+
newErrors.email = 'Email is required';
|
|
32
|
+
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
33
|
+
newErrors.email = 'Please enter a valid email address';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Object.keys(newErrors).length > 0) {
|
|
37
|
+
setErrors(newErrors);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onSubmit(formData);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<motion.div
|
|
46
|
+
initial={{ opacity: 0, y: 10 }}
|
|
47
|
+
animate={{ opacity: 1, y: 0 }}
|
|
48
|
+
className="bg-gradient-to-br from-white to-zinc-50 dark:from-zinc-800 dark:to-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-2xl p-6 max-h-[360px]"
|
|
49
|
+
>
|
|
50
|
+
<h3 className="text-lg text-zinc-900 dark:text-white mb-4">Send Me More Information</h3>
|
|
51
|
+
|
|
52
|
+
<form onSubmit={handleSubmit} className="space-y-3">
|
|
53
|
+
<div>
|
|
54
|
+
<input
|
|
55
|
+
type="text"
|
|
56
|
+
placeholder="First Name"
|
|
57
|
+
value={formData.firstName}
|
|
58
|
+
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
|
|
59
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
60
|
+
/>
|
|
61
|
+
{errors.firstName && <p className="text-red-500 text-xs mt-1">{errors.firstName}</p>}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div>
|
|
65
|
+
<input
|
|
66
|
+
type="text"
|
|
67
|
+
placeholder="Last Name"
|
|
68
|
+
value={formData.lastName}
|
|
69
|
+
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
|
|
70
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
71
|
+
/>
|
|
72
|
+
{errors.lastName && <p className="text-red-500 text-xs mt-1">{errors.lastName}</p>}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div>
|
|
76
|
+
<input
|
|
77
|
+
type="email"
|
|
78
|
+
placeholder="Email Address"
|
|
79
|
+
value={formData.email}
|
|
80
|
+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
81
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
82
|
+
/>
|
|
83
|
+
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div>
|
|
87
|
+
<input
|
|
88
|
+
type="tel"
|
|
89
|
+
placeholder="Phone (optional)"
|
|
90
|
+
value={formData.phone}
|
|
91
|
+
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
|
92
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="flex gap-2 pt-2">
|
|
97
|
+
<button
|
|
98
|
+
type="submit"
|
|
99
|
+
className="flex-1 bg-[#8B5CF6] hover:bg-[#7C3AED] text-white py-2.5 rounded-lg transition-colors"
|
|
100
|
+
>
|
|
101
|
+
Send Information
|
|
102
|
+
</button>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={onCancel}
|
|
106
|
+
className="px-4 text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
|
107
|
+
>
|
|
108
|
+
Cancel
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
</form>
|
|
112
|
+
</motion.div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { motion } from 'motion/react';
|
|
5
|
+
import { Calendar, Clock } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
interface LeadershipCallFormProps {
|
|
8
|
+
onSubmit: (data: {
|
|
9
|
+
firstName: string;
|
|
10
|
+
lastName: string;
|
|
11
|
+
email: string;
|
|
12
|
+
phone: string;
|
|
13
|
+
preferredDate?: string;
|
|
14
|
+
preferredTime?: string;
|
|
15
|
+
}) => void;
|
|
16
|
+
onCancel: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function LeadershipCallForm({ onSubmit, onCancel }: LeadershipCallFormProps) {
|
|
20
|
+
const [formData, setFormData] = useState({
|
|
21
|
+
firstName: '',
|
|
22
|
+
lastName: '',
|
|
23
|
+
email: '',
|
|
24
|
+
phone: '',
|
|
25
|
+
preferredDate: '',
|
|
26
|
+
preferredTime: '',
|
|
27
|
+
});
|
|
28
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
29
|
+
|
|
30
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
const newErrors: Record<string, string> = {};
|
|
33
|
+
|
|
34
|
+
if (!formData.firstName.trim()) {
|
|
35
|
+
newErrors.firstName = 'First name is required';
|
|
36
|
+
}
|
|
37
|
+
if (!formData.lastName.trim()) {
|
|
38
|
+
newErrors.lastName = 'Last name is required';
|
|
39
|
+
}
|
|
40
|
+
if (!formData.email.trim()) {
|
|
41
|
+
newErrors.email = 'Email is required';
|
|
42
|
+
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
43
|
+
newErrors.email = 'Please enter a valid email address';
|
|
44
|
+
}
|
|
45
|
+
if (!formData.phone.trim()) {
|
|
46
|
+
newErrors.phone = 'Phone number is required';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Object.keys(newErrors).length > 0) {
|
|
50
|
+
setErrors(newErrors);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onSubmit(formData);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const timeSlots = ['Morning (8am - 12pm)', 'Afternoon (12pm - 5pm)', 'Evening (5pm - 8pm)'];
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<motion.div
|
|
61
|
+
initial={{ opacity: 0, y: 10 }}
|
|
62
|
+
animate={{ opacity: 1, y: 0 }}
|
|
63
|
+
className="bg-gradient-to-br from-white to-zinc-50 dark:from-zinc-800 dark:to-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-2xl p-6"
|
|
64
|
+
>
|
|
65
|
+
<h3 className="text-lg text-zinc-900 dark:text-white mb-4">Talk to AFN Leadership</h3>
|
|
66
|
+
|
|
67
|
+
<form onSubmit={handleSubmit} className="space-y-3">
|
|
68
|
+
<div>
|
|
69
|
+
<input
|
|
70
|
+
type="text"
|
|
71
|
+
placeholder="First Name"
|
|
72
|
+
value={formData.firstName}
|
|
73
|
+
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
|
|
74
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
75
|
+
/>
|
|
76
|
+
{errors.firstName && <p className="text-red-500 text-xs mt-1">{errors.firstName}</p>}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div>
|
|
80
|
+
<input
|
|
81
|
+
type="text"
|
|
82
|
+
placeholder="Last Name"
|
|
83
|
+
value={formData.lastName}
|
|
84
|
+
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
|
|
85
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
86
|
+
/>
|
|
87
|
+
{errors.lastName && <p className="text-red-500 text-xs mt-1">{errors.lastName}</p>}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div>
|
|
91
|
+
<input
|
|
92
|
+
type="email"
|
|
93
|
+
placeholder="Email Address"
|
|
94
|
+
value={formData.email}
|
|
95
|
+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
96
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
97
|
+
/>
|
|
98
|
+
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div>
|
|
102
|
+
<input
|
|
103
|
+
type="tel"
|
|
104
|
+
placeholder="Phone Number"
|
|
105
|
+
value={formData.phone}
|
|
106
|
+
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
|
107
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
108
|
+
/>
|
|
109
|
+
{errors.phone && <p className="text-red-500 text-xs mt-1">{errors.phone}</p>}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="pt-2 border-t border-zinc-200 dark:border-zinc-700">
|
|
113
|
+
<p className="text-sm text-zinc-600 dark:text-zinc-400 mb-2">Preferred Call Time (optional)</p>
|
|
114
|
+
<div className="grid grid-cols-2 gap-2">
|
|
115
|
+
<div className="relative">
|
|
116
|
+
<Calendar className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
|
|
117
|
+
<input
|
|
118
|
+
type="date"
|
|
119
|
+
value={formData.preferredDate}
|
|
120
|
+
onChange={(e) => setFormData({ ...formData, preferredDate: e.target.value })}
|
|
121
|
+
className="w-full pl-12 pr-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white text-sm focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="relative">
|
|
125
|
+
<Clock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
|
|
126
|
+
<select
|
|
127
|
+
value={formData.preferredTime}
|
|
128
|
+
onChange={(e) => setFormData({ ...formData, preferredTime: e.target.value })}
|
|
129
|
+
className="w-full pl-10 pr-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white text-sm focus:border-[#8B5CF6] focus:outline-none transition-colors appearance-none"
|
|
130
|
+
>
|
|
131
|
+
<option value="">Select time</option>
|
|
132
|
+
{timeSlots.map((slot) => (
|
|
133
|
+
<option key={slot} value={slot}>
|
|
134
|
+
{slot}
|
|
135
|
+
</option>
|
|
136
|
+
))}
|
|
137
|
+
</select>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div className="flex gap-2 pt-2">
|
|
143
|
+
<button
|
|
144
|
+
type="submit"
|
|
145
|
+
className="flex-1 bg-[#8B5CF6] hover:bg-[#7C3AED] text-white py-2.5 rounded-lg transition-colors"
|
|
146
|
+
>
|
|
147
|
+
Request Call
|
|
148
|
+
</button>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={onCancel}
|
|
152
|
+
className="px-4 text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
|
153
|
+
>
|
|
154
|
+
Cancel
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</form>
|
|
158
|
+
</motion.div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { motion } from 'motion/react';
|
|
4
|
+
|
|
5
|
+
interface ModePromptTreeProps {
|
|
6
|
+
mode: 'earnings' | 'profit';
|
|
7
|
+
step: string;
|
|
8
|
+
hasPersonalizedData: boolean;
|
|
9
|
+
onResponse: (response: string, data?: any) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ModePromptTree({ mode, step, hasPersonalizedData, onResponse }: ModePromptTreeProps) {
|
|
13
|
+
if (mode === 'earnings') {
|
|
14
|
+
if (step === 'volume') {
|
|
15
|
+
if (hasPersonalizedData) {
|
|
16
|
+
return (
|
|
17
|
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-3">
|
|
18
|
+
<div className="grid grid-cols-1 gap-2">
|
|
19
|
+
<button
|
|
20
|
+
onClick={() => onResponse('yes', { volumeMultiplier: 1.0 })}
|
|
21
|
+
className="px-4 py-3 bg-[#8B5CF6] hover:bg-[#7C3AED] text-white rounded-lg text-sm transition-colors text-left"
|
|
22
|
+
>
|
|
23
|
+
Yes, that's about right
|
|
24
|
+
</button>
|
|
25
|
+
<button
|
|
26
|
+
onClick={() => onResponse('higher', { volumeMultiplier: 1.1 })}
|
|
27
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
28
|
+
>
|
|
29
|
+
It's a bit higher
|
|
30
|
+
</button>
|
|
31
|
+
<button
|
|
32
|
+
onClick={() => onResponse('lower', { volumeMultiplier: 0.9 })}
|
|
33
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
34
|
+
>
|
|
35
|
+
It's a bit lower
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
</motion.div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-3">
|
|
44
|
+
<div className="grid grid-cols-1 gap-2">
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => onResponse('Under $500k', { volume: 400000 })}
|
|
47
|
+
className="px-4 py-3 bg-[#8B5CF6] hover:bg-[#7C3AED] text-white rounded-lg text-sm transition-colors text-left"
|
|
48
|
+
>
|
|
49
|
+
Under $500k
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
onClick={() => onResponse('$500k – $1M', { volume: 750000 })}
|
|
53
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
54
|
+
>
|
|
55
|
+
$500k – $1M
|
|
56
|
+
</button>
|
|
57
|
+
<button
|
|
58
|
+
onClick={() => onResponse('$1M – $2M', { volume: 1500000 })}
|
|
59
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
60
|
+
>
|
|
61
|
+
$1M – $2M
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
onClick={() => onResponse('$2M+', { volume: 2500000 })}
|
|
65
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
66
|
+
>
|
|
67
|
+
$2M+
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</motion.div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (step === 'comp') {
|
|
75
|
+
return (
|
|
76
|
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-3">
|
|
77
|
+
<div className="grid grid-cols-1 gap-2">
|
|
78
|
+
<button
|
|
79
|
+
onClick={() => onResponse('Under 100 bps', { comp: 0.95 })}
|
|
80
|
+
className="px-4 py-3 bg-[#8B5CF6] hover:bg-[#7C3AED] text-white rounded-lg text-sm transition-colors text-left"
|
|
81
|
+
>
|
|
82
|
+
Under 100 bps
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
onClick={() => onResponse('Around 125 bps', { comp: 1.25 })}
|
|
86
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
87
|
+
>
|
|
88
|
+
Around 125 bps
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => onResponse('150+ bps', { comp: 1.5 })}
|
|
92
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
93
|
+
>
|
|
94
|
+
150+ bps
|
|
95
|
+
</button>
|
|
96
|
+
<button
|
|
97
|
+
onClick={() => onResponse("I'm not sure", { comp: 1.15 })}
|
|
98
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
99
|
+
>
|
|
100
|
+
I'm not sure
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</motion.div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (step === 'complete') {
|
|
108
|
+
return (
|
|
109
|
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-3">
|
|
110
|
+
<div className="grid grid-cols-1 gap-2">
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => onResponse('Talk through this with AFN')}
|
|
113
|
+
className="px-4 py-3 bg-[#8B5CF6] hover:bg-[#7C3AED] text-white rounded-lg text-sm transition-colors text-left"
|
|
114
|
+
>
|
|
115
|
+
Talk through this with AFN
|
|
116
|
+
</button>
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => onResponse('Adjust the numbers')}
|
|
119
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
120
|
+
>
|
|
121
|
+
Adjust the numbers
|
|
122
|
+
</button>
|
|
123
|
+
<button
|
|
124
|
+
onClick={() => onResponse('Keep exploring')}
|
|
125
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
126
|
+
>
|
|
127
|
+
Keep exploring
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
</motion.div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (mode === 'profit') {
|
|
136
|
+
if (step === 'volume') {
|
|
137
|
+
if (hasPersonalizedData) {
|
|
138
|
+
return (
|
|
139
|
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-3">
|
|
140
|
+
<div className="grid grid-cols-1 gap-2">
|
|
141
|
+
<button
|
|
142
|
+
onClick={() => onResponse('Yes', { volumeMultiplier: 1.0 })}
|
|
143
|
+
className="px-4 py-3 bg-[#4399D1] hover:bg-[#2B7AB8] text-white rounded-lg text-sm transition-colors text-left"
|
|
144
|
+
>
|
|
145
|
+
Yes
|
|
146
|
+
</button>
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => onResponse('Volume is higher', { volumeMultiplier: 1.1 })}
|
|
149
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
150
|
+
>
|
|
151
|
+
Volume is higher
|
|
152
|
+
</button>
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => onResponse('Volume is lower', { volumeMultiplier: 0.9 })}
|
|
155
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
156
|
+
>
|
|
157
|
+
Volume is lower
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</motion.div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-3">
|
|
166
|
+
<div className="grid grid-cols-1 gap-2">
|
|
167
|
+
<button
|
|
168
|
+
onClick={() => onResponse('Under $100M', { volume: 75000000 })}
|
|
169
|
+
className="px-4 py-3 bg-[#4399D1] hover:bg-[#2B7AB8] text-white rounded-lg text-sm transition-colors text-left"
|
|
170
|
+
>
|
|
171
|
+
Under $100M
|
|
172
|
+
</button>
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => onResponse('$100M – $250M', { volume: 175000000 })}
|
|
175
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
176
|
+
>
|
|
177
|
+
$100M – $250M
|
|
178
|
+
</button>
|
|
179
|
+
<button
|
|
180
|
+
onClick={() => onResponse('$250M – $500M', { volume: 375000000 })}
|
|
181
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
182
|
+
>
|
|
183
|
+
$250M – $500M
|
|
184
|
+
</button>
|
|
185
|
+
<button
|
|
186
|
+
onClick={() => onResponse('$500M+', { volume: 650000000 })}
|
|
187
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
188
|
+
>
|
|
189
|
+
$500M+
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</motion.div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (step === 'economics') {
|
|
197
|
+
return (
|
|
198
|
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-3">
|
|
199
|
+
<div className="grid grid-cols-1 gap-2">
|
|
200
|
+
<button
|
|
201
|
+
onClick={() => onResponse('Sounds right', { margin: 3.0, grossProfit: 1.5, opEx: 0.75 })}
|
|
202
|
+
className="px-4 py-3 bg-[#4399D1] hover:bg-[#2B7AB8] text-white rounded-lg text-sm transition-colors text-left"
|
|
203
|
+
>
|
|
204
|
+
Sounds right
|
|
205
|
+
</button>
|
|
206
|
+
<button
|
|
207
|
+
onClick={() =>
|
|
208
|
+
onResponse('Margin is higher', {
|
|
209
|
+
margin: 3.5,
|
|
210
|
+
grossProfit: 1.75,
|
|
211
|
+
opEx: 0.75,
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
215
|
+
>
|
|
216
|
+
Margin is higher
|
|
217
|
+
</button>
|
|
218
|
+
<button
|
|
219
|
+
onClick={() =>
|
|
220
|
+
onResponse('Expenses are higher', {
|
|
221
|
+
margin: 3.0,
|
|
222
|
+
grossProfit: 1.5,
|
|
223
|
+
opEx: 0.95,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
227
|
+
>
|
|
228
|
+
Expenses are higher
|
|
229
|
+
</button>
|
|
230
|
+
<button
|
|
231
|
+
onClick={() =>
|
|
232
|
+
onResponse('Let me adjust', {
|
|
233
|
+
margin: 3.0,
|
|
234
|
+
grossProfit: 1.5,
|
|
235
|
+
opEx: 0.75,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
239
|
+
>
|
|
240
|
+
Let me adjust
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
</motion.div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (step === 'complete') {
|
|
248
|
+
return (
|
|
249
|
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-3">
|
|
250
|
+
<div className="grid grid-cols-1 gap-2">
|
|
251
|
+
<button
|
|
252
|
+
onClick={() => onResponse('Sanity-check with AFN')}
|
|
253
|
+
className="px-4 py-3 bg-[#4399D1] hover:bg-[#2B7AB8] text-white rounded-lg text-sm transition-colors text-left"
|
|
254
|
+
>
|
|
255
|
+
Sanity-check with AFN
|
|
256
|
+
</button>
|
|
257
|
+
<button
|
|
258
|
+
onClick={() => onResponse('Adjust assumptions')}
|
|
259
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
260
|
+
>
|
|
261
|
+
Adjust assumptions
|
|
262
|
+
</button>
|
|
263
|
+
<button
|
|
264
|
+
onClick={() => onResponse('Schedule leadership call')}
|
|
265
|
+
className="px-4 py-3 dark:bg-zinc-800 bg-zinc-100 dark:hover:bg-zinc-700 hover:bg-zinc-200 dark:text-white text-zinc-900 rounded-lg text-sm transition-colors text-left"
|
|
266
|
+
>
|
|
267
|
+
Schedule leadership call
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
</motion.div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { motion } from 'motion/react';
|
|
5
|
+
|
|
6
|
+
interface PersonalizedOverviewFormProps {
|
|
7
|
+
onSubmit: (data: {
|
|
8
|
+
firstName: string;
|
|
9
|
+
lastName: string;
|
|
10
|
+
email: string;
|
|
11
|
+
phone?: string;
|
|
12
|
+
nmlsId?: string;
|
|
13
|
+
}) => void;
|
|
14
|
+
onCancel: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function PersonalizedOverviewForm({ onSubmit, onCancel }: PersonalizedOverviewFormProps) {
|
|
18
|
+
const [formData, setFormData] = useState({
|
|
19
|
+
firstName: '',
|
|
20
|
+
lastName: '',
|
|
21
|
+
email: '',
|
|
22
|
+
phone: '',
|
|
23
|
+
nmlsId: '',
|
|
24
|
+
});
|
|
25
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
26
|
+
|
|
27
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
const newErrors: Record<string, string> = {};
|
|
30
|
+
|
|
31
|
+
if (!formData.firstName.trim()) {
|
|
32
|
+
newErrors.firstName = 'First name is required';
|
|
33
|
+
}
|
|
34
|
+
if (!formData.lastName.trim()) {
|
|
35
|
+
newErrors.lastName = 'Last name is required';
|
|
36
|
+
}
|
|
37
|
+
if (!formData.email.trim()) {
|
|
38
|
+
newErrors.email = 'Email is required';
|
|
39
|
+
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
40
|
+
newErrors.email = 'Please enter a valid email address';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (Object.keys(newErrors).length > 0) {
|
|
44
|
+
setErrors(newErrors);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onSubmit(formData);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<motion.div
|
|
53
|
+
initial={{ opacity: 0, y: 10 }}
|
|
54
|
+
animate={{ opacity: 1, y: 0 }}
|
|
55
|
+
className="bg-gradient-to-br from-white to-zinc-50 dark:from-zinc-800 dark:to-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-2xl p-6"
|
|
56
|
+
>
|
|
57
|
+
<h3 className="text-lg text-zinc-900 dark:text-white mb-4">Request Personalized Overview</h3>
|
|
58
|
+
|
|
59
|
+
<form onSubmit={handleSubmit} className="space-y-3">
|
|
60
|
+
<div>
|
|
61
|
+
<input
|
|
62
|
+
type="text"
|
|
63
|
+
placeholder="First Name"
|
|
64
|
+
value={formData.firstName}
|
|
65
|
+
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
|
|
66
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
67
|
+
/>
|
|
68
|
+
{errors.firstName && <p className="text-red-500 text-xs mt-1">{errors.firstName}</p>}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div>
|
|
72
|
+
<input
|
|
73
|
+
type="text"
|
|
74
|
+
placeholder="Last Name"
|
|
75
|
+
value={formData.lastName}
|
|
76
|
+
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
|
|
77
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
78
|
+
/>
|
|
79
|
+
{errors.lastName && <p className="text-red-500 text-xs mt-1">{errors.lastName}</p>}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div>
|
|
83
|
+
<input
|
|
84
|
+
type="email"
|
|
85
|
+
placeholder="Email Address"
|
|
86
|
+
value={formData.email}
|
|
87
|
+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
88
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
89
|
+
/>
|
|
90
|
+
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div>
|
|
94
|
+
<input
|
|
95
|
+
type="tel"
|
|
96
|
+
placeholder="Phone (optional)"
|
|
97
|
+
value={formData.phone}
|
|
98
|
+
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
|
99
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div>
|
|
104
|
+
<input
|
|
105
|
+
type="text"
|
|
106
|
+
placeholder="NMLS ID (optional)"
|
|
107
|
+
value={formData.nmlsId}
|
|
108
|
+
onChange={(e) => setFormData({ ...formData, nmlsId: e.target.value })}
|
|
109
|
+
className="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white placeholder-zinc-500 focus:border-[#8B5CF6] focus:outline-none transition-colors"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="flex gap-2 pt-2">
|
|
114
|
+
<button
|
|
115
|
+
type="submit"
|
|
116
|
+
className="flex-1 bg-[#8B5CF6] hover:bg-[#7C3AED] text-white py-2.5 rounded-lg transition-colors"
|
|
117
|
+
>
|
|
118
|
+
Request Overview
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={onCancel}
|
|
123
|
+
className="px-4 text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
|
124
|
+
>
|
|
125
|
+
Cancel
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</form>
|
|
129
|
+
</motion.div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|