@afncdelacru/brady-chat 0.4.0 → 0.4.1
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.js +1389 -1387
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1357 -1355
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/lib/BradyChatContext.tsx +1 -0
- package/src/lib/EnhancedBradyChat.tsx +34 -3
- package/src/lib/BranchProfitabilityCalculator.tsx +0 -615
- package/src/lib/CalculatorFollowUp.tsx +0 -200
- package/src/lib/CalculatorManager.tsx +0 -37
- package/src/lib/LOEarningsCalculator.tsx +0 -366
- package/src/lib/ProgressiveContactForm.tsx +0 -337
- package/src/lib/RecruitingFlowContext.tsx +0 -259
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { motion } from 'motion/react';
|
|
3
|
-
import { useRecruitingFlow, UserRole } from './RecruitingFlowContext';
|
|
4
|
-
|
|
5
|
-
interface ProgressiveContactFormProps {
|
|
6
|
-
onSubmit: (data: any) => void;
|
|
7
|
-
onCancel?: () => void;
|
|
8
|
-
primaryCTA: string;
|
|
9
|
-
secondaryCTA?: string;
|
|
10
|
-
context: 'LO' | 'BM';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function ProgressiveContactForm({
|
|
14
|
-
onSubmit,
|
|
15
|
-
onCancel,
|
|
16
|
-
primaryCTA,
|
|
17
|
-
secondaryCTA = "Not right now",
|
|
18
|
-
context
|
|
19
|
-
}: ProgressiveContactFormProps) {
|
|
20
|
-
const { identityState, userIdentity, currentRole, setCurrentRole, sessionData, updateSessionData } = useRecruitingFlow();
|
|
21
|
-
|
|
22
|
-
type RoleType = 'LO' | 'BM';
|
|
23
|
-
const [formData, setFormData] = useState<{
|
|
24
|
-
firstName: string;
|
|
25
|
-
lastName: string;
|
|
26
|
-
email: string;
|
|
27
|
-
phone: string;
|
|
28
|
-
role: RoleType;
|
|
29
|
-
notes: string;
|
|
30
|
-
}>({
|
|
31
|
-
firstName: userIdentity.firstName || '',
|
|
32
|
-
lastName: userIdentity.lastName || '',
|
|
33
|
-
email: userIdentity.email || '',
|
|
34
|
-
phone: userIdentity.phone || '',
|
|
35
|
-
role: (currentRole === 'BM' || currentRole === 'LO') ? currentRole : (context === 'BM' ? 'BM' : 'LO'),
|
|
36
|
-
notes: ''
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const [showNotes, setShowNotes] = useState(false);
|
|
40
|
-
const [roleChanged, setRoleChanged] = useState(false);
|
|
41
|
-
|
|
42
|
-
const handleRoleChange = (newRole: UserRole) => {
|
|
43
|
-
setFormData(prev => ({ ...prev, role: newRole }));
|
|
44
|
-
|
|
45
|
-
// Detect role crossover
|
|
46
|
-
if (newRole !== (context === 'BM' ? 'BM' : 'LO')) {
|
|
47
|
-
setRoleChanged(true);
|
|
48
|
-
setCurrentRole(newRole);
|
|
49
|
-
} else {
|
|
50
|
-
setRoleChanged(false);
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const handleSubmit = (e: React.FormEvent) => {
|
|
55
|
-
e.preventDefault();
|
|
56
|
-
|
|
57
|
-
// Update session data with role at submit
|
|
58
|
-
updateSessionData({
|
|
59
|
-
rolePrefAtSubmit: formData.role,
|
|
60
|
-
submissionTimestamp: Date.now()
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Tag submission with all tracking data
|
|
64
|
-
const submissionData = {
|
|
65
|
-
...formData,
|
|
66
|
-
...sessionData,
|
|
67
|
-
intentLevel: sessionData,
|
|
68
|
-
timestamp: Date.now()
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
onSubmit(submissionData);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const updateField = (field: string, value: string) => {
|
|
75
|
-
setFormData(prev => ({ ...prev, [field]: value }));
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Render based on identity state
|
|
79
|
-
const renderForm = () => {
|
|
80
|
-
switch (identityState) {
|
|
81
|
-
case 'KNOWN_RECRUIT':
|
|
82
|
-
return (
|
|
83
|
-
<>
|
|
84
|
-
{/* Pre-filled email */}
|
|
85
|
-
<div>
|
|
86
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
87
|
-
Email
|
|
88
|
-
</label>
|
|
89
|
-
<input
|
|
90
|
-
type="email"
|
|
91
|
-
value={formData.email}
|
|
92
|
-
onChange={(e) => updateField('email', e.target.value)}
|
|
93
|
-
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#4399D1] focus:border-transparent"
|
|
94
|
-
required
|
|
95
|
-
/>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
{/* Pre-filled phone if exists */}
|
|
99
|
-
{formData.phone && (
|
|
100
|
-
<div>
|
|
101
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
102
|
-
Mobile Phone
|
|
103
|
-
</label>
|
|
104
|
-
<input
|
|
105
|
-
type="tel"
|
|
106
|
-
value={formData.phone}
|
|
107
|
-
onChange={(e) => updateField('phone', e.target.value)}
|
|
108
|
-
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#4399D1] focus:border-transparent"
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
)}
|
|
112
|
-
|
|
113
|
-
{/* Optional notes (collapsed) */}
|
|
114
|
-
{!showNotes ? (
|
|
115
|
-
<button
|
|
116
|
-
type="button"
|
|
117
|
-
onClick={() => setShowNotes(true)}
|
|
118
|
-
className="text-sm text-[#4399D1] hover:text-[#2B7AB8] transition-colors"
|
|
119
|
-
>
|
|
120
|
-
+ Anything we should know before the conversation?
|
|
121
|
-
</button>
|
|
122
|
-
) : (
|
|
123
|
-
<div>
|
|
124
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
125
|
-
Additional Notes (Optional)
|
|
126
|
-
</label>
|
|
127
|
-
<textarea
|
|
128
|
-
value={formData.notes}
|
|
129
|
-
onChange={(e) => updateField('notes', e.target.value)}
|
|
130
|
-
rows={3}
|
|
131
|
-
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#4399D1] focus:border-transparent resize-none"
|
|
132
|
-
placeholder="Any questions or specific scenarios you'd like to discuss..."
|
|
133
|
-
/>
|
|
134
|
-
</div>
|
|
135
|
-
)}
|
|
136
|
-
</>
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
case 'SEMI_KNOWN':
|
|
140
|
-
return (
|
|
141
|
-
<>
|
|
142
|
-
{/* Pre-filled name */}
|
|
143
|
-
<div className="grid grid-cols-2 gap-4">
|
|
144
|
-
<div>
|
|
145
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
146
|
-
First Name
|
|
147
|
-
</label>
|
|
148
|
-
<input
|
|
149
|
-
type="text"
|
|
150
|
-
value={formData.firstName}
|
|
151
|
-
onChange={(e) => updateField('firstName', e.target.value)}
|
|
152
|
-
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#4399D1] focus:border-transparent"
|
|
153
|
-
required
|
|
154
|
-
/>
|
|
155
|
-
</div>
|
|
156
|
-
<div>
|
|
157
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
158
|
-
Last Name
|
|
159
|
-
</label>
|
|
160
|
-
<input
|
|
161
|
-
type="text"
|
|
162
|
-
value={formData.lastName}
|
|
163
|
-
onChange={(e) => updateField('lastName', e.target.value)}
|
|
164
|
-
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#4399D1] focus:border-transparent"
|
|
165
|
-
required
|
|
166
|
-
/>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
{/* Email required */}
|
|
171
|
-
<div>
|
|
172
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
173
|
-
Email
|
|
174
|
-
</label>
|
|
175
|
-
<input
|
|
176
|
-
type="email"
|
|
177
|
-
value={formData.email}
|
|
178
|
-
onChange={(e) => updateField('email', e.target.value)}
|
|
179
|
-
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#4399D1] focus:border-transparent"
|
|
180
|
-
required
|
|
181
|
-
/>
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
{/* Phone optional */}
|
|
185
|
-
<div>
|
|
186
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
187
|
-
Mobile Phone (Optional)
|
|
188
|
-
</label>
|
|
189
|
-
<input
|
|
190
|
-
type="tel"
|
|
191
|
-
value={formData.phone}
|
|
192
|
-
onChange={(e) => updateField('phone', e.target.value)}
|
|
193
|
-
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#4399D1] focus:border-transparent"
|
|
194
|
-
/>
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
{/* Role selector */}
|
|
198
|
-
<div>
|
|
199
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
200
|
-
I'm exploring
|
|
201
|
-
</label>
|
|
202
|
-
<div className="flex gap-4">
|
|
203
|
-
<label className="flex-1 flex items-center gap-3 px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg cursor-pointer hover:border-[#4399D1] transition-colors">
|
|
204
|
-
<input
|
|
205
|
-
type="radio"
|
|
206
|
-
name="role"
|
|
207
|
-
value="LO"
|
|
208
|
-
checked={formData.role === 'LO'}
|
|
209
|
-
onChange={() => handleRoleChange('LO')}
|
|
210
|
-
className="text-[#4399D1] focus:ring-[#4399D1]"
|
|
211
|
-
/>
|
|
212
|
-
<span className="text-sm text-zinc-900 dark:text-white">Loan Officer</span>
|
|
213
|
-
</label>
|
|
214
|
-
<label className="flex-1 flex items-center gap-3 px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg cursor-pointer hover:border-[#4399D1] transition-colors">
|
|
215
|
-
<input
|
|
216
|
-
type="radio"
|
|
217
|
-
name="role"
|
|
218
|
-
value="BM"
|
|
219
|
-
checked={formData.role === 'BM'}
|
|
220
|
-
onChange={() => handleRoleChange('BM')}
|
|
221
|
-
className="text-[#4399D1] focus:ring-[#4399D1]"
|
|
222
|
-
/>
|
|
223
|
-
<span className="text-sm text-zinc-900 dark:text-white">Branch Manager</span>
|
|
224
|
-
</label>
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
</>
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
case 'ANONYMOUS':
|
|
231
|
-
default:
|
|
232
|
-
return (
|
|
233
|
-
<>
|
|
234
|
-
{/* First name required */}
|
|
235
|
-
<div>
|
|
236
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
237
|
-
First Name
|
|
238
|
-
</label>
|
|
239
|
-
<input
|
|
240
|
-
type="text"
|
|
241
|
-
value={formData.firstName}
|
|
242
|
-
onChange={(e) => updateField('firstName', e.target.value)}
|
|
243
|
-
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#4399D1] focus:border-transparent"
|
|
244
|
-
required
|
|
245
|
-
placeholder="Enter your first name"
|
|
246
|
-
/>
|
|
247
|
-
</div>
|
|
248
|
-
|
|
249
|
-
{/* Email required */}
|
|
250
|
-
<div>
|
|
251
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-2">
|
|
252
|
-
Email
|
|
253
|
-
</label>
|
|
254
|
-
<input
|
|
255
|
-
type="email"
|
|
256
|
-
value={formData.email}
|
|
257
|
-
onChange={(e) => updateField('email', e.target.value)}
|
|
258
|
-
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-[#4399D1] focus:border-transparent"
|
|
259
|
-
required
|
|
260
|
-
placeholder="your.email@example.com"
|
|
261
|
-
/>
|
|
262
|
-
</div>
|
|
263
|
-
|
|
264
|
-
{/* Role selector (optional) */}
|
|
265
|
-
<div>
|
|
266
|
-
<label className="block text-sm text-zinc-700 dark:text-zinc-300 mb-3">
|
|
267
|
-
I'm exploring (optional)
|
|
268
|
-
</label>
|
|
269
|
-
<div className="flex gap-4">
|
|
270
|
-
<label className="flex-1 flex items-center gap-3 px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg cursor-pointer hover:border-[#4399D1] transition-colors">
|
|
271
|
-
<input
|
|
272
|
-
type="radio"
|
|
273
|
-
name="role"
|
|
274
|
-
value="LO"
|
|
275
|
-
checked={formData.role === 'LO'}
|
|
276
|
-
onChange={() => handleRoleChange('LO')}
|
|
277
|
-
className="text-[#4399D1] focus:ring-[#4399D1]"
|
|
278
|
-
/>
|
|
279
|
-
<span className="text-sm text-zinc-900 dark:text-white">Loan Officer</span>
|
|
280
|
-
</label>
|
|
281
|
-
<label className="flex-1 flex items-center gap-3 px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded-lg cursor-pointer hover:border-[#4399D1] transition-colors">
|
|
282
|
-
<input
|
|
283
|
-
type="radio"
|
|
284
|
-
name="role"
|
|
285
|
-
value="BM"
|
|
286
|
-
checked={formData.role === 'BM'}
|
|
287
|
-
onChange={() => handleRoleChange('BM')}
|
|
288
|
-
className="text-[#4399D1] focus:ring-[#4399D1]"
|
|
289
|
-
/>
|
|
290
|
-
<span className="text-sm text-zinc-900 dark:text-white">Branch Manager</span>
|
|
291
|
-
</label>
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
</>
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
return (
|
|
300
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
301
|
-
{/* Role Crossover Confirmation */}
|
|
302
|
-
{roleChanged && (
|
|
303
|
-
<motion.div
|
|
304
|
-
initial={{ opacity: 0, height: 0 }}
|
|
305
|
-
animate={{ opacity: 1, height: 'auto' }}
|
|
306
|
-
className="p-3 bg-[#4399D1]/10 border border-[#4399D1]/20 rounded-lg"
|
|
307
|
-
>
|
|
308
|
-
<p className="text-sm text-[#4399D1]">
|
|
309
|
-
Got it — we'll tailor this for a {formData.role === 'BM' ? 'Branch Manager' : 'Loan Officer'} perspective.
|
|
310
|
-
</p>
|
|
311
|
-
</motion.div>
|
|
312
|
-
)}
|
|
313
|
-
|
|
314
|
-
{/* Progressive Form Fields */}
|
|
315
|
-
{renderForm()}
|
|
316
|
-
|
|
317
|
-
{/* CTAs */}
|
|
318
|
-
<div className="space-y-3 pt-4">
|
|
319
|
-
<button
|
|
320
|
-
type="submit"
|
|
321
|
-
className="w-full px-6 py-3 bg-gradient-to-r from-[#4399D1] to-[#2B7AB8] hover:from-[#2B7AB8] hover:to-[#4399D1] text-white rounded-lg transition-all duration-300 shadow-lg"
|
|
322
|
-
>
|
|
323
|
-
{primaryCTA}
|
|
324
|
-
</button>
|
|
325
|
-
{onCancel && (
|
|
326
|
-
<button
|
|
327
|
-
type="button"
|
|
328
|
-
onClick={onCancel}
|
|
329
|
-
className="w-full px-6 py-3 bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-300 dark:border-zinc-700 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors"
|
|
330
|
-
>
|
|
331
|
-
{secondaryCTA}
|
|
332
|
-
</button>
|
|
333
|
-
)}
|
|
334
|
-
</div>
|
|
335
|
-
</form>
|
|
336
|
-
);
|
|
337
|
-
}
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
// Identity States
|
|
4
|
-
export type IdentityState = 'KNOWN_RECRUIT' | 'SEMI_KNOWN' | 'ANONYMOUS';
|
|
5
|
-
|
|
6
|
-
// Intent Levels
|
|
7
|
-
export type IntentLevel = 'LOW' | 'MEDIUM' | 'HIGH';
|
|
8
|
-
|
|
9
|
-
// User Role
|
|
10
|
-
export type UserRole = 'LO' | 'BM' | 'UNKNOWN';
|
|
11
|
-
|
|
12
|
-
// Page Context
|
|
13
|
-
export type PageContext = 'LO' | 'BM';
|
|
14
|
-
|
|
15
|
-
// User Identity Data
|
|
16
|
-
interface UserIdentity {
|
|
17
|
-
firstName?: string;
|
|
18
|
-
lastName?: string;
|
|
19
|
-
email?: string;
|
|
20
|
-
phone?: string;
|
|
21
|
-
role?: UserRole;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Intent Tracking
|
|
25
|
-
interface IntentData {
|
|
26
|
-
level: IntentLevel;
|
|
27
|
-
actions: string[];
|
|
28
|
-
calculatorOpens: number;
|
|
29
|
-
calculatorAdjustments: number;
|
|
30
|
-
calculatorTimeSpent: number;
|
|
31
|
-
highIntentActions: string[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Session Data for Tagging
|
|
35
|
-
interface SessionData {
|
|
36
|
-
identityState: IdentityState;
|
|
37
|
-
pageContext: PageContext;
|
|
38
|
-
pageType: 'Personalized' | 'Generic';
|
|
39
|
-
calculatorsUsed: ('LO' | 'BM')[];
|
|
40
|
-
userAdjustedInputs: boolean;
|
|
41
|
-
rolePrefAtSubmit?: UserRole;
|
|
42
|
-
submissionTimestamp?: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface RecruitingFlowContextType {
|
|
46
|
-
// Identity
|
|
47
|
-
identityState: IdentityState;
|
|
48
|
-
userIdentity: UserIdentity;
|
|
49
|
-
updateUserIdentity: (data: Partial<UserIdentity>) => void;
|
|
50
|
-
|
|
51
|
-
// Intent
|
|
52
|
-
intentData: IntentData;
|
|
53
|
-
trackAction: (action: string, type?: 'low' | 'medium' | 'high') => void;
|
|
54
|
-
trackCalculatorOpen: (type: 'LO' | 'BM') => void;
|
|
55
|
-
trackCalculatorAdjustment: () => void;
|
|
56
|
-
trackCalculatorTime: (seconds: number) => void;
|
|
57
|
-
|
|
58
|
-
// Role Management
|
|
59
|
-
currentRole: UserRole;
|
|
60
|
-
setCurrentRole: (role: UserRole) => void;
|
|
61
|
-
|
|
62
|
-
// Session
|
|
63
|
-
sessionData: SessionData;
|
|
64
|
-
updateSessionData: (data: Partial<SessionData>) => void;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const RecruitingFlowContext = createContext<RecruitingFlowContextType | undefined>(undefined);
|
|
68
|
-
|
|
69
|
-
interface RecruitingFlowProviderProps {
|
|
70
|
-
children: ReactNode;
|
|
71
|
-
initialIdentityState?: IdentityState;
|
|
72
|
-
initialUserData?: UserIdentity;
|
|
73
|
-
pageContext: PageContext;
|
|
74
|
-
pageType: 'Personalized' | 'Generic';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function RecruitingFlowProvider({
|
|
78
|
-
children,
|
|
79
|
-
initialIdentityState = 'ANONYMOUS',
|
|
80
|
-
initialUserData = {},
|
|
81
|
-
pageContext,
|
|
82
|
-
pageType
|
|
83
|
-
}: RecruitingFlowProviderProps) {
|
|
84
|
-
const [identityState, setIdentityState] = useState<IdentityState>(initialIdentityState);
|
|
85
|
-
const [userIdentity, setUserIdentity] = useState<UserIdentity>(initialUserData);
|
|
86
|
-
const [currentRole, setCurrentRole] = useState<UserRole>(initialUserData.role || 'UNKNOWN');
|
|
87
|
-
|
|
88
|
-
const [intentData, setIntentData] = useState<IntentData>({
|
|
89
|
-
level: 'LOW',
|
|
90
|
-
actions: [],
|
|
91
|
-
calculatorOpens: 0,
|
|
92
|
-
calculatorAdjustments: 0,
|
|
93
|
-
calculatorTimeSpent: 0,
|
|
94
|
-
highIntentActions: []
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const [sessionData, setSessionData] = useState<SessionData>({
|
|
98
|
-
identityState,
|
|
99
|
-
pageContext,
|
|
100
|
-
pageType,
|
|
101
|
-
calculatorsUsed: [],
|
|
102
|
-
userAdjustedInputs: false
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Update identity state when user data changes
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
const hasName = !!(userIdentity.firstName && userIdentity.lastName);
|
|
108
|
-
const hasContact = !!(userIdentity.email || userIdentity.phone);
|
|
109
|
-
|
|
110
|
-
if (hasName && hasContact) {
|
|
111
|
-
setIdentityState('KNOWN_RECRUIT');
|
|
112
|
-
} else if (hasName) {
|
|
113
|
-
setIdentityState('SEMI_KNOWN');
|
|
114
|
-
} else {
|
|
115
|
-
setIdentityState('ANONYMOUS');
|
|
116
|
-
}
|
|
117
|
-
}, [userIdentity]);
|
|
118
|
-
|
|
119
|
-
// Update session data when identity state changes
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
setSessionData(prev => ({ ...prev, identityState }));
|
|
122
|
-
}, [identityState]);
|
|
123
|
-
|
|
124
|
-
const updateUserIdentity = useCallback((data: Partial<UserIdentity>) => {
|
|
125
|
-
setUserIdentity(prev => ({ ...prev, ...data }));
|
|
126
|
-
}, []);
|
|
127
|
-
|
|
128
|
-
const trackAction = useCallback((action: string, type: 'low' | 'medium' | 'high' = 'low') => {
|
|
129
|
-
setIntentData(prev => {
|
|
130
|
-
const newActions = [...prev.actions, action];
|
|
131
|
-
let newLevel = prev.level;
|
|
132
|
-
const newHighIntentActions = [...prev.highIntentActions];
|
|
133
|
-
|
|
134
|
-
// Upgrade intent level (never downgrade)
|
|
135
|
-
if (type === 'high') {
|
|
136
|
-
newLevel = 'HIGH';
|
|
137
|
-
newHighIntentActions.push(action);
|
|
138
|
-
} else if (type === 'medium' && prev.level === 'LOW') {
|
|
139
|
-
newLevel = 'MEDIUM';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
...prev,
|
|
144
|
-
level: newLevel,
|
|
145
|
-
actions: newActions,
|
|
146
|
-
highIntentActions: newHighIntentActions
|
|
147
|
-
};
|
|
148
|
-
});
|
|
149
|
-
}, []);
|
|
150
|
-
|
|
151
|
-
const trackCalculatorOpen = useCallback((type: 'LO' | 'BM') => {
|
|
152
|
-
setIntentData(prev => {
|
|
153
|
-
const newOpens = prev.calculatorOpens + 1;
|
|
154
|
-
const newActions = [...prev.actions];
|
|
155
|
-
let newLevel = prev.level;
|
|
156
|
-
|
|
157
|
-
// Multiple opens = medium intent
|
|
158
|
-
if (newOpens > 1 && prev.level === 'LOW') {
|
|
159
|
-
newLevel = 'MEDIUM';
|
|
160
|
-
newActions.push('calculator_reopened');
|
|
161
|
-
} else if (newOpens === 1) {
|
|
162
|
-
newActions.push('calculator_opened');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
...prev,
|
|
167
|
-
calculatorOpens: newOpens,
|
|
168
|
-
level: newLevel,
|
|
169
|
-
actions: newActions
|
|
170
|
-
};
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
setSessionData(prev => {
|
|
174
|
-
const calculatorsUsed = prev.calculatorsUsed.includes(type)
|
|
175
|
-
? prev.calculatorsUsed
|
|
176
|
-
: [...prev.calculatorsUsed, type];
|
|
177
|
-
return { ...prev, calculatorsUsed };
|
|
178
|
-
});
|
|
179
|
-
}, []);
|
|
180
|
-
|
|
181
|
-
const trackCalculatorAdjustment = useCallback(() => {
|
|
182
|
-
setIntentData(prev => {
|
|
183
|
-
const newAdjustments = prev.calculatorAdjustments + 1;
|
|
184
|
-
let newLevel = prev.level;
|
|
185
|
-
const newActions = [...prev.actions];
|
|
186
|
-
|
|
187
|
-
// Multiple adjustments = medium intent (only upgrade, never downgrade)
|
|
188
|
-
if (newAdjustments >= 2 && prev.level === 'LOW') {
|
|
189
|
-
newLevel = 'MEDIUM';
|
|
190
|
-
newActions.push('calculator_adjusted_multiple');
|
|
191
|
-
} else if (newAdjustments === 1) {
|
|
192
|
-
newActions.push('calculator_adjusted');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
...prev,
|
|
197
|
-
calculatorAdjustments: newAdjustments,
|
|
198
|
-
level: newLevel,
|
|
199
|
-
actions: newActions
|
|
200
|
-
};
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
setSessionData(prev => ({ ...prev, userAdjustedInputs: true }));
|
|
204
|
-
}, []);
|
|
205
|
-
|
|
206
|
-
const trackCalculatorTime = useCallback((seconds: number) => {
|
|
207
|
-
setIntentData(prev => {
|
|
208
|
-
const newTimeSpent = prev.calculatorTimeSpent + seconds;
|
|
209
|
-
let newLevel = prev.level;
|
|
210
|
-
const newActions = [...prev.actions];
|
|
211
|
-
|
|
212
|
-
// Lingering > 30 seconds = medium intent
|
|
213
|
-
if (newTimeSpent > 30 && prev.level === 'LOW' && prev.calculatorTimeSpent <= 30) {
|
|
214
|
-
newLevel = 'MEDIUM';
|
|
215
|
-
newActions.push('calculator_lingered');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
...prev,
|
|
220
|
-
calculatorTimeSpent: newTimeSpent,
|
|
221
|
-
level: newLevel,
|
|
222
|
-
actions: newActions
|
|
223
|
-
};
|
|
224
|
-
});
|
|
225
|
-
}, []);
|
|
226
|
-
|
|
227
|
-
const updateSessionData = useCallback((data: Partial<SessionData>) => {
|
|
228
|
-
setSessionData(prev => ({ ...prev, ...data }));
|
|
229
|
-
}, []);
|
|
230
|
-
|
|
231
|
-
const value: RecruitingFlowContextType = {
|
|
232
|
-
identityState,
|
|
233
|
-
userIdentity,
|
|
234
|
-
updateUserIdentity,
|
|
235
|
-
intentData,
|
|
236
|
-
trackAction,
|
|
237
|
-
trackCalculatorOpen,
|
|
238
|
-
trackCalculatorAdjustment,
|
|
239
|
-
trackCalculatorTime,
|
|
240
|
-
currentRole,
|
|
241
|
-
setCurrentRole,
|
|
242
|
-
sessionData,
|
|
243
|
-
updateSessionData
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
return (
|
|
247
|
-
<RecruitingFlowContext.Provider value={value}>
|
|
248
|
-
{children}
|
|
249
|
-
</RecruitingFlowContext.Provider>
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export function useRecruitingFlow() {
|
|
254
|
-
const context = useContext(RecruitingFlowContext);
|
|
255
|
-
if (context === undefined) {
|
|
256
|
-
throw new Error('useRecruitingFlow must be used within a RecruitingFlowProvider');
|
|
257
|
-
}
|
|
258
|
-
return context;
|
|
259
|
-
}
|