src/App.tsx CHANGED
@@ -1,566 +1,24 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
- import { Send, MapPin, User, Users, DollarSign, Briefcase, CheckCircle, Bot } from 'lucide-react';
3
- import InsuCompassLogo from './assets/InsuCompass_Logo.png';
4
- import ReactMarkdown from 'react-markdown';
5
 
6
- // Types
7
- interface UserProfile {
8
- zip_code?: string;
9
- city?: string;
10
- state?: string;
11
- county?: string;
12
- age?: number;
13
- gender?: string;
14
- household_size?: number;
15
- income?: number;
16
- employment_status?: string;
17
- citizenship?: string;
18
- medical_history?: string | null;
19
- medications?: string | null;
20
- special_cases?: string | null;
21
- }
22
-
23
- interface ChatMessage {
24
- role: 'user' | 'agent';
25
- content: string;
26
- timestamp: number;
27
- }
28
-
29
- interface Plan {
30
- plan_name: string;
31
- plan_type: string;
32
- key_features: string[];
33
- estimated_premium: string;
34
- reasoning: string;
35
- }
36
-
37
- interface PlanRecommendations {
38
- recommendations: Plan[];
39
- }
40
-
41
- interface ApiResponse {
42
- updated_profile: UserProfile;
43
- updated_history: string[];
44
- is_profile_complete: boolean;
45
- plan_recommendations?: PlanRecommendations | null;
46
- }
47
-
48
- // Constants
49
- const BACKEND_URL = "https://nagur-shareef-shaik-insucompass-api.hf.space/api";
50
-
51
- /* PlanCard helper */
52
- const PlanCard: React.FC<{ plan: Plan }> = ({ plan }) => (
53
- <div className="bg-white border border-gray-200 rounded-2xl shadow-sm p-6 hover:shadow-lg transition">
54
- <h3 className="text-xl font-semibold text-gray-900">{plan.plan_name}</h3>
55
- <p className="text-sm text-gray-600 mb-2">{plan.plan_type}</p>
56
-
57
- <p className="text-sm italic text-gray-700 mb-4">{plan.reasoning}</p>
58
-
59
- <p className="text-sm font-medium text-gray-800 mb-2">
60
- Estimated Premium: {plan.estimated_premium}
61
- </p>
62
-
63
- <ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
64
- {plan.key_features.map((feat, i) => (
65
- <li key={i}>{feat}</li>
66
- ))}
67
- </ul>
68
- </div>
69
- );
70
-
71
- const InsuCompassApp: React.FC = () => {
72
- const [threadId] = useState(() => crypto.randomUUID());
73
- const [userProfile, setUserProfile] = useState<UserProfile>({});
74
- const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
75
- const [isProfileComplete, setIsProfileComplete] = useState(false);
76
  const [phase, setPhase] = useState<'profiling' | 'chat'>('profiling');
77
- const [isLoading, setIsLoading] = useState(false);
78
- const [currentMessage, setCurrentMessage] = useState('');
79
- const [isValidatingZip, setIsValidatingZip] = useState(false);
80
- const [zipError, setZipError] = useState('');
81
- const [planRecs, setPlanRecs] = useState<Plan[] | null>(null); // NEW
82
- const chatEndRef = useRef<HTMLDivElement>(null);
83
-
84
- // Form state
85
- const [formData, setFormData] = useState({
86
- zip_code: '',
87
- age: '',
88
- gender: '',
89
- household_size: '',
90
- income: '',
91
- employment_status: '',
92
- citizenship: ''
93
- });
94
-
95
- // Auto-scroll to bottom of chat
96
- useEffect(() => {
97
- chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
98
- }, [chatHistory]);
99
-
100
- // API Functions
101
- const getGeodata = async (zipCode: string) => {
102
- setIsValidatingZip(true);
103
- setZipError('');
104
- try {
105
- const response = await fetch(`${BACKEND_URL}/geodata/${zipCode}`);
106
- if (!response.ok) throw new Error('Invalid ZIP code');
107
- const data = await response.json();
108
- setUserProfile(prev => ({ ...prev, ...data }));
109
- setFormData(prev => ({ ...prev, zip_code: zipCode }));
110
- return data;
111
- } catch (error) {
112
- setZipError('Invalid ZIP code or could not retrieve location data');
113
- return null;
114
- } finally {
115
- setIsValidatingZip(false);
116
- }
117
- };
118
-
119
-
120
- const sendChatMessage = async (message: string, profileOverride?: UserProfile) => {
121
- setIsLoading(true);
122
- try {
123
- const payload = {
124
- thread_id: threadId,
125
- user_profile: profileOverride ?? userProfile,
126
- message,
127
- is_profile_complete: isProfileComplete,
128
- conversation_history: chatHistory.map(m => `${m.role}: ${m.content}`)
129
- };
130
-
131
- const response = await fetch(`${BACKEND_URL}/chat`, {
132
- method: 'POST',
133
- headers: { 'Content-Type': 'application/json' },
134
- body: JSON.stringify(payload)
135
- });
136
-
137
- if (!response.ok) throw new Error('Failed to send message');
138
-
139
- const data: ApiResponse = await response.json();
140
-
141
- // Update state from backend response
142
- setUserProfile(data.updated_profile);
143
- setIsProfileComplete(data.is_profile_complete);
144
-
145
- if (data.plan_recommendations?.recommendations && !planRecs) {
146
- setPlanRecs(data.plan_recommendations.recommendations);
147
- }
148
-
149
- // Convert backend history format to our chat format
150
- const newHistory: ChatMessage[] = data.updated_history.map(msg => {
151
- const [role, ...contentParts] = msg.split(':');
152
- return {
153
- role: role.toLowerCase() as 'user' | 'agent',
154
- content: contentParts.join(':').trim(),
155
- timestamp: Date.now()
156
- };
157
- });
158
-
159
- setChatHistory(newHistory);
160
- } catch (error) {
161
- console.error('Error sending message:', error);
162
- } finally {
163
- setIsLoading(false);
164
- }
165
- };
166
-
167
- // Handle ZIP code input
168
- const handleZipChange = async (value: string) => {
169
- const numericValue = value.replace(/\D/g, '').slice(0, 5);
170
- setFormData(prev => ({ ...prev, zip_code: numericValue }));
171
-
172
- if (numericValue.length === 5) {
173
- await getGeodata(numericValue);
174
- }
175
- };
176
-
177
- // Handle form submission
178
- const handleSubmit = async () => {
179
-
180
- // Validate all fields
181
- const requiredFields = ['age', 'gender', 'household_size', 'income', 'employment_status', 'citizenship'];
182
- const missingFields = requiredFields.filter(field => !formData[field as keyof typeof formData]);
183
-
184
- if (missingFields.length > 0) {
185
- alert('Please fill out all fields to continue.');
186
- return;
187
- }
188
-
189
- // Update user profile
190
- // const updatedProfile: UserProfile = {
191
- // ...userProfile,
192
- // age: +formData.age,
193
- // gender: formData.gender,
194
- // household_size: +formData.household_size,
195
- // income: +formData.income,
196
- // employment_status: formData.employment_status,
197
- // citizenship: formData.citizenship
198
- // };
199
 
200
- // Update user profile
201
- const updatedProfile: UserProfile = {
202
- ...userProfile,
203
- age: +formData.age,
204
- gender: formData.gender,
205
- household_size: +formData.household_size,
206
- income: +formData.income,
207
- employment_status: formData.employment_status,
208
- citizenship: formData.citizenship,
209
- medical_history: null,
210
- medications: null,
211
- special_cases: null
212
- };
213
-
214
- setUserProfile(updatedProfile);
215
  setPhase('chat');
216
-
217
- // Start profile building conversation
218
- await sendChatMessage('START_PROFILE_BUILDING', updatedProfile);
219
  };
220
 
221
- // Handle chat message send
222
- const handleSendMessage = async () => {
223
- if (!currentMessage.trim()) return;
224
-
225
- // Add user message to chat immediately
226
- const userMessage: ChatMessage = {
227
- role: 'user',
228
- content: currentMessage,
229
- timestamp: Date.now()
230
- };
231
-
232
- setChatHistory(prev => [...prev, userMessage]);
233
- const messageToSend = currentMessage;
234
- setCurrentMessage('');
235
-
236
- await sendChatMessage(messageToSend);
237
- };
238
-
239
- // Render profile form
240
- const renderProfileForm = () => (
241
- <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
242
- {/* Header */}
243
- <div className="fixed top-0 left-0 w-full bg-white shadow-sm border-b z-50">
244
- <div className="max-w-10xl mx-auto px-6 py-4">
245
- <div className="flex items-center space-x-3">
246
- <img src={InsuCompassLogo} alt="InsuCompass Logo" className="h-12 w-auto" />
247
- <div>
248
- <h1 className="text-2xl font-bold text-gray-900">InsuCompass</h1>
249
- <p className="text-sm text-gray-600">Your AI guide to U.S. Health Insurance</p>
250
- </div>
251
- </div>
252
- </div>
253
- </div>
254
-
255
- {/* Main Content */}
256
- <div className="flex-1 overflow-y-auto pt-20">
257
- <div className="max-w-2xl mx-auto px-6 py-12">
258
- <div className="text-center mb-8">
259
- <h2 className="text-3xl font-bold text-gray-900 mb-4">Let's Get Started</h2>
260
- <p className="text-lg text-gray-600">Tell us about yourself to receive personalized insurance guidance</p>
261
- </div>
262
-
263
- <div className="space-y-6">
264
- {/* ZIP Code Section */}
265
- <div className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100">
266
- <div className="flex items-center space-x-3 mb-4">
267
- <MapPin className="w-5 h-5 text-blue-500" />
268
- <h3 className="text-lg font-semibold text-gray-900">Location</h3>
269
- </div>
270
-
271
- <div className="space-y-4">
272
- <div>
273
- <label className="block text-sm font-medium text-gray-700 mb-2">
274
- ZIP Code
275
- </label>
276
- <input
277
- type="text"
278
- value={formData.zip_code}
279
- onChange={(e) => handleZipChange(e.target.value)}
280
- placeholder="Enter your 5-digit ZIP code"
281
- className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
282
- maxLength={5}
283
- />
284
- {isValidatingZip && (
285
- <p className="text-sm text-blue-600 mt-1">Validating ZIP code...</p>
286
- )}
287
- {zipError && (
288
- <p className="text-sm text-red-600 mt-1">{zipError}</p>
289
- )}
290
- </div>
291
-
292
- {userProfile.city && (
293
- <div className="bg-green-50 border border-green-200 rounded-lg p-4">
294
- <div className="flex items-center space-x-2">
295
- <CheckCircle className="w-5 h-5 text-green-500" />
296
- <span className="font-medium text-green-800">Location Verified</span>
297
- </div>
298
- <p className="text-sm text-green-700 mt-1">
299
- {userProfile.county} County, {userProfile.city}, {userProfile.state}
300
- </p>
301
- </div>
302
- )}
303
- </div>
304
- </div>
305
-
306
- {/* Personal Information */}
307
- {userProfile.city && (
308
- <div className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100">
309
- <div className="flex items-center space-x-3 mb-4">
310
- <User className="w-5 h-5 text-blue-500" />
311
- <h3 className="text-lg font-semibold text-gray-900">Personal Information</h3>
312
- </div>
313
-
314
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
315
- <div>
316
- <label className="block text-sm font-medium text-gray-700 mb-2">Age</label>
317
- <input
318
- type="number"
319
- value={formData.age}
320
- onChange={(e) => setFormData(prev => ({ ...prev, age: e.target.value }))}
321
- placeholder="Enter your age"
322
- min="1"
323
- max="120"
324
- className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
325
- />
326
- </div>
327
-
328
- <div>
329
- <label className="block text-sm font-medium text-gray-700 mb-2">Gender</label>
330
- <select
331
- value={formData.gender}
332
- onChange={(e) => setFormData(prev => ({ ...prev, gender: e.target.value }))}
333
- className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
334
- >
335
- <option value="">Select an option</option>
336
- <option value="Male">Male</option>
337
- <option value="Female">Female</option>
338
- <option value="Non-binary">Non-binary</option>
339
- <option value="Prefer not to say">Prefer not to say</option>
340
- </select>
341
- </div>
342
- </div>
343
- </div>
344
- )}
345
-
346
- {/* Household Information */}
347
- {userProfile.city && (
348
- <div className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100">
349
- <div className="flex items-center space-x-3 mb-4">
350
- <Users className="w-5 h-5 text-blue-500" />
351
- <h3 className="text-lg font-semibold text-gray-900">Household Information</h3>
352
- </div>
353
-
354
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
355
- <div>
356
- <label className="block text-sm font-medium text-gray-700 mb-2">Household Size</label>
357
- <input
358
- type="number"
359
- value={formData.household_size}
360
- onChange={(e) => setFormData(prev => ({ ...prev, household_size: e.target.value }))}
361
- placeholder="Including yourself"
362
- min="1"
363
- className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
364
- />
365
- </div>
366
-
367
- <div>
368
- <label className="block text-sm font-medium text-gray-700 mb-2">Annual Income</label>
369
- <div className="relative">
370
- <DollarSign className="absolute left-3 top-3 w-5 h-5 text-gray-400" />
371
- <input
372
- type="number"
373
- value={formData.income}
374
- onChange={(e) => setFormData(prev => ({ ...prev, income: e.target.value }))}
375
- placeholder="55000"
376
- min="0"
377
- className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
378
- />
379
- </div>
380
- </div>
381
- </div>
382
- </div>
383
- )}
384
-
385
- {/* Employment & Citizenship */}
386
- {userProfile.city && (
387
- <div className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100">
388
- <div className="flex items-center space-x-3 mb-4">
389
- <Briefcase className="w-5 h-5 text-blue-500" />
390
- <h3 className="text-lg font-semibold text-gray-900">Employment & Status</h3>
391
- </div>
392
-
393
- <div className="space-y-4">
394
- <div>
395
- <label className="block text-sm font-medium text-gray-700 mb-2">Employment Status</label>
396
- <select
397
- value={formData.employment_status}
398
- onChange={(e) => setFormData(prev => ({ ...prev, employment_status: e.target.value }))}
399
- className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
400
- >
401
- <option value="">Select an option</option>
402
- <option value="Employed with employer coverage">Employed with employer coverage</option>
403
- <option value="Employed without coverage">Employed without coverage</option>
404
- <option value="Unemployed">Unemployed</option>
405
- <option value="Retired">Retired</option>
406
- <option value="Student">Student</option>
407
- <option value="Self-employed">Self-employed</option>
408
- </select>
409
- </div>
410
-
411
- <div>
412
- <label className="block text-sm font-medium text-gray-700 mb-2">Citizenship Status</label>
413
- <select
414
- value={formData.citizenship}
415
- onChange={(e) => setFormData(prev => ({ ...prev, citizenship: e.target.value }))}
416
- className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
417
- >
418
- <option value="">Select an option</option>
419
- <option value="US Citizen">US Citizen</option>
420
- <option value="Lawful Permanent Resident">Lawful Permanent Resident</option>
421
- <option value="Other legal resident">Other legal resident</option>
422
- <option value="Non-resident">Non-resident</option>
423
- </select>
424
- </div>
425
- </div>
426
- </div>
427
- )}
428
-
429
- {/* Submit Button */}
430
- {userProfile.city && (
431
- <button
432
- onClick={handleSubmit}
433
- disabled={isLoading}
434
- className="w-full bg-gradient-to-r from-blue-500 to-indigo-600 text-white py-4 px-8 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transform hover:scale-[1.02] transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
435
- >
436
- {isLoading ? 'Starting Session...' : 'Start My Personalized Session'}
437
- </button>
438
- )}
439
- </div>
440
- </div>
441
- </div>
442
- </div>
443
- );
444
-
445
- // Render chat interface
446
- const renderChatInterface = () => (
447
- <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 flex flex-col">
448
- {/* Header */}
449
- <div className="fixed top-0 left-0 w-full bg-white shadow-sm border-b z-50">
450
- <div className="max-w-10xl mx-auto px-6 py-4">
451
- <div className="flex items-center space-x-3">
452
- <img src={InsuCompassLogo} alt="InsuCompass Logo" className="h-12 w-auto" />
453
- <div>
454
- <h1 className="text-2xl font-bold text-gray-900">InsuCompass</h1>
455
- <p className="text-sm text-gray-600">Chat with your AI insurance advisor</p>
456
- </div>
457
- </div>
458
- </div>
459
- </div>
460
-
461
- {/* Chat Messages */}
462
- <div className="flex-1 overflow-y-auto pt-20">
463
- <div className="max-w-4xl mx-auto px-6 py-8">
464
- <div className="space-y-6">
465
- {planRecs && (
466
- <>
467
- <h2 className="text-2xl font-bold text-gray-900">Recommended Plans for You</h2>
468
- <div className="grid md:grid-cols-2 gap-6">
469
- {planRecs.map((plan, idx) => (
470
- <PlanCard key={idx} plan={plan} />
471
- ))}
472
- </div>
473
- </>
474
- )}
475
- {chatHistory.map((message, index) => (
476
- <div
477
- key={index}
478
- className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
479
- >
480
- <div className={`flex items-start space-x-3 max-w-3xl ${message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''}`}>
481
- {/* Avatar */}
482
- <div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
483
- message.role === 'user'
484
- ? 'bg-blue-500 text-white'
485
- : 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
486
- }`}>
487
- {message.role === 'user' ? (
488
- <User className="w-5 h-5" />
489
- ) : (
490
- <Bot className="w-5 h-5" />
491
- )}
492
- </div>
493
-
494
- {/* Message Bubble */}
495
- <div className={`px-6 py-4 rounded-2xl shadow-sm ${
496
- message.role === 'user'
497
- ? 'bg-blue-500 text-white rounded-tr-sm'
498
- : 'bg-white text-gray-900 rounded-tl-sm border border-gray-200'
499
- }`}>
500
- <div className="text-sm font-medium mb-1 opacity-75">
501
- {message.role === 'user' ? 'You' : 'InsuCompass AI'}
502
- </div>
503
- {/* <div className="whitespace-pre-wrap">{message.content}</div> */}
504
- <ReactMarkdown className="prose prose-sm max-w-none prose-table:border prose-table:border-gray-300 prose-th:bg-gray-100 prose-th:p-2 prose-td:p-2 prose-td:border prose-td:border-gray-300">
505
- {message.content}
506
- </ReactMarkdown>
507
- </div>
508
- </div>
509
- </div>
510
- ))}
511
-
512
- {/* Loading indicator */}
513
- {isLoading && (
514
- <div className="flex justify-start">
515
- <div className="flex items-start space-x-3">
516
- <div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 text-white flex items-center justify-center">
517
- <Bot className="w-5 h-5" />
518
- </div>
519
- <div className="bg-white px-6 py-4 rounded-2xl rounded-tl-sm border border-gray-200">
520
- <div className="flex space-x-2">
521
- <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
522
- <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
523
- <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
524
- </div>
525
- </div>
526
- </div>
527
- </div>
528
- )}
529
-
530
- <div ref={chatEndRef} />
531
- </div>
532
- </div>
533
- </div>
534
-
535
- {/* Message Input */}
536
- <div className="bg-white border-t border-gray-200 p-4">
537
- <div className="max-w-4xl mx-auto">
538
- <div className="flex items-center space-x-4">
539
- <div className="flex-1 relative">
540
- <input
541
- type="text"
542
- value={currentMessage}
543
- onChange={(e) => setCurrentMessage(e.target.value)}
544
- onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
545
- placeholder="Type your message..."
546
- className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
547
- disabled={isLoading}
548
- />
549
- <button
550
- onClick={handleSendMessage}
551
- disabled={isLoading || !currentMessage.trim()}
552
- className="absolute right-2 top-1/2 transform -translate-y-1/2 p-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
553
- >
554
- <Send className="w-4 h-4" />
555
- </button>
556
- </div>
557
- </div>
558
- </div>
559
- </div>
560
- </div>
561
  );
562
-
563
- return phase === 'profiling' ? renderProfileForm() : renderChatInterface();
564
  };
565
 
566
- export default InsuCompassApp;
 
1
+ import React, { useState } from 'react';
2
+ import ProfilingScreen from './screens/ProfileScreen/ProfilingScreen';
3
+ import ChatInterfaceScreen from './screens/ChatInterfaceScreen/ChatInterfaceScreen';
4
+ import { UserProfile } from './interface';
5
 
6
+ const App: React.FC = () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  const [phase, setPhase] = useState<'profiling' | 'chat'>('profiling');
8
+ const [userProfile, setUserProfile] = useState<UserProfile>({});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ // Handler to move from profiling to chat, passing userProfile if needed
11
+ const handleStartChat = (profile: UserProfile) => {
12
+ console.log('Starting chat with profile:', profile);
13
+ setUserProfile(profile);
 
 
 
 
 
 
 
 
 
 
 
14
  setPhase('chat');
 
 
 
15
  };
16
 
17
+ return phase === 'profiling' ? (
18
+ <ProfilingScreen onComplete={handleStartChat} />
19
+ ) : (
20
+ <ChatInterfaceScreen userProfile={userProfile} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  );
 
 
22
  };
23
 
24
+ export default App;
src/components/EmploymentSection.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Briefcase } from 'lucide-react';
3
+
4
+ interface Props {
5
+ formData: { employment_status: string; citizenship: string };
6
+ onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
7
+ }
8
+
9
+ const EmploymentSection: React.FC<Props> = ({ formData, onChange }) => (
10
+ <div className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100">
11
+ <div className="flex items-center space-x-3 mb-4">
12
+ <Briefcase className="w-5 h-5 text-blue-500" />
13
+ <h3 className="text-lg font-semibold text-gray-900">Employment & Status</h3>
14
+ </div>
15
+ <div className="space-y-4">
16
+ <div>
17
+ <label className="block text-sm font-medium text-gray-700 mb-2">Employment Status</label>
18
+ <select
19
+ name="employment_status"
20
+ value={formData.employment_status}
21
+ onChange={onChange}
22
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
23
+ >
24
+ <option value="">Select an option</option>
25
+ <option value="Employed with employer coverage">Employed with employer coverage</option>
26
+ <option value="Employed without coverage">Employed without coverage</option>
27
+ <option value="Unemployed">Unemployed</option>
28
+ <option value="Retired">Retired</option>
29
+ <option value="Student">Student</option>
30
+ <option value="Self-employed">Self-employed</option>
31
+ </select>
32
+ </div>
33
+ <div>
34
+ <label className="block text-sm font-medium text-gray-700 mb-2">Citizenship Status</label>
35
+ <select
36
+ name="citizenship"
37
+ value={formData.citizenship}
38
+ onChange={onChange}
39
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
40
+ >
41
+ <option value="">Select an option</option>
42
+ <option value="US Citizen">US Citizen</option>
43
+ <option value="Lawful Permanent Resident">Lawful Permanent Resident</option>
44
+ <option value="Other legal resident">Other legal resident</option>
45
+ <option value="Non-resident">Non-resident</option>
46
+ </select>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ );
51
+
52
+ export default EmploymentSection;
src/components/HouseholdInfoSection.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Users, DollarSign } from 'lucide-react';
3
+
4
+ interface Props {
5
+ formData: { household_size: string; income: string };
6
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
7
+ }
8
+
9
+ const HouseholdInfoSection: React.FC<Props> = ({ formData, onChange }) => (
10
+ <div className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100">
11
+ <div className="flex items-center space-x-3 mb-4">
12
+ <Users className="w-5 h-5 text-blue-500" />
13
+ <h3 className="text-lg font-semibold text-gray-900">Household Information</h3>
14
+ </div>
15
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
16
+ <div>
17
+ <label className="block text-sm font-medium text-gray-700 mb-2">Household Size</label>
18
+ <input
19
+ type="number"
20
+ name="household_size"
21
+ value={formData.household_size}
22
+ onChange={onChange}
23
+ placeholder="Including yourself"
24
+ min="1"
25
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
26
+ />
27
+ </div>
28
+ <div>
29
+ <label className="block text-sm font-medium text-gray-700 mb-2">Annual Income</label>
30
+ <div className="relative">
31
+ <DollarSign className="absolute left-3 top-3 w-5 h-5 text-gray-400" />
32
+ <input
33
+ type="number"
34
+ name="income"
35
+ value={formData.income}
36
+ onChange={onChange}
37
+ placeholder="55000"
38
+ min="0"
39
+ className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
40
+ />
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ );
46
+
47
+ export default HouseholdInfoSection;
src/components/LocationSection.tsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { MapPin, CheckCircle } from 'lucide-react';
3
+
4
+ interface Props {
5
+ formData: { zip_code: string };
6
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
7
+ isValidatingZip: boolean;
8
+ zipError: string;
9
+ userProfile: { city?: string; county?: string; state?: string };
10
+ }
11
+
12
+ const LocationSection: React.FC<Props> = ({ formData, onChange, isValidatingZip, zipError, userProfile }) => (
13
+ <div className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100">
14
+ <div className="flex items-center space-x-3 mb-4">
15
+ <MapPin className="w-5 h-5 text-blue-500" />
16
+ <h3 className="text-lg font-semibold text-gray-900">Location</h3>
17
+ </div>
18
+ <div className="space-y-4">
19
+ <div>
20
+ <label className="block text-sm font-medium text-gray-700 mb-2">
21
+ ZIP Code
22
+ </label>
23
+ <input
24
+ type="text"
25
+ name="zip_code"
26
+ value={formData.zip_code}
27
+ onChange={onChange}
28
+ placeholder="Enter your 5-digit ZIP code"
29
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
30
+ maxLength={5}
31
+ />
32
+ {isValidatingZip && (
33
+ <p className="text-sm text-blue-600 mt-1">Validating ZIP code...</p>
34
+ )}
35
+ {zipError && (
36
+ <p className="text-sm text-red-600 mt-1">{zipError}</p>
37
+ )}
38
+ </div>
39
+ {userProfile.city && (
40
+ <div className="bg-green-50 border border-green-200 rounded-lg p-4">
41
+ <div className="flex items-center space-x-2">
42
+ <CheckCircle className="w-5 h-5 text-green-500" />
43
+ <span className="font-medium text-green-800">Location Verified</span>
44
+ </div>
45
+ <p className="text-sm text-green-700 mt-1">
46
+ {userProfile.county} County, {userProfile.city}, {userProfile.state}
47
+ </p>
48
+ </div>
49
+ )}
50
+ </div>
51
+ </div>
52
+ );
53
+
54
+ export default LocationSection;
src/components/PersonalInfoSection.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { User } from 'lucide-react';
3
+
4
+ interface Props {
5
+ formData: { age: string; gender: string };
6
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
7
+ }
8
+
9
+ const PersonalInfoSection: React.FC<Props> = ({ formData, onChange }) => (
10
+ <div className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100">
11
+ <div className="flex items-center space-x-3 mb-4">
12
+ <User className="w-5 h-5 text-blue-500" />
13
+ <h3 className="text-lg font-semibold text-gray-900">Personal Information</h3>
14
+ </div>
15
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
16
+ <div>
17
+ <label className="block text-sm font-medium text-gray-700 mb-2">Age</label>
18
+ <input
19
+ type="number"
20
+ name="age"
21
+ value={formData.age}
22
+ onChange={onChange}
23
+ placeholder="Enter your age"
24
+ min="1"
25
+ max="120"
26
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
27
+ />
28
+ </div>
29
+ <div>
30
+ <label className="block text-sm font-medium text-gray-700 mb-2">Gender</label>
31
+ <select
32
+ name="gender"
33
+ value={formData.gender}
34
+ onChange={onChange}
35
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
36
+ >
37
+ <option value="">Select an option</option>
38
+ <option value="Male">Male</option>
39
+ <option value="Female">Female</option>
40
+ <option value="Non-binary">Non-binary</option>
41
+ <option value="Prefer not to say">Prefer not to say</option>
42
+ </select>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ );
47
+
48
+ export default PersonalInfoSection;
src/endpoint.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ const BACKEND_URL = "https://nagur-shareef-shaik-insucompass-api.hf.space/api";
2
+ export const CHAT_ENDPOINT = `${BACKEND_URL}/chat`;
3
+ export const GEODATA_ENDPOINT = `${BACKEND_URL}/geodata`;
src/interface.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ApiResponse {
2
+ updated_profile: UserProfile;
3
+ updated_history: string[];
4
+ is_profile_complete: boolean;
5
+ plan_recommendations?: PlanRecommendations | null;
6
+ }
7
+
8
+ export interface Plan {
9
+ plan_name: string;
10
+ plan_type: string;
11
+ key_features: string[];
12
+ estimated_premium: string;
13
+ reasoning: string;
14
+ }
15
+
16
+ export interface UserProfile {
17
+ zip_code?: string;
18
+ city?: string;
19
+ state?: string;
20
+ county?: string;
21
+ age?: number;
22
+ gender?: string;
23
+ household_size?: number;
24
+ income?: number;
25
+ employment_status?: string;
26
+ citizenship?: string;
27
+ medical_history?: string | null;
28
+ medications?: string | null;
29
+ special_cases?: string | null;
30
+ }
31
+
32
+ export interface PlanRecommendations {
33
+ recommendations: Plan[];
34
+ }
35
+
36
+ export interface ChatMessage {
37
+ role: 'user' | 'agent';
38
+ content: string;
39
+ timestamp: number;
40
+ plans?: Plan[];
41
+ }
src/screens/ChatInterfaceScreen/ChatInterfaceScreen.tsx ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from 'react';
2
+ import { Send, User, Bot } from 'lucide-react';
3
+ import InsuCompassLogo from '../../assets/InsuCompass_Logo.png';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import { useChatInterface } from './useChatInterface';
6
+ import { Plan, UserProfile } from '../../interface';
7
+
8
+ const PlanCard: React.FC<{ plan: Plan }> = ({ plan }) => (
9
+ <div className="bg-white border border-gray-200 rounded-2xl shadow-sm p-6 hover:shadow-lg transition">
10
+ <h3 className="text-xl font-semibold text-gray-900">{plan.plan_name}</h3>
11
+ <p className="text-sm text-gray-600 mb-2">{plan.plan_type}</p>
12
+ <p className="text-sm italic text-gray-700 mb-4">{plan.reasoning}</p>
13
+ <p className="text-sm font-medium text-gray-800 mb-2">
14
+ Estimated Premium: {plan.estimated_premium}
15
+ </p>
16
+ <ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
17
+ {plan.key_features.map((feat, i) => (
18
+ <li key={i}>{feat}</li>
19
+ ))}
20
+ </ul>
21
+ </div>
22
+ );
23
+
24
+ interface ChatInterfaceScreenProps {
25
+ userProfile: UserProfile;
26
+ }
27
+
28
+ const ChatInterfaceScreen: React.FC<ChatInterfaceScreenProps> = ({ userProfile }) => {
29
+ const {
30
+ chatHistory,
31
+ isLoading,
32
+ currentMessage,
33
+ setCurrentMessage,
34
+ handleSendMessage,
35
+ showPlanRecs,
36
+ chatEndRef,
37
+ sendChatMessage
38
+ } = useChatInterface(userProfile);
39
+
40
+ useEffect(() => {
41
+ if (chatHistory.length === 0) {
42
+ sendChatMessage('START_PROFILE_BUILDING', userProfile);
43
+ }
44
+ }, [userProfile])
45
+ return (
46
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 flex flex-col">
47
+ {/* Header */}
48
+ <div className="fixed top-0 left-0 w-full bg-white shadow-sm border-b z-50">
49
+ <div className="max-w-10xl mx-auto px-6 py-4">
50
+ <div className="flex items-center space-x-3">
51
+ <img src={InsuCompassLogo} alt="InsuCompass Logo" className="h-12 w-auto" />
52
+ <div>
53
+ <h1 className="text-2xl font-bold text-gray-900">InsuCompass</h1>
54
+ <p className="text-sm text-gray-600">Chat with your AI insurance advisor</p>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ {/* Chat Messages */}
61
+ <div className="flex-1 overflow-y-auto pt-20">
62
+ <div className="max-w-4xl mx-auto px-6 py-8">
63
+ <div className="space-y-6">
64
+ {chatHistory.map((message, index) => (
65
+ <div
66
+ key={index}
67
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
68
+ >
69
+ <div className={`flex items-start space-x-3 max-w-3xl ${message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''}`}>
70
+ {/* Avatar */}
71
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
72
+ message.role === 'user'
73
+ ? 'bg-blue-500 text-white'
74
+ : 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
75
+ }`}>
76
+ {message.role === 'user' ? (
77
+ <User className="w-5 h-5" />
78
+ ) : (
79
+ <Bot className="w-5 h-5" />
80
+ )}
81
+ </div>
82
+ {/* Message Bubble */}
83
+ <div className={`px-6 py-4 rounded-2xl shadow-sm ${
84
+ message.role === 'user'
85
+ ? 'bg-blue-500 text-white rounded-tr-sm'
86
+ : 'bg-white text-gray-900 rounded-tl-sm border border-gray-200'
87
+ }`}>
88
+ <div className="text-sm font-medium mb-1 opacity-75">
89
+ {message.role === 'user' ? 'You' : 'InsuCompass AI'}
90
+ </div>
91
+ <ReactMarkdown className="prose prose-sm max-w-none prose-table:border prose-table:border-gray-300 prose-th:bg-gray-100 prose-th:p-2 prose-td:p-2 prose-td:border prose-td:border-gray-300">
92
+ {message.content}
93
+ </ReactMarkdown>
94
+ {showPlanRecs && message.plans?.length && (
95
+ <>
96
+ <h2 className="text-2xl font-bold text-gray-900">Recommended Plans for You</h2>
97
+ <div className="grid md:grid-cols-2 gap-6">
98
+ {message.plans?.map((plan, idx) => (
99
+ <PlanCard key={idx} plan={plan} />
100
+ ))}
101
+ </div>
102
+ </>
103
+ )}
104
+ </div>
105
+ </div>
106
+ </div>
107
+ ))}
108
+ {/* Loading indicator */}
109
+ {isLoading && (
110
+ <div className="flex justify-start">
111
+ <div className="flex items-start space-x-3">
112
+ <div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 text-white flex items-center justify-center">
113
+ <Bot className="w-5 h-5" />
114
+ </div>
115
+ <div className="bg-white px-6 py-4 rounded-2xl rounded-tl-sm border border-gray-200">
116
+ <div className="flex space-x-2">
117
+ <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
118
+ <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
119
+ <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ )}
125
+ <div ref={chatEndRef} />
126
+ </div>
127
+ </div>
128
+ </div>
129
+
130
+ {/* Message Input */}
131
+ <div className="bg-white border-t border-gray-200 p-4">
132
+ <div className="max-w-4xl mx-auto">
133
+ <div className="flex items-center space-x-4">
134
+ <div className="flex-1 relative">
135
+ <input
136
+ type="text"
137
+ value={currentMessage}
138
+ onChange={(e) => setCurrentMessage(e.target.value)}
139
+ onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
140
+ placeholder="Type your message..."
141
+ className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
142
+ disabled={isLoading}
143
+ />
144
+ <button
145
+ onClick={handleSendMessage}
146
+ disabled={isLoading || !currentMessage.trim()}
147
+ className="absolute right-2 top-1/2 transform -translate-y-1/2 p-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
148
+ >
149
+ <Send className="w-4 h-4" />
150
+ </button>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ );
157
+ };
158
+
159
+ export default ChatInterfaceScreen;
src/screens/ChatInterfaceScreen/useChatInterface.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { ApiResponse, ChatMessage, UserProfile } from '../../interface';
3
+ import { CHAT_ENDPOINT } from '../../endpoint';
4
+
5
+ export function useChatInterface(userProfile: UserProfile, isProfileComplete?: boolean) {
6
+ const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
7
+ const [isLoading, setIsLoading] = useState(false);
8
+ const [currentMessage, setCurrentMessage] = useState('');
9
+ const [showPlanRecs, setShowPlanRecs] = useState<boolean>(false);
10
+ const chatEndRef = useRef<HTMLDivElement>(null);
11
+ // threadId is generated once per chat session and persists for the hook's lifetime
12
+ const [threadId] = useState(() => crypto.randomUUID());
13
+
14
+ useEffect(() => {
15
+ chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
16
+ }, [chatHistory]);
17
+
18
+ const sendChatMessage = async (
19
+ message: string,
20
+ profileOverride?: UserProfile
21
+ ) => {
22
+ setIsLoading(true);
23
+ try {
24
+ const payload = {
25
+ thread_id: threadId,
26
+ user_profile: profileOverride ?? userProfile,
27
+ message,
28
+ is_profile_complete: isProfileComplete ?? false,
29
+ conversation_history: chatHistory.map(m => `${m.role}: ${m.content}`)
30
+ };
31
+ const response = await fetch(CHAT_ENDPOINT, {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify(payload)
35
+ });
36
+ if (!response.ok) throw new Error('Failed to send message');
37
+ const data: ApiResponse = await response.json();
38
+ // Update state from backend response
39
+ // let newHistory: ChatMessage[] = data.updated_history.map(msg => {
40
+ // const [role, ...contentParts] = msg.split(':');
41
+ // return {
42
+ // role: role.toLowerCase() as 'user' | 'agent',
43
+ // content: contentParts.join(':').trim(),
44
+ // timestamp: Date.now()
45
+ // };
46
+ // });
47
+ let newHistory: ChatMessage[] = [...chatHistory, {
48
+ role: 'agent',
49
+ content: data.updated_history[data.updated_history.length - 1],
50
+ timestamp: Date.now(),
51
+ plans: !showPlanRecs && data.plan_recommendations?.recommendations?.length && data.plan_recommendations?.recommendations?.length > 0 ? data.plan_recommendations.recommendations : undefined
52
+ }];
53
+ setChatHistory(newHistory);
54
+ if (!showPlanRecs && data.plan_recommendations?.recommendations?.length && data.plan_recommendations?.recommendations?.length > 0) {
55
+ setShowPlanRecs(true);
56
+ }
57
+ return data;
58
+ } catch (error) {
59
+ console.error('Error sending message:', error);
60
+ } finally {
61
+ setIsLoading(false);
62
+ }
63
+ };
64
+
65
+ const handleSendMessage = async () => {
66
+ if (!currentMessage.trim()) return;
67
+ // Add user message to chat immediately
68
+ const userMessage: ChatMessage = {
69
+ role: 'user',
70
+ content: currentMessage,
71
+ timestamp: Date.now()
72
+ };
73
+ setChatHistory(prev => [...prev, userMessage]);
74
+ const messageToSend = currentMessage;
75
+ setCurrentMessage('');
76
+ await sendChatMessage(messageToSend);
77
+ };
78
+
79
+ return {
80
+ chatHistory,
81
+ isLoading,
82
+ currentMessage,
83
+ setCurrentMessage,
84
+ handleSendMessage,
85
+ showPlanRecs,
86
+ chatEndRef,
87
+ sendChatMessage
88
+ };
89
+ }
src/screens/ProfileScreen/ProfilingScreen.tsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import InsuCompassLogo from '../../assets/InsuCompass_Logo.png';
3
+ import LocationSection from '../../components/LocationSection';
4
+ import PersonalInfoSection from '../../components/PersonalInfoSection';
5
+ import HouseholdInfoSection from '../../components/HouseholdInfoSection';
6
+ import EmploymentSection from '../../components/EmploymentSection';
7
+ import { UserProfile } from '../../interface';
8
+ import useProfileScreen from './useProfileScreen';
9
+
10
+ interface ProfilingScreenProps {
11
+ onComplete: (userProfile: UserProfile) => void;
12
+ }
13
+
14
+ const ProfilingScreen: React.FC<ProfilingScreenProps> = ({ onComplete }) => {
15
+ const {
16
+ formData,
17
+ userProfile,
18
+ isValidatingZip,
19
+ zipError,
20
+ isLoading,
21
+ handleZipChange,
22
+ handleFormChange,
23
+ handleSubmit,
24
+ } = useProfileScreen({
25
+ onComplete,
26
+ });
27
+
28
+ return (
29
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
30
+ {/* Header */}
31
+ <div className="fixed top-0 left-0 w-full bg-white shadow-sm border-b z-50">
32
+ <div className="max-w-10xl mx-auto px-6 py-4">
33
+ <div className="flex items-center space-x-3">
34
+ <img src={InsuCompassLogo} alt="InsuCompass Logo" className="h-12 w-auto" />
35
+ <div>
36
+ <h1 className="text-2xl font-bold text-gray-900">InsuCompass</h1>
37
+ <p className="text-sm text-gray-600">Your AI guide to U.S. Health Insurance</p>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ {/* Main Content */}
44
+ <div className="flex-1 overflow-y-auto pt-20">
45
+ <div className="max-w-2xl mx-auto px-6 py-12">
46
+ <div className="text-center mb-8">
47
+ <h2 className="text-3xl font-bold text-gray-900 mb-4">Let's Get Started</h2>
48
+ <p className="text-lg text-gray-600">Tell us about yourself to receive personalized insurance guidance</p>
49
+ </div>
50
+ <div className="space-y-6">
51
+ {/* ZIP Code Section */}
52
+ <LocationSection
53
+ formData={formData}
54
+ onChange={handleZipChange}
55
+ isValidatingZip={isValidatingZip}
56
+ zipError={zipError}
57
+ userProfile={userProfile}
58
+ />
59
+ {/* Personal Information */}
60
+ {userProfile.city && (
61
+ <PersonalInfoSection formData={formData} onChange={handleFormChange} />
62
+ )}
63
+ {/* Household Information */}
64
+ {userProfile.city && (
65
+ <HouseholdInfoSection formData={formData} onChange={handleFormChange} />
66
+ )}
67
+ {/* Employment & Citizenship */}
68
+ {userProfile.city && (
69
+ <EmploymentSection formData={formData} onChange={handleFormChange} />
70
+ )}
71
+ {/* Submit Button */}
72
+ {userProfile.city && (
73
+ <button
74
+ onClick={handleSubmit}
75
+ disabled={isLoading}
76
+ className="w-full bg-gradient-to-r from-blue-500 to-indigo-600 text-white py-4 px-8 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transform hover:scale-[1.02] transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
77
+ >
78
+ {isLoading ? 'Starting Session...' : 'Start My Personalized Session'}
79
+ </button>
80
+ )}
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ );
86
+ };
87
+
88
+ export default ProfilingScreen;
src/screens/ProfileScreen/useProfileScreen.ts ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { UserProfile } from '../../interface';
3
+ import { GEODATA_ENDPOINT } from '../../endpoint';
4
+
5
+ interface UseProfileScreenOptions {
6
+ onComplete: (userProfile: UserProfile) => void;
7
+ }
8
+
9
+ const useProfileScreen = ({ onComplete }: UseProfileScreenOptions) => {
10
+ const [formData, setFormData] = useState({
11
+ zip_code: '',
12
+ age: '',
13
+ gender: '',
14
+ household_size: '',
15
+ income: '',
16
+ employment_status: '',
17
+ citizenship: ''
18
+ });
19
+
20
+ const [userProfile, setUserProfile] = useState<UserProfile>({});
21
+ const [isValidatingZip, setIsValidatingZip] = useState(false);
22
+ const [zipError, setZipError] = useState('');
23
+ const [isLoading, setIsLoading] = useState(false);
24
+
25
+ // Fetch geodata for ZIP code
26
+ const getGeodata = async (zipCode: string) => {
27
+ setIsValidatingZip(true);
28
+ setZipError('');
29
+ try {
30
+ const response = await fetch(`${GEODATA_ENDPOINT}/${zipCode}`);
31
+ if (!response.ok) throw new Error('Invalid ZIP code');
32
+ const data = await response.json();
33
+ setUserProfile(prev => ({ ...prev, ...data }));
34
+ setFormData(prev => ({ ...prev, zip_code: zipCode }));
35
+ return data;
36
+ } catch (error) {
37
+ setZipError('Invalid ZIP code or could not retrieve location data');
38
+ return null;
39
+ } finally {
40
+ setIsValidatingZip(false);
41
+ }
42
+ };
43
+
44
+ // Handle ZIP code input
45
+ const handleZipChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
46
+ const value = e.target.value;
47
+ const numericValue = value.replace(/\D/g, '').slice(0, 5);
48
+ setFormData(prev => ({ ...prev, zip_code: numericValue }));
49
+ if (numericValue.length === 5) {
50
+ await getGeodata(numericValue);
51
+ }
52
+ };
53
+
54
+ // Handle other form input changes
55
+ const handleFormChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
56
+ const { name, value } = e.target;
57
+ setFormData(prev => ({ ...prev, [name]: value }));
58
+ };
59
+
60
+ // Handle form submission
61
+ const handleSubmit = async () => {
62
+ // Validate all fields
63
+ const requiredFields = ['age', 'gender', 'household_size', 'income', 'employment_status', 'citizenship'];
64
+ const missingFields = requiredFields.filter(field => !formData[field as keyof typeof formData]);
65
+ if (missingFields.length > 0) {
66
+ alert('Please fill out all fields to continue.');
67
+ return;
68
+ }
69
+
70
+ console.log('Submitting profile data:', formData);
71
+ setIsLoading(true);
72
+
73
+ // Build the final user profile
74
+ const updatedProfile: UserProfile = {
75
+ ...userProfile,
76
+ age: +formData.age,
77
+ gender: formData.gender,
78
+ household_size: +formData.household_size,
79
+ income: +formData.income,
80
+ employment_status: formData.employment_status,
81
+ citizenship: formData.citizenship,
82
+ medical_history: null,
83
+ medications: null,
84
+ special_cases: null
85
+ };
86
+
87
+ setUserProfile(updatedProfile);
88
+ setIsLoading(false);
89
+
90
+ // Notify parent (App) to proceed to chat phase
91
+ onComplete(updatedProfile);
92
+ };
93
+
94
+ return {
95
+ formData,
96
+ userProfile,
97
+ isValidatingZip,
98
+ zipError,
99
+ isLoading,
100
+ handleZipChange,
101
+ handleFormChange,
102
+ handleSubmit,
103
+ };
104
+ };
105
+
106
+ export default useProfileScreen;