AnjanaNAshokan commited on
Commit
271f538
·
verified ·
1 Parent(s): 66f3298

Upload 5 files

Browse files

The changes includes the fixes for the following issues:
1. User message getting hidden after the Agent response
2. 'Agent:' prefix getting displayed since it was present in the API response. Fix added to remove it if present in the API response.
3. Initiate the first chat API call only when the userProfile is complete (needs rule review again).
4. Default focus to the input field after receiving an API response for chat.

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;