Spaces:
Sleeping
Sleeping
Upload 5 files
Browse filesThe 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;
|