@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,590 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { motion, AnimatePresence } from 'motion/react';
|
|
5
|
+
import { Send, Mic } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { checkBradyHealth } from './api/bradyHealth';
|
|
8
|
+
import { sendBradyPrompt } from './api/afnBradyApi';
|
|
9
|
+
import { useBradyChat } from './BradyChatContext';
|
|
10
|
+
import { InfoRequestForm } from './InfoRequestForm';
|
|
11
|
+
import { LeadershipCallForm } from './LeadershipCallForm';
|
|
12
|
+
import { PersonalizedOverviewForm } from './PersonalizedOverviewForm';
|
|
13
|
+
import { QuickSuggestions } from './QuickSuggestions';
|
|
14
|
+
import { ModePromptTree } from './ModePromptTree';
|
|
15
|
+
import { ImageWithFallback } from './ImageWithFallback';
|
|
16
|
+
|
|
17
|
+
type ModeVariant = 'loan-officer' | 'branch-manager';
|
|
18
|
+
|
|
19
|
+
export interface EnhancedBradyChatProps {
|
|
20
|
+
/**
|
|
21
|
+
* Controls whether the mode toggle uses Earnings (LO) or Profit (branch manager) language.
|
|
22
|
+
* Defaults to 'loan-officer'.
|
|
23
|
+
*/
|
|
24
|
+
modeVariant?: ModeVariant;
|
|
25
|
+
/**
|
|
26
|
+
* Avatar image URL for Brady.
|
|
27
|
+
* Typically you pass something like `/bradyIcon.png` from your app's public assets.
|
|
28
|
+
*/
|
|
29
|
+
avatarSrc: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function EnhancedBradyChat({ modeVariant = 'loan-officer', avatarSrc }: EnhancedBradyChatProps) {
|
|
33
|
+
const [bradyHealthy, setBradyHealthy] = useState(true);
|
|
34
|
+
const {
|
|
35
|
+
workflowType,
|
|
36
|
+
messages,
|
|
37
|
+
addMessage,
|
|
38
|
+
resetChat,
|
|
39
|
+
setWorkflow,
|
|
40
|
+
activeMode,
|
|
41
|
+
modeStep,
|
|
42
|
+
modeData,
|
|
43
|
+
setModeStep,
|
|
44
|
+
updateModeData,
|
|
45
|
+
setCalculatorOpen,
|
|
46
|
+
activateMode,
|
|
47
|
+
resetMode,
|
|
48
|
+
isHidden,
|
|
49
|
+
setIsHidden,
|
|
50
|
+
} = useBradyChat();
|
|
51
|
+
const [inputValue, setInputValue] = useState('');
|
|
52
|
+
const [showForm, setShowForm] = useState(false);
|
|
53
|
+
const [inputDisabled, setInputDisabled] = useState(false);
|
|
54
|
+
const [showModePrompts, setShowModePrompts] = useState(false);
|
|
55
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
56
|
+
const [suggestionLinks, setSuggestionLinks] = useState<{ text: string; prompt: string }[] | null>(null);
|
|
57
|
+
|
|
58
|
+
const isBranchManager = modeVariant === 'branch-manager';
|
|
59
|
+
const pageMode = isBranchManager ? 'profit' : 'earnings';
|
|
60
|
+
const isModeActive = activeMode === pageMode;
|
|
61
|
+
const hasPersonalizedData = false;
|
|
62
|
+
|
|
63
|
+
const handleFocusToggle = () => {
|
|
64
|
+
if (isModeActive) {
|
|
65
|
+
resetMode();
|
|
66
|
+
} else {
|
|
67
|
+
activateMode(pageMode);
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
addMessage({
|
|
70
|
+
type: 'brady',
|
|
71
|
+
text: `${
|
|
72
|
+
isBranchManager ? 'Profit' : 'Earnings'
|
|
73
|
+
} Mode is on.\n\nI'll focus on ways to increase ${
|
|
74
|
+
isBranchManager ? 'profit' : 'income'
|
|
75
|
+
} using ${
|
|
76
|
+
isBranchManager
|
|
77
|
+
? 'branch economics, recruiting leverage, and operational efficiency'
|
|
78
|
+
: 'volume, efficiency, and time savings'
|
|
79
|
+
}.`,
|
|
80
|
+
});
|
|
81
|
+
}, 300);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
let mounted = true;
|
|
87
|
+
const checkHealth = async () => {
|
|
88
|
+
const healthy = await checkBradyHealth();
|
|
89
|
+
if (mounted) setBradyHealthy(healthy);
|
|
90
|
+
};
|
|
91
|
+
checkHealth();
|
|
92
|
+
const interval = setInterval(checkHealth, 60000);
|
|
93
|
+
return () => {
|
|
94
|
+
mounted = false;
|
|
95
|
+
clearInterval(interval);
|
|
96
|
+
};
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (
|
|
101
|
+
workflowType === 'info-request' ||
|
|
102
|
+
workflowType === 'leadership-call' ||
|
|
103
|
+
workflowType === 'personalized-overview-request'
|
|
104
|
+
) {
|
|
105
|
+
setShowForm(true);
|
|
106
|
+
setInputDisabled(true);
|
|
107
|
+
setShowModePrompts(false);
|
|
108
|
+
} else {
|
|
109
|
+
setShowForm(false);
|
|
110
|
+
setInputDisabled(false);
|
|
111
|
+
}
|
|
112
|
+
}, [workflowType]);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (activeMode !== 'none' && modeStep === 'volume') {
|
|
116
|
+
setShowModePrompts(true);
|
|
117
|
+
|
|
118
|
+
if (activeMode === 'earnings') {
|
|
119
|
+
if (hasPersonalizedData) {
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
addMessage({
|
|
122
|
+
type: 'brady',
|
|
123
|
+
text: 'I have an estimate of your recent production.\nDoes this feel close?',
|
|
124
|
+
});
|
|
125
|
+
}, 500);
|
|
126
|
+
} else {
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
addMessage({
|
|
129
|
+
type: 'brady',
|
|
130
|
+
text: 'To keep this realistic, about how much volume do you fund in an average month?',
|
|
131
|
+
});
|
|
132
|
+
}, 500);
|
|
133
|
+
}
|
|
134
|
+
} else if (activeMode === 'profit') {
|
|
135
|
+
if (hasPersonalizedData) {
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
addMessage({
|
|
138
|
+
type: 'brady',
|
|
139
|
+
text: 'I have a baseline view of your branch.\nDoes this feel directionally right?',
|
|
140
|
+
});
|
|
141
|
+
}, 500);
|
|
142
|
+
} else {
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
addMessage({
|
|
145
|
+
type: 'brady',
|
|
146
|
+
text: 'To start, about how much volume does your branch fund annually?',
|
|
147
|
+
});
|
|
148
|
+
}, 500);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}, [activeMode, modeStep, addMessage]);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
scrollToBottom();
|
|
156
|
+
}, [messages, showForm, showModePrompts]);
|
|
157
|
+
|
|
158
|
+
const scrollToBottom = () => {
|
|
159
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleModeResponse = (response: string, data?: any) => {
|
|
163
|
+
addMessage({ type: 'user', text: response });
|
|
164
|
+
setShowModePrompts(false);
|
|
165
|
+
|
|
166
|
+
if (data) {
|
|
167
|
+
updateModeData(data);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (activeMode === 'earnings') {
|
|
171
|
+
if (modeStep === 'volume') {
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
addMessage({
|
|
174
|
+
type: 'brady',
|
|
175
|
+
text: 'And roughly how do you get paid today?',
|
|
176
|
+
});
|
|
177
|
+
setModeStep('comp');
|
|
178
|
+
setShowModePrompts(true);
|
|
179
|
+
}, 800);
|
|
180
|
+
} else if (modeStep === 'comp') {
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
addMessage({
|
|
183
|
+
type: 'brady',
|
|
184
|
+
text: "Got it. I'll pull this together so you can see it clearly.",
|
|
185
|
+
});
|
|
186
|
+
setModeStep('calculator');
|
|
187
|
+
setTimeout(() => {
|
|
188
|
+
setCalculatorOpen(true);
|
|
189
|
+
setModeStep('complete');
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
addMessage({
|
|
192
|
+
type: 'brady',
|
|
193
|
+
text: 'Want to sanity-check these assumptions together?',
|
|
194
|
+
});
|
|
195
|
+
setShowModePrompts(true);
|
|
196
|
+
}, 1000);
|
|
197
|
+
}, 800);
|
|
198
|
+
}, 800);
|
|
199
|
+
} else if (modeStep === 'complete') {
|
|
200
|
+
if (response.includes('Talk through')) {
|
|
201
|
+
setShowModePrompts(false);
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
startWorkflow('leadership-call');
|
|
204
|
+
}, 500);
|
|
205
|
+
} else if (response.includes('Adjust')) {
|
|
206
|
+
setShowModePrompts(false);
|
|
207
|
+
setTimeout(() => {
|
|
208
|
+
setCalculatorOpen(true);
|
|
209
|
+
}, 500);
|
|
210
|
+
} else {
|
|
211
|
+
setShowModePrompts(false);
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
addMessage({
|
|
214
|
+
type: 'brady',
|
|
215
|
+
text: "I'm here whenever you need me. What else would you like to explore?",
|
|
216
|
+
});
|
|
217
|
+
}, 800);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} else if (activeMode === 'profit') {
|
|
221
|
+
if (modeStep === 'volume') {
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
addMessage({
|
|
224
|
+
type: 'brady',
|
|
225
|
+
text:
|
|
226
|
+
"For modeling, I'll assume:\n• ~300 bps branch margin\n• ~150 bps gross profit\n" +
|
|
227
|
+
'• ~75 bps operating expenses\n\nWe can adjust all of this.',
|
|
228
|
+
});
|
|
229
|
+
setModeStep('economics');
|
|
230
|
+
setShowModePrompts(true);
|
|
231
|
+
}, 800);
|
|
232
|
+
} else if (modeStep === 'economics') {
|
|
233
|
+
setTimeout(() => {
|
|
234
|
+
addMessage({
|
|
235
|
+
type: 'brady',
|
|
236
|
+
text: "Here's a clean P&L view based on what we discussed.",
|
|
237
|
+
});
|
|
238
|
+
setModeStep('calculator');
|
|
239
|
+
setTimeout(() => {
|
|
240
|
+
setCalculatorOpen(true);
|
|
241
|
+
setModeStep('complete');
|
|
242
|
+
setTimeout(() => {
|
|
243
|
+
addMessage({
|
|
244
|
+
type: 'brady',
|
|
245
|
+
text: "Want help building a realistic pro forma based on AFN's structure?",
|
|
246
|
+
});
|
|
247
|
+
setShowModePrompts(true);
|
|
248
|
+
}, 1000);
|
|
249
|
+
}, 800);
|
|
250
|
+
}, 800);
|
|
251
|
+
} else if (modeStep === 'complete') {
|
|
252
|
+
if (response.includes('Sanity-check') || response.includes('leadership')) {
|
|
253
|
+
setShowModePrompts(false);
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
startWorkflow('leadership-call');
|
|
256
|
+
}, 500);
|
|
257
|
+
} else if (response.includes('Adjust')) {
|
|
258
|
+
setShowModePrompts(false);
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
setCalculatorOpen(true);
|
|
261
|
+
}, 500);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const handleSend = async () => {
|
|
268
|
+
if (inputValue.trim() && !inputDisabled) {
|
|
269
|
+
const userText = inputValue;
|
|
270
|
+
addMessage({ type: 'user', text: userText });
|
|
271
|
+
setInputValue('');
|
|
272
|
+
try {
|
|
273
|
+
const apiResponse = await sendBradyPrompt([{ role: 'user', content: userText }]);
|
|
274
|
+
let mappedType: 'user' | 'brady' | 'form' = 'brady';
|
|
275
|
+
if (apiResponse.role === 'user') mappedType = 'user';
|
|
276
|
+
addMessage({ type: mappedType, text: apiResponse.content });
|
|
277
|
+
} catch {
|
|
278
|
+
addMessage({ type: 'brady', text: 'Sorry, Brady AI is currently unavailable.' });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const handleQuickSuggestion = async (text: string) => {
|
|
284
|
+
addMessage({ type: 'user', text });
|
|
285
|
+
try {
|
|
286
|
+
const apiResponse = await sendBradyPrompt([{ role: 'user', content: text }]);
|
|
287
|
+
|
|
288
|
+
if (Array.isArray(apiResponse.suggestionLink)) {
|
|
289
|
+
setSuggestionLinks(apiResponse.suggestionLink);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let mappedType: 'user' | 'brady' | 'form' = 'brady';
|
|
293
|
+
if (apiResponse.role === 'user') mappedType = 'user';
|
|
294
|
+
addMessage({ type: mappedType, text: apiResponse.content });
|
|
295
|
+
} catch {
|
|
296
|
+
addMessage({ type: 'brady', text: 'Sorry, Brady AI is currently unavailable.' });
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const handleInfoFormSubmit = (data: any) => {
|
|
301
|
+
addMessage({
|
|
302
|
+
type: 'brady',
|
|
303
|
+
text:
|
|
304
|
+
"Got it — thanks.\n\nWe'll email you a personalized overview shortly.\nIf you have questions in the meantime, I'm here to help.",
|
|
305
|
+
});
|
|
306
|
+
setShowForm(false);
|
|
307
|
+
setInputDisabled(false);
|
|
308
|
+
setWorkflow('free');
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const handleLeadershipFormSubmit = (data: any) => {
|
|
312
|
+
addMessage({
|
|
313
|
+
type: 'brady',
|
|
314
|
+
text:
|
|
315
|
+
"Thanks — you're all set.\n\nSomeone from AFN leadership will reach out shortly to connect.\nIn the meantime, feel free to ask me anything.",
|
|
316
|
+
});
|
|
317
|
+
setShowForm(false);
|
|
318
|
+
setInputDisabled(false);
|
|
319
|
+
setWorkflow('free');
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const handlePersonalizedOverviewFormSubmit = (data: any) => {
|
|
323
|
+
addMessage({
|
|
324
|
+
type: 'brady',
|
|
325
|
+
text:
|
|
326
|
+
"Perfect — thanks!\n\nWe'll create a personalized dashboard and earnings calculator based on your information and send it to you shortly.\n\nYou'll be able to see exactly what your business could look like at AFN with specific projections tailored to your situation.",
|
|
327
|
+
});
|
|
328
|
+
setShowForm(false);
|
|
329
|
+
setInputDisabled(false);
|
|
330
|
+
setWorkflow('free');
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const handleFormCancel = () => {
|
|
334
|
+
setShowForm(false);
|
|
335
|
+
setInputDisabled(false);
|
|
336
|
+
resetChat();
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const buildSuggestions = () => {
|
|
340
|
+
if (suggestionLinks && suggestionLinks.length > 0) {
|
|
341
|
+
return suggestionLinks.map((s) => ({
|
|
342
|
+
text: s.text,
|
|
343
|
+
action: () => handleQuickSuggestion(s.prompt),
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
return [
|
|
347
|
+
{
|
|
348
|
+
text: 'How does AFN help LOs grow?',
|
|
349
|
+
action: () => handleQuickSuggestion('How does AFN help LOs grow?'),
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
text: 'How does AFN help branch managers earn more?',
|
|
353
|
+
action: () => handleQuickSuggestion('How does AFN help branch managers earn more?'),
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
text: 'Why do people choose AFN?',
|
|
357
|
+
action: () => handleQuickSuggestion('Why do people choose AFN?'),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
text: 'The AFN Advantage',
|
|
361
|
+
action: () => handleQuickSuggestion('See How AFN Supports Loan Officer Success'),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
text: 'Why AFN?',
|
|
365
|
+
action: () => handleQuickSuggestion('Why AFN?'),
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
text: 'How does AI help me earn more?',
|
|
369
|
+
action: () => handleQuickSuggestion('How does AI help me earn more?'),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
text: 'What makes AFN different?',
|
|
373
|
+
action: () => handleQuickSuggestion('What makes AFN different?'),
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const initialSuggestions = buildSuggestions();
|
|
379
|
+
const postSubmitSuggestions = buildSuggestions();
|
|
380
|
+
|
|
381
|
+
const showPostSubmitSuggestions = messages.length > 2 && !showForm && workflowType === 'free' && activeMode === 'none';
|
|
382
|
+
|
|
383
|
+
const startWorkflow = (workflow: any) => {
|
|
384
|
+
setWorkflow(workflow);
|
|
385
|
+
if (workflow === 'info-request') {
|
|
386
|
+
setShowForm(true);
|
|
387
|
+
setInputDisabled(true);
|
|
388
|
+
} else if (workflow === 'leadership-call') {
|
|
389
|
+
setShowForm(true);
|
|
390
|
+
setInputDisabled(true);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const modeBadge =
|
|
395
|
+
activeMode !== 'none' ? (
|
|
396
|
+
<div
|
|
397
|
+
className={`px-3 py-1 rounded-full text-xs font-medium ${
|
|
398
|
+
activeMode === 'earnings'
|
|
399
|
+
? 'bg-[#8B5CF6]/20 text-[#8B5CF6] border border-[#8B5CF6]/30'
|
|
400
|
+
: 'bg-[#4399D1]/20 text-[#4399D1] border border-[#4399D1]/30'
|
|
401
|
+
}`}
|
|
402
|
+
>
|
|
403
|
+
{activeMode === 'earnings' ? 'Earnings Mode' : 'Profit Mode'}
|
|
404
|
+
</div>
|
|
405
|
+
) : null;
|
|
406
|
+
|
|
407
|
+
if (isHidden) return null;
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<div className="fixed top-0 right-0 w-full md:w-[360px] h-screen flex flex-col bg-gradient-to-br dark:from-zinc-900 dark:to-zinc-800 from-white to-zinc-50 border-l md:border-l dark:border-zinc-700 border-zinc-300 shadow-2xl z-[100] overflow-hidden transition-all duration-300">
|
|
411
|
+
<div className="flex items-center justify-between px-4 py-3 border-b dark:border-zinc-800 border-zinc-200 dark:bg-zinc-950/80 bg-white/95 backdrop-blur-lg">
|
|
412
|
+
<div className="flex items-center gap-3">
|
|
413
|
+
<div className="relative">
|
|
414
|
+
<span className="relative w-10 h-10 block group">
|
|
415
|
+
<span
|
|
416
|
+
className={`absolute inset-0 rounded-full transition-colors duration-300 ${
|
|
417
|
+
bradyHealthy ? 'dark:bg-zinc-700/40 bg-transparent' : 'bg-red-600/80 dark:bg-red-700/80'
|
|
418
|
+
}`}
|
|
419
|
+
></span>
|
|
420
|
+
<ImageWithFallback
|
|
421
|
+
src={avatarSrc}
|
|
422
|
+
alt="Brady AI"
|
|
423
|
+
className="w-10 h-10 relative z-10"
|
|
424
|
+
onClick={() => setIsHidden(true)}
|
|
425
|
+
/>
|
|
426
|
+
</span>
|
|
427
|
+
</div>
|
|
428
|
+
<div className="dark:text-white text-zinc-900">Brady AI</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div className="relative inline-flex items-center h-[34px] w-[150px] dark:bg-zinc-800/50 bg-zinc-200 rounded-full p-1">
|
|
432
|
+
<button
|
|
433
|
+
onClick={() => isModeActive && resetMode()}
|
|
434
|
+
className={`relative z-10 flex-1 h-full rounded-full text-[11px] tracking-wide uppercase font-medium transition-all duration-300 flex items-center justify-center ${
|
|
435
|
+
!isModeActive
|
|
436
|
+
? 'text-white'
|
|
437
|
+
: 'dark:text-zinc-500 text-zinc-600 hover:dark:bg-zinc-700/50 hover:bg-zinc-300/50'
|
|
438
|
+
}`}
|
|
439
|
+
>
|
|
440
|
+
NORMAL
|
|
441
|
+
</button>
|
|
442
|
+
|
|
443
|
+
<div className="relative flex-1">
|
|
444
|
+
{!isModeActive && (
|
|
445
|
+
<div
|
|
446
|
+
className="absolute inset-0 rounded-full animate-pulse"
|
|
447
|
+
style={{
|
|
448
|
+
boxShadow: isBranchManager
|
|
449
|
+
? '0 0 16px rgba(67, 153, 209, 0.6), inset 0 0 10px rgba(67, 153, 209, 0.3)'
|
|
450
|
+
: '0 0 16px rgba(139, 92, 246, 0.6), inset 0 0 10px rgba(139, 92, 246, 0.3)',
|
|
451
|
+
animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
452
|
+
}}
|
|
453
|
+
/>
|
|
454
|
+
)}
|
|
455
|
+
<button
|
|
456
|
+
onClick={() => !isModeActive && handleFocusToggle()}
|
|
457
|
+
className={`relative z-10 w-full h-full rounded-full text-[11px] tracking-wide uppercase font-medium transition-all duration-300 flex items-center justify-center ${
|
|
458
|
+
isModeActive
|
|
459
|
+
? 'text-white'
|
|
460
|
+
: 'dark:text-zinc-500 text-zinc-600 hover:dark:bg-zinc-700/50 hover:bg-zinc-300/50'
|
|
461
|
+
}`}
|
|
462
|
+
>
|
|
463
|
+
{isBranchManager ? 'PROFIT' : 'EARNINGS'}
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<div
|
|
468
|
+
className={`absolute top-1 bottom-1 rounded-full transition-all duration-300 ${
|
|
469
|
+
isModeActive ? 'left-[50%] right-1' : 'left-1 right-[50%]'
|
|
470
|
+
}`}
|
|
471
|
+
style={
|
|
472
|
+
isModeActive
|
|
473
|
+
? {
|
|
474
|
+
background: isBranchManager
|
|
475
|
+
? 'linear-gradient(135deg, #4399D1 0%, #3b82d9 100%)'
|
|
476
|
+
: 'linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%)',
|
|
477
|
+
boxShadow: isBranchManager
|
|
478
|
+
? '0 0 16px rgba(67, 153, 209, 0.15)'
|
|
479
|
+
: '0 0 16px rgba(139, 92, 246, 0.15)',
|
|
480
|
+
}
|
|
481
|
+
: {
|
|
482
|
+
background: 'var(--tw-gradient-stops)',
|
|
483
|
+
backgroundImage: 'linear-gradient(135deg, rgb(82, 82, 91) 0%, rgb(63, 63, 70) 100%)',
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
{modeBadge && (
|
|
491
|
+
<div className="px-4 py-2 border-b dark:border-zinc-800 border-zinc-200">
|
|
492
|
+
{modeBadge}
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
|
|
496
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
497
|
+
<AnimatePresence>
|
|
498
|
+
{messages.map((msg, idx) => (
|
|
499
|
+
<motion.div
|
|
500
|
+
key={idx}
|
|
501
|
+
initial={{ opacity: 0, y: 10 }}
|
|
502
|
+
animate={{ opacity: 1, y: 0 }}
|
|
503
|
+
className={`flex ${msg.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
504
|
+
>
|
|
505
|
+
<div
|
|
506
|
+
className={`px-4 py-3 rounded-2xl whitespace-pre-line ${
|
|
507
|
+
msg.type === 'user'
|
|
508
|
+
? 'bg-[#8B5CF6] text-white rounded-br-sm'
|
|
509
|
+
: 'dark:bg-zinc-800 bg-zinc-100 dark:text-zinc-100 text-zinc-900 rounded-bl-sm'
|
|
510
|
+
}`}
|
|
511
|
+
>
|
|
512
|
+
{msg.text}
|
|
513
|
+
</div>
|
|
514
|
+
</motion.div>
|
|
515
|
+
))}
|
|
516
|
+
</AnimatePresence>
|
|
517
|
+
|
|
518
|
+
{showForm && workflowType === 'info-request' && (
|
|
519
|
+
<InfoRequestForm onSubmit={handleInfoFormSubmit} onCancel={handleFormCancel} />
|
|
520
|
+
)}
|
|
521
|
+
|
|
522
|
+
{showForm && workflowType === 'leadership-call' && (
|
|
523
|
+
<LeadershipCallForm onSubmit={handleLeadershipFormSubmit} onCancel={handleFormCancel} />
|
|
524
|
+
)}
|
|
525
|
+
|
|
526
|
+
{showForm && workflowType === 'personalized-overview-request' && (
|
|
527
|
+
<PersonalizedOverviewForm
|
|
528
|
+
onSubmit={handlePersonalizedOverviewFormSubmit}
|
|
529
|
+
onCancel={handleFormCancel}
|
|
530
|
+
/>
|
|
531
|
+
)}
|
|
532
|
+
|
|
533
|
+
{showModePrompts && activeMode !== 'none' && (
|
|
534
|
+
<ModePromptTree
|
|
535
|
+
mode={activeMode as 'earnings' | 'profit'}
|
|
536
|
+
step={modeStep}
|
|
537
|
+
hasPersonalizedData={hasPersonalizedData}
|
|
538
|
+
onResponse={handleModeResponse}
|
|
539
|
+
/>
|
|
540
|
+
)}
|
|
541
|
+
|
|
542
|
+
{!showForm &&
|
|
543
|
+
!showModePrompts &&
|
|
544
|
+
messages.length === 1 &&
|
|
545
|
+
workflowType === 'free' &&
|
|
546
|
+
activeMode === 'none' && <QuickSuggestions suggestions={initialSuggestions} />}
|
|
547
|
+
|
|
548
|
+
{showPostSubmitSuggestions && !showModePrompts && (
|
|
549
|
+
<QuickSuggestions suggestions={postSubmitSuggestions} />
|
|
550
|
+
)}
|
|
551
|
+
|
|
552
|
+
<div ref={messagesEndRef} />
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<div className="p-4 border-t dark:border-zinc-800 border-zinc-200 dark:bg-zinc-900 bg-zinc-50">
|
|
556
|
+
<div
|
|
557
|
+
className={`flex items-center gap-2 dark:bg-zinc-800 bg-white rounded-full px-4 py-3 border dark:border-zinc-700 border-zinc-300 focus-within:border-[#8B5CF6] transition-colors ${
|
|
558
|
+
inputDisabled || showModePrompts ? 'opacity-50 cursor-not-allowed' : ''
|
|
559
|
+
}`}
|
|
560
|
+
>
|
|
561
|
+
<input
|
|
562
|
+
type="text"
|
|
563
|
+
value={inputValue}
|
|
564
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
565
|
+
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
|
566
|
+
placeholder={
|
|
567
|
+
inputDisabled || showModePrompts ? 'Use buttons above...' : 'Ask me anything...'
|
|
568
|
+
}
|
|
569
|
+
disabled={inputDisabled || showModePrompts}
|
|
570
|
+
className="flex-1 bg-transparent dark:text-white text-zinc-900 dark:placeholder-zinc-500 placeholder-zinc-400 outline-none text-sm disabled:cursor-not-allowed"
|
|
571
|
+
/>
|
|
572
|
+
<button
|
|
573
|
+
className="dark:text-zinc-400 text-zinc-500 dark:hover:text-white hover:text-zinc-900 transition-colors disabled:opacity-50"
|
|
574
|
+
disabled={inputDisabled || showModePrompts}
|
|
575
|
+
>
|
|
576
|
+
<Mic className="w-5 h-5" />
|
|
577
|
+
</button>
|
|
578
|
+
<button
|
|
579
|
+
onClick={handleSend}
|
|
580
|
+
disabled={inputDisabled || showModePrompts}
|
|
581
|
+
className="bg-[#8B5CF6] hover:bg-[#7C3AED] text-white rounded-full p-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
582
|
+
>
|
|
583
|
+
<Send className="w-4 h-4" />
|
|
584
|
+
</button>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const ERROR_IMG_SRC =
|
|
4
|
+
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg==';
|
|
5
|
+
|
|
6
|
+
export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
|
|
7
|
+
const [didError, setDidError] = useState(false);
|
|
8
|
+
|
|
9
|
+
const handleError = () => {
|
|
10
|
+
setDidError(true);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const { src, alt, style, className, ...rest } = props;
|
|
14
|
+
|
|
15
|
+
return didError ? (
|
|
16
|
+
<div
|
|
17
|
+
className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
|
|
18
|
+
style={style}
|
|
19
|
+
>
|
|
20
|
+
<div className="flex items-center justify-center w-full h-full">
|
|
21
|
+
<img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
) : (
|
|
25
|
+
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|