App.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;
screens/ChatInterfaceScreen/ChatInterfaceScreen.tsx ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React 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
+
30
+ const {
31
+ chatHistory,
32
+ isLoading,
33
+ currentMessage,
34
+ setCurrentMessage,
35
+ handleSendMessage,
36
+ showPlanRecs,
37
+ chatEndRef,
38
+ inputRef
39
+ } = useChatInterface(userProfile);
40
+
41
+ return (
42
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 flex flex-col">
43
+ {/* Header */}
44
+ <div className="fixed top-0 left-0 w-full bg-white shadow-sm border-b z-50">
45
+ <div className="max-w-10xl mx-auto px-6 py-4">
46
+ <div className="flex items-center space-x-3">
47
+ <img src={InsuCompassLogo} alt="InsuCompass Logo" className="h-12 w-auto" />
48
+ <div>
49
+ <h1 className="text-2xl font-bold text-gray-900">InsuCompass</h1>
50
+ <p className="text-sm text-gray-600">Chat with your AI insurance advisor</p>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ {/* Chat Messages */}
57
+ <div className="flex-1 overflow-y-auto pt-20">
58
+ <div className="max-w-4xl mx-auto px-6 py-8">
59
+ <div className="space-y-6">
60
+ {chatHistory.map((message, index) => (
61
+ <div
62
+ key={index}
63
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
64
+ >
65
+ <div className={`flex items-start space-x-3 max-w-3xl ${message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''}`}>
66
+ {/* Avatar */}
67
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
68
+ message.role === 'user'
69
+ ? 'bg-blue-500 text-white'
70
+ : 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
71
+ }`}>
72
+ {message.role === 'user' ? (
73
+ <User className="w-5 h-5" />
74
+ ) : (
75
+ <Bot className="w-5 h-5" />
76
+ )}
77
+ </div>
78
+ {/* Message Bubble */}
79
+ <div className={`px-6 py-4 rounded-2xl shadow-sm ${
80
+ message.role === 'user'
81
+ ? 'bg-blue-500 text-white rounded-tr-sm'
82
+ : 'bg-white text-gray-900 rounded-tl-sm border border-gray-200'
83
+ }`}>
84
+ <div className="text-sm font-medium mb-1 opacity-75">
85
+ {message.role === 'user' ? 'You' : 'InsuCompass AI'}
86
+ </div>
87
+ <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">
88
+ {message.content}
89
+ </ReactMarkdown>
90
+ {showPlanRecs && message.plans?.length && (
91
+ <>
92
+ <h2 className="text-2xl font-bold text-gray-900">Recommended Plans for You</h2>
93
+ <div className="grid md:grid-cols-2 gap-6">
94
+ {message.plans?.map((plan, idx) => (
95
+ <PlanCard key={idx} plan={plan} />
96
+ ))}
97
+ </div>
98
+ </>
99
+ )}
100
+ </div>
101
+ </div>
102
+ </div>
103
+ ))}
104
+ {/* Loading indicator */}
105
+ {isLoading && (
106
+ <div className="flex justify-start">
107
+ <div className="flex items-start space-x-3">
108
+ <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">
109
+ <Bot className="w-5 h-5" />
110
+ </div>
111
+ <div className="bg-white px-6 py-4 rounded-2xl rounded-tl-sm border border-gray-200">
112
+ <div className="flex space-x-2">
113
+ <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
114
+ <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
115
+ <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ )}
121
+ <div ref={chatEndRef} />
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ {/* Message Input */}
127
+ <div className="bg-white border-t border-gray-200 p-4">
128
+ <div className="max-w-4xl mx-auto">
129
+ <div className="flex items-center space-x-4">
130
+ <div className="flex-1 relative">
131
+ <input
132
+ ref={inputRef}
133
+ type="text"
134
+ value={currentMessage}
135
+ onChange={(e) => setCurrentMessage(e.target.value)}
136
+ onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
137
+ placeholder="Type your message..."
138
+ 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"
139
+ disabled={isLoading}
140
+ />
141
+ <button
142
+ onClick={handleSendMessage}
143
+ disabled={isLoading || !currentMessage.trim()}
144
+ 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"
145
+ >
146
+ <Send className="w-4 h-4" />
147
+ </button>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ );
154
+ };
155
+
156
+ export default ChatInterfaceScreen;
screens/ChatInterfaceScreen/useChatInterface.ts ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ const inputRef = useRef<HTMLInputElement>(null);
12
+ // threadId is generated once per chat session and persists for the hook's lifetime
13
+ const [threadId] = useState(() => crypto.randomUUID());
14
+
15
+ useEffect(() => {
16
+ chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
17
+ // Focus input after chatHistory changes (i.e., after response is rendered)
18
+ if (inputRef.current) {
19
+ inputRef.current.focus();
20
+ }
21
+ }, [chatHistory]);
22
+
23
+ // Prevent double API call: only send initial chat message once when userProfile is complete
24
+ const hasSentInitialMessage = useRef(false);
25
+ useEffect(() => {
26
+ // TODO: review this logic
27
+ // Check if userProfile is complete before sending initial message
28
+ const isValidProfile = !!userProfile &&
29
+ userProfile?.zip_code?.length && userProfile?.zip_code?.length > 0 &&
30
+ !!userProfile.age && userProfile.age > 0 &&
31
+ !!userProfile.gender && userProfile.gender.length > 0 &&
32
+ !!userProfile.household_size &&
33
+ !!userProfile.income &&
34
+ !!userProfile.employment_status &&
35
+ !!userProfile.citizenship;
36
+ if (isValidProfile && !hasSentInitialMessage.current) {
37
+ sendChatMessage('START_PROFILE_BUILDING', userProfile);
38
+ hasSentInitialMessage.current = true;
39
+ }
40
+ }, [userProfile]);
41
+
42
+ const sendChatMessage = async (
43
+ message: string,
44
+ profileOverride?: UserProfile
45
+ ) => {
46
+ setIsLoading(true);
47
+ try {
48
+ const payload = {
49
+ thread_id: threadId,
50
+ user_profile: profileOverride ?? userProfile,
51
+ message,
52
+ is_profile_complete: isProfileComplete ?? false,
53
+ conversation_history: chatHistory.map(m => `${m.role}: ${m.content}`)
54
+ };
55
+ const response = await fetch(CHAT_ENDPOINT, {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify(payload)
59
+ });
60
+ if (!response.ok) throw new Error('Failed to send message');
61
+ const data: ApiResponse = await response.json();
62
+ // Only append the agent's message to the existing chatHistory
63
+ const agentMsgRaw = data.updated_history[data.updated_history.length - 1] || '';
64
+ const agentMsgContent = agentMsgRaw.replace(/^Agent:\s*/, '');
65
+ const agentMessage: ChatMessage = {
66
+ role: 'agent',
67
+ content: agentMsgContent,
68
+ timestamp: Date.now(),
69
+ plans: !showPlanRecs && data.plan_recommendations?.recommendations?.length && data.plan_recommendations?.recommendations?.length > 0 ? data.plan_recommendations.recommendations : undefined
70
+ };
71
+ setChatHistory(prev => [...prev, agentMessage]);
72
+ if (!showPlanRecs && data.plan_recommendations?.recommendations?.length && data.plan_recommendations?.recommendations?.length > 0) {
73
+ setShowPlanRecs(true);
74
+ }
75
+ return data;
76
+ } catch (error) {
77
+ console.error('Error sending message:', error);
78
+ } finally {
79
+ setIsLoading(false);
80
+ }
81
+ };
82
+
83
+ const handleSendMessage = async () => {
84
+ if (!currentMessage.trim()) return;
85
+ // Add user message to chat immediately
86
+ const userMessage: ChatMessage = {
87
+ role: 'user',
88
+ content: currentMessage,
89
+ timestamp: Date.now()
90
+ };
91
+ setChatHistory(prev => [...prev, userMessage]);
92
+ const messageToSend = currentMessage;
93
+ setCurrentMessage('');
94
+ await sendChatMessage(messageToSend);
95
+ };
96
+
97
+ return {
98
+ chatHistory,
99
+ isLoading,
100
+ currentMessage,
101
+ setCurrentMessage,
102
+ handleSendMessage,
103
+ showPlanRecs,
104
+ chatEndRef,
105
+ inputRef,
106
+ sendChatMessage
107
+ };
108
+ }
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;
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;