Spaces:
Sleeping
Sleeping
“vinit5112”
commited on
Commit
·
6f1f94e
1
Parent(s):
45f1618
Upgrade UI
Browse files- frontend/src/App.js +137 -31
- frontend/src/components/ChatInterface.js +62 -91
- frontend/src/components/FileUploader.js +232 -121
- frontend/src/components/MessageBubble.js +257 -108
- frontend/src/components/Sidebar.js +239 -164
frontend/src/App.js
CHANGED
@@ -3,7 +3,7 @@ import { Toaster } from 'react-hot-toast';
|
|
3 |
import ChatInterface from './components/ChatInterface';
|
4 |
import Sidebar from './components/Sidebar';
|
5 |
import WelcomeScreen from './components/WelcomeScreen';
|
6 |
-
import { SunIcon, MoonIcon, HomeIcon } from '@heroicons/react/24/outline';
|
7 |
import ConversationStorage from './utils/conversationStorage';
|
8 |
|
9 |
function App() {
|
@@ -104,66 +104,104 @@ function App() {
|
|
104 |
? 'bg-gray-900 text-white'
|
105 |
: 'bg-gray-50 text-gray-900'
|
106 |
}`}>
|
107 |
-
{/* Header */}
|
108 |
<header className={`fixed top-0 left-0 right-0 z-50 ${
|
109 |
darkMode
|
110 |
? 'bg-gray-800/95 border-gray-700'
|
111 |
: 'bg-white/95 border-gray-200'
|
112 |
-
} backdrop-blur-sm border-b`}>
|
113 |
-
<div className="flex items-center justify-between px-4 py-3">
|
114 |
-
|
|
|
|
|
115 |
<button
|
116 |
onClick={() => setSidebarOpen(!sidebarOpen)}
|
117 |
-
className={`p-2 rounded-lg transition-
|
118 |
darkMode
|
119 |
-
? 'hover:bg-gray-700 text-gray-300'
|
120 |
-
: 'hover:bg-gray-100 text-gray-600'
|
121 |
-
}`}
|
|
|
122 |
>
|
123 |
-
<
|
124 |
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
125 |
-
</svg>
|
126 |
</button>
|
127 |
|
|
|
128 |
<button
|
129 |
onClick={goBackToHome}
|
130 |
-
className="
|
131 |
>
|
132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
</button>
|
134 |
</div>
|
135 |
|
136 |
-
|
|
|
|
|
137 |
{chatStarted && (
|
138 |
<button
|
139 |
onClick={goBackToHome}
|
140 |
-
className={`p-2 rounded-lg transition-
|
141 |
darkMode
|
142 |
-
? 'hover:bg-gray-700 text-gray-300'
|
143 |
-
: 'hover:bg-gray-100 text-gray-600'
|
144 |
-
}`}
|
145 |
title="Back to Home"
|
146 |
>
|
147 |
-
<HomeIcon className="w-5 h-5" />
|
148 |
</button>
|
149 |
)}
|
150 |
|
|
|
151 |
<button
|
152 |
onClick={toggleDarkMode}
|
153 |
-
className={`p-2 rounded-lg transition-
|
154 |
darkMode
|
155 |
-
? 'hover:bg-gray-700 text-gray-300'
|
156 |
-
: 'hover:bg-gray-100 text-gray-600'
|
157 |
-
}`}
|
|
|
158 |
>
|
159 |
{darkMode ? (
|
160 |
-
<SunIcon className="w-5 h-5" />
|
161 |
) : (
|
162 |
-
<MoonIcon className="w-5 h-5" />
|
163 |
)}
|
164 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
</div>
|
166 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
</header>
|
168 |
|
169 |
{/* Sidebar */}
|
@@ -179,10 +217,10 @@ function App() {
|
|
179 |
darkMode={darkMode}
|
180 |
/>
|
181 |
|
182 |
-
{/* Main Content */}
|
183 |
<main className={`transition-all duration-200 ${
|
184 |
-
sidebarOpen ? 'md:ml-
|
185 |
-
} pt-16`}>
|
186 |
{chatStarted ? (
|
187 |
<ChatInterface
|
188 |
conversationId={activeConversationId}
|
@@ -199,18 +237,86 @@ function App() {
|
|
199 |
)}
|
200 |
</main>
|
201 |
|
202 |
-
{/* Toast notifications */}
|
203 |
<Toaster
|
204 |
-
position="top-
|
|
|
|
|
|
|
205 |
toastOptions={{
|
206 |
duration: 4000,
|
207 |
style: {
|
208 |
background: darkMode ? '#374151' : '#ffffff',
|
209 |
color: darkMode ? '#f9fafb' : '#111827',
|
210 |
border: darkMode ? '1px solid #4b5563' : '1px solid #e5e7eb',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
211 |
},
|
212 |
}}
|
213 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
214 |
</div>
|
215 |
);
|
216 |
}
|
|
|
3 |
import ChatInterface from './components/ChatInterface';
|
4 |
import Sidebar from './components/Sidebar';
|
5 |
import WelcomeScreen from './components/WelcomeScreen';
|
6 |
+
import { SunIcon, MoonIcon, HomeIcon, Bars3Icon } from '@heroicons/react/24/outline';
|
7 |
import ConversationStorage from './utils/conversationStorage';
|
8 |
|
9 |
function App() {
|
|
|
104 |
? 'bg-gray-900 text-white'
|
105 |
: 'bg-gray-50 text-gray-900'
|
106 |
}`}>
|
107 |
+
{/* Header - Mobile optimized */}
|
108 |
<header className={`fixed top-0 left-0 right-0 z-50 ${
|
109 |
darkMode
|
110 |
? 'bg-gray-800/95 border-gray-700'
|
111 |
: 'bg-white/95 border-gray-200'
|
112 |
+
} backdrop-blur-sm border-b shadow-sm`}>
|
113 |
+
<div className="flex items-center justify-between px-3 md:px-4 py-3 md:py-3">
|
114 |
+
{/* Left side - Menu and Title */}
|
115 |
+
<div className="flex items-center space-x-2 md:space-x-4 flex-1 min-w-0">
|
116 |
+
{/* Hamburger Menu Button - Larger touch target */}
|
117 |
<button
|
118 |
onClick={() => setSidebarOpen(!sidebarOpen)}
|
119 |
+
className={`p-2 md:p-2 rounded-lg md:rounded-xl transition-all duration-200 touch-manipulation ${
|
120 |
darkMode
|
121 |
+
? 'hover:bg-gray-700 active:bg-gray-600 text-gray-300 hover:text-white'
|
122 |
+
: 'hover:bg-gray-100 active:bg-gray-200 text-gray-600 hover:text-gray-900'
|
123 |
+
} min-w-[44px] min-h-[44px] md:min-w-[40px] md:min-h-[40px] flex items-center justify-center`}
|
124 |
+
title="Toggle menu"
|
125 |
>
|
126 |
+
<Bars3Icon className="w-6 h-6 md:w-5 md:h-5" />
|
|
|
|
|
127 |
</button>
|
128 |
|
129 |
+
{/* App Title - Mobile optimized */}
|
130 |
<button
|
131 |
onClick={goBackToHome}
|
132 |
+
className="flex-1 min-w-0 text-left group"
|
133 |
>
|
134 |
+
<h1 className="text-lg md:text-xl font-bold gradient-text hover:opacity-80 transition-opacity truncate">
|
135 |
+
CA Study Assistant
|
136 |
+
</h1>
|
137 |
+
{/* Subtitle for larger screens */}
|
138 |
+
<p className={`text-xs hidden md:block ${
|
139 |
+
darkMode ? 'text-gray-400' : 'text-gray-500'
|
140 |
+
}`}>
|
141 |
+
AI-powered study companion
|
142 |
+
</p>
|
143 |
</button>
|
144 |
</div>
|
145 |
|
146 |
+
{/* Right side - Action buttons */}
|
147 |
+
<div className="flex items-center space-x-1 md:space-x-2 flex-shrink-0">
|
148 |
+
{/* Home Button - Only show in chat mode */}
|
149 |
{chatStarted && (
|
150 |
<button
|
151 |
onClick={goBackToHome}
|
152 |
+
className={`p-2 md:p-2 rounded-lg md:rounded-xl transition-all duration-200 touch-manipulation ${
|
153 |
darkMode
|
154 |
+
? 'hover:bg-gray-700 active:bg-gray-600 text-gray-300 hover:text-white'
|
155 |
+
: 'hover:bg-gray-100 active:bg-gray-200 text-gray-600 hover:text-gray-900'
|
156 |
+
} min-w-[44px] min-h-[44px] md:min-w-[40px] md:min-h-[40px] flex items-center justify-center`}
|
157 |
title="Back to Home"
|
158 |
>
|
159 |
+
<HomeIcon className="w-6 h-6 md:w-5 md:h-5" />
|
160 |
</button>
|
161 |
)}
|
162 |
|
163 |
+
{/* Dark Mode Toggle - Larger touch target */}
|
164 |
<button
|
165 |
onClick={toggleDarkMode}
|
166 |
+
className={`p-2 md:p-2 rounded-lg md:rounded-xl transition-all duration-200 touch-manipulation ${
|
167 |
darkMode
|
168 |
+
? 'hover:bg-gray-700 active:bg-gray-600 text-gray-300 hover:text-yellow-400'
|
169 |
+
: 'hover:bg-gray-100 active:bg-gray-200 text-gray-600 hover:text-yellow-600'
|
170 |
+
} min-w-[44px] min-h-[44px] md:min-w-[40px] md:min-h-[40px] flex items-center justify-center`}
|
171 |
+
title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
172 |
>
|
173 |
{darkMode ? (
|
174 |
+
<SunIcon className="w-6 h-6 md:w-5 md:h-5" />
|
175 |
) : (
|
176 |
+
<MoonIcon className="w-6 h-6 md:w-5 md:h-5" />
|
177 |
)}
|
178 |
</button>
|
179 |
+
|
180 |
+
{/* Conversation count badge - Mobile optimized */}
|
181 |
+
{conversations.length > 0 && (
|
182 |
+
<div className={`hidden sm:flex items-center px-2 md:px-3 py-1 md:py-1.5 rounded-full text-xs md:text-sm font-medium ${
|
183 |
+
darkMode
|
184 |
+
? 'bg-primary-900/30 text-primary-300 border border-primary-700/50'
|
185 |
+
: 'bg-primary-50 text-primary-700 border border-primary-200'
|
186 |
+
}`}>
|
187 |
+
{conversations.length} chat{conversations.length !== 1 ? 's' : ''}
|
188 |
+
</div>
|
189 |
+
)}
|
190 |
</div>
|
191 |
</div>
|
192 |
+
|
193 |
+
{/* Mobile conversation indicator - Shows only on small screens */}
|
194 |
+
{conversations.length > 0 && (
|
195 |
+
<div className="sm:hidden px-3 pb-2">
|
196 |
+
<div className={`flex items-center justify-center px-3 py-1 rounded-full text-xs font-medium ${
|
197 |
+
darkMode
|
198 |
+
? 'bg-primary-900/30 text-primary-300 border border-primary-700/50'
|
199 |
+
: 'bg-primary-50 text-primary-700 border border-primary-200'
|
200 |
+
}`}>
|
201 |
+
📚 {conversations.length} conversation{conversations.length !== 1 ? 's' : ''} saved
|
202 |
+
</div>
|
203 |
+
</div>
|
204 |
+
)}
|
205 |
</header>
|
206 |
|
207 |
{/* Sidebar */}
|
|
|
217 |
darkMode={darkMode}
|
218 |
/>
|
219 |
|
220 |
+
{/* Main Content - Mobile optimized */}
|
221 |
<main className={`transition-all duration-200 ${
|
222 |
+
sidebarOpen ? 'md:ml-80' : 'ml-0'
|
223 |
+
} pt-16 md:pt-20 min-h-screen`}>
|
224 |
{chatStarted ? (
|
225 |
<ChatInterface
|
226 |
conversationId={activeConversationId}
|
|
|
237 |
)}
|
238 |
</main>
|
239 |
|
240 |
+
{/* Toast notifications - Mobile optimized */}
|
241 |
<Toaster
|
242 |
+
position="top-center"
|
243 |
+
containerStyle={{
|
244 |
+
top: '80px', // Account for header height
|
245 |
+
}}
|
246 |
toastOptions={{
|
247 |
duration: 4000,
|
248 |
style: {
|
249 |
background: darkMode ? '#374151' : '#ffffff',
|
250 |
color: darkMode ? '#f9fafb' : '#111827',
|
251 |
border: darkMode ? '1px solid #4b5563' : '1px solid #e5e7eb',
|
252 |
+
borderRadius: '12px',
|
253 |
+
padding: '12px 16px',
|
254 |
+
fontSize: '14px',
|
255 |
+
maxWidth: '90vw',
|
256 |
+
wordBreak: 'break-word',
|
257 |
+
},
|
258 |
+
success: {
|
259 |
+
iconTheme: {
|
260 |
+
primary: '#10b981',
|
261 |
+
secondary: '#ffffff',
|
262 |
+
},
|
263 |
+
},
|
264 |
+
error: {
|
265 |
+
iconTheme: {
|
266 |
+
primary: '#ef4444',
|
267 |
+
secondary: '#ffffff',
|
268 |
+
},
|
269 |
},
|
270 |
}}
|
271 |
/>
|
272 |
+
|
273 |
+
{/* Global loading overlay for mobile (if needed) */}
|
274 |
+
{/* This can be used for app-wide loading states */}
|
275 |
+
|
276 |
+
{/* Safe area spacing for mobile devices */}
|
277 |
+
<style jsx global>{`
|
278 |
+
@supports (padding: max(0px)) {
|
279 |
+
.pb-safe {
|
280 |
+
padding-bottom: max(env(safe-area-inset-bottom), 1rem);
|
281 |
+
}
|
282 |
+
}
|
283 |
+
|
284 |
+
/* Prevent zoom on double-tap for iOS */
|
285 |
+
button, input, select, textarea {
|
286 |
+
touch-action: manipulation;
|
287 |
+
}
|
288 |
+
|
289 |
+
/* Improve scrolling on mobile */
|
290 |
+
* {
|
291 |
+
-webkit-overflow-scrolling: touch;
|
292 |
+
}
|
293 |
+
|
294 |
+
/* Custom gradient text */
|
295 |
+
.gradient-text {
|
296 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
297 |
+
-webkit-background-clip: text;
|
298 |
+
-webkit-text-fill-color: transparent;
|
299 |
+
background-clip: text;
|
300 |
+
}
|
301 |
+
|
302 |
+
/* Mobile-specific touch improvements */
|
303 |
+
@media (max-width: 768px) {
|
304 |
+
/* Increase minimum touch target size */
|
305 |
+
button, [role="button"] {
|
306 |
+
min-height: 44px;
|
307 |
+
min-width: 44px;
|
308 |
+
}
|
309 |
+
|
310 |
+
/* Reduce motion for users who prefer it */
|
311 |
+
@media (prefers-reduced-motion: reduce) {
|
312 |
+
* {
|
313 |
+
animation-duration: 0.01ms !important;
|
314 |
+
animation-iteration-count: 1 !important;
|
315 |
+
transition-duration: 0.01ms !important;
|
316 |
+
}
|
317 |
+
}
|
318 |
+
}
|
319 |
+
`}</style>
|
320 |
</div>
|
321 |
);
|
322 |
}
|
frontend/src/components/ChatInterface.js
CHANGED
@@ -112,40 +112,40 @@ const ChatInterface = ({ conversationId, conversations, setConversations, darkMo
|
|
112 |
|
113 |
return (
|
114 |
<div className="flex flex-col h-screen">
|
115 |
-
{/* Messages Container */}
|
116 |
-
<div className="flex-1 overflow-y-auto px-4 py-6">
|
117 |
-
<div className="max-w-
|
118 |
-
{/* Empty State */}
|
119 |
{messages.length === 0 && !isLoading && (
|
120 |
<motion.div
|
121 |
initial={{ opacity: 0, y: 20 }}
|
122 |
animate={{ opacity: 1, y: 0 }}
|
123 |
transition={{ duration: 0.6 }}
|
124 |
-
className="flex flex-col items-center justify-center min-h-[60vh] text-center"
|
125 |
>
|
126 |
-
{/* CA Assistant Avatar */}
|
127 |
<motion.div
|
128 |
initial={{ scale: 0.8 }}
|
129 |
animate={{ scale: 1 }}
|
130 |
transition={{ duration: 0.5, delay: 0.2 }}
|
131 |
-
className={`w-20 h-20 rounded-full flex items-center justify-center mb-6 ${
|
132 |
darkMode
|
133 |
? 'bg-gradient-to-br from-primary-600 to-purple-600'
|
134 |
: 'bg-gradient-to-br from-primary-500 to-purple-500'
|
135 |
} shadow-lg`}
|
136 |
>
|
137 |
-
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
138 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
139 |
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
140 |
</svg>
|
141 |
</motion.div>
|
142 |
|
143 |
-
{/* Welcome Message */}
|
144 |
<motion.h2
|
145 |
initial={{ opacity: 0 }}
|
146 |
animate={{ opacity: 1 }}
|
147 |
transition={{ delay: 0.4 }}
|
148 |
-
className="text-
|
149 |
>
|
150 |
Hello! I'm your CA Study Assistant
|
151 |
</motion.h2>
|
@@ -154,26 +154,27 @@ const ChatInterface = ({ conversationId, conversations, setConversations, darkMo
|
|
154 |
initial={{ opacity: 0 }}
|
155 |
animate={{ opacity: 1 }}
|
156 |
transition={{ delay: 0.5 }}
|
157 |
-
className={`text-lg mb-8 ${darkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
158 |
>
|
159 |
I'm here to help you with accounting, finance, taxation, and auditing concepts.
|
160 |
Ask me anything or upload your study materials!
|
161 |
</motion.p>
|
162 |
|
163 |
-
{/* Quick Start Suggestions */}
|
164 |
<motion.div
|
165 |
initial={{ opacity: 0, y: 20 }}
|
166 |
animate={{ opacity: 1, y: 0 }}
|
167 |
transition={{ delay: 0.6 }}
|
168 |
-
className="w-full max-w-2xl"
|
169 |
>
|
170 |
-
<h3 className={`text-sm font-semibold mb-4 ${
|
171 |
darkMode ? 'text-gray-400' : 'text-gray-500'
|
172 |
-
}`}>
|
173 |
Try asking me about:
|
174 |
</h3>
|
175 |
|
176 |
-
|
|
|
177 |
{[
|
178 |
{ icon: "📊", text: "Financial statement analysis", query: "Explain financial statement analysis" },
|
179 |
{ icon: "💰", text: "Depreciation methods", query: "What are different depreciation methods?" },
|
@@ -190,41 +191,41 @@ const ChatInterface = ({ conversationId, conversations, setConversations, darkMo
|
|
190 |
whileHover={{ scale: 1.02, y: -2 }}
|
191 |
whileTap={{ scale: 0.98 }}
|
192 |
onClick={() => setMessage(suggestion.query)}
|
193 |
-
className={`flex items-center p-4 rounded-xl text-left transition-all ${
|
194 |
darkMode
|
195 |
-
? 'bg-gray-800 hover:bg-gray-700 border-gray-700 text-gray-300'
|
196 |
-
: 'bg-gray-50 hover:bg-gray-100 border-gray-200 text-gray-700'
|
197 |
-
} border hover:border-primary-300 hover:shadow-md`}
|
198 |
>
|
199 |
-
<span className="text-2xl mr-3">{suggestion.icon}</span>
|
200 |
-
<span className="font-medium">{suggestion.text}</span>
|
201 |
</motion.button>
|
202 |
))}
|
203 |
</div>
|
204 |
</motion.div>
|
205 |
|
206 |
-
{/* Upload Reminder */}
|
207 |
<motion.div
|
208 |
initial={{ opacity: 0 }}
|
209 |
animate={{ opacity: 1 }}
|
210 |
transition={{ delay: 1.2 }}
|
211 |
-
className={`mt-8 p-4 rounded-xl ${
|
212 |
darkMode
|
213 |
? 'bg-primary-900/20 border-primary-700/30'
|
214 |
: 'bg-primary-50 border-primary-200'
|
215 |
} border`}
|
216 |
>
|
217 |
<div className="flex items-center justify-center">
|
218 |
-
<svg className={`w-5 h-5 mr-2 ${
|
219 |
darkMode ? 'text-primary-400' : 'text-primary-600'
|
220 |
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
221 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
222 |
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
223 |
</svg>
|
224 |
-
<span className={`text-sm ${
|
225 |
darkMode ? 'text-primary-300' : 'text-primary-700'
|
226 |
}`}>
|
227 |
-
💡 Upload your study materials for more specific
|
228 |
</span>
|
229 |
</div>
|
230 |
</motion.div>
|
@@ -249,118 +250,96 @@ const ChatInterface = ({ conversationId, conversations, setConversations, darkMo
|
|
249 |
</div>
|
250 |
</div>
|
251 |
|
252 |
-
{/* File Uploader Modal */}
|
253 |
<AnimatePresence>
|
254 |
{showFileUploader && (
|
255 |
<motion.div
|
256 |
initial={{ opacity: 0 }}
|
257 |
animate={{ opacity: 1 }}
|
258 |
exit={{ opacity: 0 }}
|
259 |
-
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
260 |
onClick={() => setShowFileUploader(false)}
|
261 |
>
|
262 |
<motion.div
|
263 |
-
initial={{
|
264 |
-
animate={{
|
265 |
-
exit={{
|
266 |
onClick={(e) => e.stopPropagation()}
|
267 |
-
className={`max-w-md w-
|
268 |
darkMode ? 'bg-gray-800' : 'bg-white'
|
269 |
-
} shadow-2xl`}
|
270 |
>
|
271 |
-
<h3 className="text-lg font-semibold mb-4">Upload Document</h3>
|
272 |
<FileUploader darkMode={darkMode} onClose={() => setShowFileUploader(false)} />
|
273 |
</motion.div>
|
274 |
</motion.div>
|
275 |
)}
|
276 |
</AnimatePresence>
|
277 |
|
278 |
-
{/* Input Area */}
|
279 |
<div className={`border-t ${
|
280 |
darkMode ? 'border-gray-700/50 bg-gray-900/95' : 'border-gray-200/50 bg-white/95'
|
281 |
-
} backdrop-blur-sm p-6`}>
|
282 |
-
<div className="max-w-
|
283 |
<form onSubmit={handleSubmit} className="relative">
|
284 |
-
{/*
|
285 |
-
<div className={`relative overflow-hidden rounded-2xl border-2 transition-all duration-300 ${
|
286 |
darkMode
|
287 |
? 'bg-gradient-to-br from-gray-800 to-gray-900 border-gray-600 focus-within:border-primary-500 focus-within:from-gray-700 focus-within:to-gray-800'
|
288 |
: 'bg-gradient-to-br from-white to-gray-50 border-gray-300 focus-within:border-primary-500 focus-within:from-blue-50 focus-within:to-white'
|
289 |
-
} focus-within:ring-4 focus-within:ring-primary-500/20 shadow-xl hover:shadow-2xl focus-within:shadow-2xl`}>
|
290 |
|
291 |
-
{/*
|
292 |
-
<div className=
|
293 |
-
|
294 |
-
? 'bg-gradient-to-br from-primary-900/20 to-purple-900/20'
|
295 |
-
: 'bg-gradient-to-br from-primary-50/50 to-purple-50/50'
|
296 |
-
}`} />
|
297 |
-
|
298 |
-
{/* Input Content */}
|
299 |
-
<div className="relative flex items-end space-x-4 p-4">
|
300 |
-
{/* File Upload Button */}
|
301 |
<motion.button
|
302 |
type="button"
|
303 |
whileHover={{ scale: 1.05 }}
|
304 |
whileTap={{ scale: 0.95 }}
|
305 |
onClick={() => setShowFileUploader(true)}
|
306 |
-
className={`flex-shrink-0 p-3 rounded-xl transition-all duration-200 ${
|
307 |
darkMode
|
308 |
-
? 'hover:bg-gray-700/70 text-gray-400 hover:text-primary-400
|
309 |
-
: 'hover:bg-gray-100/70 text-gray-500 hover:text-primary-600
|
310 |
-
} relative group backdrop-blur-sm`}
|
311 |
title="Upload document"
|
312 |
>
|
313 |
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
314 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
315 |
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
316 |
</svg>
|
317 |
-
|
318 |
-
{/* Enhanced Tooltip */}
|
319 |
-
<div className={`absolute -top-14 left-1/2 transform -translate-x-1/2 px-3 py-2 rounded-lg text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-all duration-200 ${
|
320 |
-
darkMode ? 'bg-gray-800 text-white shadow-xl border border-gray-700' : 'bg-gray-900 text-white shadow-xl'
|
321 |
-
}`}>
|
322 |
-
Upload documents
|
323 |
-
<div className={`absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent ${
|
324 |
-
darkMode ? 'border-t-gray-800' : 'border-t-gray-900'
|
325 |
-
}`} />
|
326 |
-
</div>
|
327 |
</motion.button>
|
328 |
|
329 |
-
{/*
|
330 |
<div className="flex-1 relative">
|
331 |
<textarea
|
332 |
ref={textareaRef}
|
333 |
value={message}
|
334 |
onChange={(e) => setMessage(e.target.value)}
|
335 |
onKeyDown={handleKeyDown}
|
336 |
-
placeholder={messages.length === 0 ? "
|
337 |
-
className={`w-full resize-none border-none outline-none bg-transparent py-3 px-2 text-base leading-relaxed ${
|
338 |
darkMode ? 'text-white placeholder-gray-400' : 'text-gray-900 placeholder-gray-500'
|
339 |
-
} placeholder:text-sm placeholder:leading-relaxed`}
|
340 |
rows={1}
|
341 |
disabled={isLoading}
|
342 |
style={{
|
343 |
minHeight: '24px',
|
344 |
-
maxHeight: '
|
345 |
lineHeight: '1.5'
|
346 |
}}
|
347 |
/>
|
348 |
-
|
349 |
-
{/* Input Focus Indicator */}
|
350 |
-
<div className={`absolute left-0 bottom-0 h-0.5 w-0 bg-gradient-to-r from-primary-500 to-purple-500 transition-all duration-300 ${
|
351 |
-
message.trim() ? 'w-full' : 'group-focus-within:w-full'
|
352 |
-
}`} />
|
353 |
</div>
|
354 |
|
355 |
-
{/*
|
356 |
<motion.button
|
357 |
type="submit"
|
358 |
disabled={!message.trim() || isLoading}
|
359 |
whileHover={message.trim() && !isLoading ? { scale: 1.05 } : {}}
|
360 |
whileTap={message.trim() && !isLoading ? { scale: 0.95 } : {}}
|
361 |
-
className={`flex-shrink-0 p-3 rounded-xl transition-all duration-200 relative group ${
|
362 |
message.trim() && !isLoading
|
363 |
-
? 'bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 text-white shadow-lg hover:shadow-xl'
|
364 |
: darkMode
|
365 |
? 'bg-gray-600/50 text-gray-400 hover:bg-gray-600/70'
|
366 |
: 'bg-gray-300/50 text-gray-500 hover:bg-gray-300/70'
|
@@ -369,31 +348,23 @@ const ChatInterface = ({ conversationId, conversations, setConversations, darkMo
|
|
369 |
>
|
370 |
{isLoading ? (
|
371 |
<div className="relative">
|
372 |
-
<StopIcon className="w-5 h-5" />
|
373 |
<div className="absolute inset-0 border-2 border-white border-t-transparent rounded-full animate-spin opacity-50"></div>
|
374 |
</div>
|
375 |
) : (
|
376 |
-
<PaperAirplaneIcon className="w-5 h-5" />
|
377 |
-
)}
|
378 |
-
|
379 |
-
{/* Enhanced Send Button Glow Effect */}
|
380 |
-
{message.trim() && !isLoading && (
|
381 |
-
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-primary-600 to-primary-700 opacity-0 group-hover:opacity-30 transition-opacity duration-200 blur-lg -z-10"></div>
|
382 |
)}
|
383 |
</motion.button>
|
384 |
</div>
|
385 |
-
|
386 |
-
{/* Bottom Border Accent */}
|
387 |
-
<div className={`absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-primary-500 to-transparent opacity-0 focus-within:opacity-100 transition-opacity duration-300`} />
|
388 |
</div>
|
389 |
</form>
|
390 |
|
391 |
-
{/* Footer Text */}
|
392 |
<motion.p
|
393 |
initial={{ opacity: 0 }}
|
394 |
animate={{ opacity: 1 }}
|
395 |
transition={{ delay: 0.3 }}
|
396 |
-
className={`text-xs text-center mt-3 ${
|
397 |
darkMode ? 'text-gray-500' : 'text-gray-400'
|
398 |
}`}
|
399 |
>
|
|
|
112 |
|
113 |
return (
|
114 |
<div className="flex flex-col h-screen">
|
115 |
+
{/* Messages Container - Mobile optimized */}
|
116 |
+
<div className="flex-1 overflow-y-auto px-3 md:px-4 py-4 md:py-6">
|
117 |
+
<div className="max-w-4xl mx-auto">
|
118 |
+
{/* Empty State - Mobile optimized */}
|
119 |
{messages.length === 0 && !isLoading && (
|
120 |
<motion.div
|
121 |
initial={{ opacity: 0, y: 20 }}
|
122 |
animate={{ opacity: 1, y: 0 }}
|
123 |
transition={{ duration: 0.6 }}
|
124 |
+
className="flex flex-col items-center justify-center min-h-[50vh] md:min-h-[60vh] text-center px-4"
|
125 |
>
|
126 |
+
{/* CA Assistant Avatar - Larger on mobile */}
|
127 |
<motion.div
|
128 |
initial={{ scale: 0.8 }}
|
129 |
animate={{ scale: 1 }}
|
130 |
transition={{ duration: 0.5, delay: 0.2 }}
|
131 |
+
className={`w-16 h-16 md:w-20 md:h-20 rounded-full flex items-center justify-center mb-4 md:mb-6 ${
|
132 |
darkMode
|
133 |
? 'bg-gradient-to-br from-primary-600 to-purple-600'
|
134 |
: 'bg-gradient-to-br from-primary-500 to-purple-500'
|
135 |
} shadow-lg`}
|
136 |
>
|
137 |
+
<svg className="w-8 h-8 md:w-10 md:h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
138 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
139 |
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
140 |
</svg>
|
141 |
</motion.div>
|
142 |
|
143 |
+
{/* Welcome Message - Mobile optimized */}
|
144 |
<motion.h2
|
145 |
initial={{ opacity: 0 }}
|
146 |
animate={{ opacity: 1 }}
|
147 |
transition={{ delay: 0.4 }}
|
148 |
+
className="text-xl md:text-2xl lg:text-3xl font-bold mb-2 md:mb-3 gradient-text text-center"
|
149 |
>
|
150 |
Hello! I'm your CA Study Assistant
|
151 |
</motion.h2>
|
|
|
154 |
initial={{ opacity: 0 }}
|
155 |
animate={{ opacity: 1 }}
|
156 |
transition={{ delay: 0.5 }}
|
157 |
+
className={`text-base md:text-lg mb-6 md:mb-8 px-2 ${darkMode ? 'text-gray-300' : 'text-gray-600'} text-center max-w-md`}
|
158 |
>
|
159 |
I'm here to help you with accounting, finance, taxation, and auditing concepts.
|
160 |
Ask me anything or upload your study materials!
|
161 |
</motion.p>
|
162 |
|
163 |
+
{/* Quick Start Suggestions - Mobile optimized */}
|
164 |
<motion.div
|
165 |
initial={{ opacity: 0, y: 20 }}
|
166 |
animate={{ opacity: 1, y: 0 }}
|
167 |
transition={{ delay: 0.6 }}
|
168 |
+
className="w-full max-w-lg md:max-w-2xl"
|
169 |
>
|
170 |
+
<h3 className={`text-xs md:text-sm font-semibold mb-3 md:mb-4 ${
|
171 |
darkMode ? 'text-gray-400' : 'text-gray-500'
|
172 |
+
} text-center`}>
|
173 |
Try asking me about:
|
174 |
</h3>
|
175 |
|
176 |
+
{/* Mobile: Single column, Desktop: Two columns */}
|
177 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-3">
|
178 |
{[
|
179 |
{ icon: "📊", text: "Financial statement analysis", query: "Explain financial statement analysis" },
|
180 |
{ icon: "💰", text: "Depreciation methods", query: "What are different depreciation methods?" },
|
|
|
191 |
whileHover={{ scale: 1.02, y: -2 }}
|
192 |
whileTap={{ scale: 0.98 }}
|
193 |
onClick={() => setMessage(suggestion.query)}
|
194 |
+
className={`flex items-center p-3 md:p-4 rounded-lg md:rounded-xl text-left transition-all touch-manipulation ${
|
195 |
darkMode
|
196 |
+
? 'bg-gray-800 hover:bg-gray-700 active:bg-gray-600 border-gray-700 text-gray-300'
|
197 |
+
: 'bg-gray-50 hover:bg-gray-100 active:bg-gray-200 border-gray-200 text-gray-700'
|
198 |
+
} border hover:border-primary-300 hover:shadow-md active:shadow-lg`}
|
199 |
>
|
200 |
+
<span className="text-xl md:text-2xl mr-2 md:mr-3 flex-shrink-0">{suggestion.icon}</span>
|
201 |
+
<span className="font-medium text-sm md:text-base">{suggestion.text}</span>
|
202 |
</motion.button>
|
203 |
))}
|
204 |
</div>
|
205 |
</motion.div>
|
206 |
|
207 |
+
{/* Upload Reminder - Mobile optimized */}
|
208 |
<motion.div
|
209 |
initial={{ opacity: 0 }}
|
210 |
animate={{ opacity: 1 }}
|
211 |
transition={{ delay: 1.2 }}
|
212 |
+
className={`mt-6 md:mt-8 p-3 md:p-4 rounded-lg md:rounded-xl max-w-md ${
|
213 |
darkMode
|
214 |
? 'bg-primary-900/20 border-primary-700/30'
|
215 |
: 'bg-primary-50 border-primary-200'
|
216 |
} border`}
|
217 |
>
|
218 |
<div className="flex items-center justify-center">
|
219 |
+
<svg className={`w-4 h-4 md:w-5 md:h-5 mr-2 flex-shrink-0 ${
|
220 |
darkMode ? 'text-primary-400' : 'text-primary-600'
|
221 |
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
222 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
223 |
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
224 |
</svg>
|
225 |
+
<span className={`text-xs md:text-sm text-center ${
|
226 |
darkMode ? 'text-primary-300' : 'text-primary-700'
|
227 |
}`}>
|
228 |
+
💡 Upload your study materials for more specific answers
|
229 |
</span>
|
230 |
</div>
|
231 |
</motion.div>
|
|
|
250 |
</div>
|
251 |
</div>
|
252 |
|
253 |
+
{/* File Uploader Modal - Mobile optimized */}
|
254 |
<AnimatePresence>
|
255 |
{showFileUploader && (
|
256 |
<motion.div
|
257 |
initial={{ opacity: 0 }}
|
258 |
animate={{ opacity: 1 }}
|
259 |
exit={{ opacity: 0 }}
|
260 |
+
className="fixed inset-0 bg-black bg-opacity-50 flex items-end md:items-center justify-center z-50 p-0 md:p-4"
|
261 |
onClick={() => setShowFileUploader(false)}
|
262 |
>
|
263 |
<motion.div
|
264 |
+
initial={{ y: '100%', opacity: 0 }}
|
265 |
+
animate={{ y: 0, opacity: 1 }}
|
266 |
+
exit={{ y: '100%', opacity: 0 }}
|
267 |
onClick={(e) => e.stopPropagation()}
|
268 |
+
className={`w-full max-w-md md:max-w-lg p-4 md:p-6 rounded-t-3xl md:rounded-2xl ${
|
269 |
darkMode ? 'bg-gray-800' : 'bg-white'
|
270 |
+
} shadow-2xl max-h-[80vh] overflow-y-auto`}
|
271 |
>
|
272 |
+
<h3 className="text-lg md:text-xl font-semibold mb-4 text-center md:text-left">Upload Document</h3>
|
273 |
<FileUploader darkMode={darkMode} onClose={() => setShowFileUploader(false)} />
|
274 |
</motion.div>
|
275 |
</motion.div>
|
276 |
)}
|
277 |
</AnimatePresence>
|
278 |
|
279 |
+
{/* Input Area - Mobile-first optimized */}
|
280 |
<div className={`border-t ${
|
281 |
darkMode ? 'border-gray-700/50 bg-gray-900/95' : 'border-gray-200/50 bg-white/95'
|
282 |
+
} backdrop-blur-sm p-3 md:p-6 pb-safe`}>
|
283 |
+
<div className="max-w-4xl mx-auto">
|
284 |
<form onSubmit={handleSubmit} className="relative">
|
285 |
+
{/* Mobile-optimized Input Container */}
|
286 |
+
<div className={`relative overflow-hidden rounded-2xl md:rounded-3xl border-2 transition-all duration-300 ${
|
287 |
darkMode
|
288 |
? 'bg-gradient-to-br from-gray-800 to-gray-900 border-gray-600 focus-within:border-primary-500 focus-within:from-gray-700 focus-within:to-gray-800'
|
289 |
: 'bg-gradient-to-br from-white to-gray-50 border-gray-300 focus-within:border-primary-500 focus-within:from-blue-50 focus-within:to-white'
|
290 |
+
} focus-within:ring-4 focus-within:ring-primary-500/20 shadow-lg md:shadow-xl hover:shadow-xl md:hover:shadow-2xl focus-within:shadow-xl md:focus-within:shadow-2xl`}>
|
291 |
|
292 |
+
{/* Input Content - Mobile optimized */}
|
293 |
+
<div className="relative flex items-end space-x-2 md:space-x-4 p-3 md:p-4">
|
294 |
+
{/* File Upload Button - Larger touch target */}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
295 |
<motion.button
|
296 |
type="button"
|
297 |
whileHover={{ scale: 1.05 }}
|
298 |
whileTap={{ scale: 0.95 }}
|
299 |
onClick={() => setShowFileUploader(true)}
|
300 |
+
className={`flex-shrink-0 p-3 md:p-3 rounded-xl md:rounded-xl transition-all duration-200 touch-manipulation ${
|
301 |
darkMode
|
302 |
+
? 'hover:bg-gray-700/70 active:bg-gray-600/70 text-gray-400 hover:text-primary-400'
|
303 |
+
: 'hover:bg-gray-100/70 active:bg-gray-200/70 text-gray-500 hover:text-primary-600'
|
304 |
+
} relative group backdrop-blur-sm min-h-[44px] min-w-[44px] flex items-center justify-center`}
|
305 |
title="Upload document"
|
306 |
>
|
307 |
+
<svg className="w-6 h-6 md:w-5 md:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
308 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
309 |
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
310 |
</svg>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
311 |
</motion.button>
|
312 |
|
313 |
+
{/* Text Input - Mobile optimized */}
|
314 |
<div className="flex-1 relative">
|
315 |
<textarea
|
316 |
ref={textareaRef}
|
317 |
value={message}
|
318 |
onChange={(e) => setMessage(e.target.value)}
|
319 |
onKeyDown={handleKeyDown}
|
320 |
+
placeholder={messages.length === 0 ? "Ask me about accounting, finance, taxation..." : "Ask a follow-up question..."}
|
321 |
+
className={`w-full resize-none border-none outline-none bg-transparent py-3 md:py-3 px-2 text-base md:text-base leading-relaxed ${
|
322 |
darkMode ? 'text-white placeholder-gray-400' : 'text-gray-900 placeholder-gray-500'
|
323 |
+
} placeholder:text-sm md:placeholder:text-sm placeholder:leading-relaxed touch-manipulation`}
|
324 |
rows={1}
|
325 |
disabled={isLoading}
|
326 |
style={{
|
327 |
minHeight: '24px',
|
328 |
+
maxHeight: '100px',
|
329 |
lineHeight: '1.5'
|
330 |
}}
|
331 |
/>
|
|
|
|
|
|
|
|
|
|
|
332 |
</div>
|
333 |
|
334 |
+
{/* Send Button - Larger touch target */}
|
335 |
<motion.button
|
336 |
type="submit"
|
337 |
disabled={!message.trim() || isLoading}
|
338 |
whileHover={message.trim() && !isLoading ? { scale: 1.05 } : {}}
|
339 |
whileTap={message.trim() && !isLoading ? { scale: 0.95 } : {}}
|
340 |
+
className={`flex-shrink-0 p-3 md:p-3 rounded-xl md:rounded-xl transition-all duration-200 relative group touch-manipulation min-h-[44px] min-w-[44px] flex items-center justify-center ${
|
341 |
message.trim() && !isLoading
|
342 |
+
? 'bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 active:from-primary-800 active:to-primary-900 text-white shadow-lg hover:shadow-xl active:shadow-2xl'
|
343 |
: darkMode
|
344 |
? 'bg-gray-600/50 text-gray-400 hover:bg-gray-600/70'
|
345 |
: 'bg-gray-300/50 text-gray-500 hover:bg-gray-300/70'
|
|
|
348 |
>
|
349 |
{isLoading ? (
|
350 |
<div className="relative">
|
351 |
+
<StopIcon className="w-6 h-6 md:w-5 md:h-5" />
|
352 |
<div className="absolute inset-0 border-2 border-white border-t-transparent rounded-full animate-spin opacity-50"></div>
|
353 |
</div>
|
354 |
) : (
|
355 |
+
<PaperAirplaneIcon className="w-6 h-6 md:w-5 md:h-5" />
|
|
|
|
|
|
|
|
|
|
|
356 |
)}
|
357 |
</motion.button>
|
358 |
</div>
|
|
|
|
|
|
|
359 |
</div>
|
360 |
</form>
|
361 |
|
362 |
+
{/* Footer Text - Mobile optimized */}
|
363 |
<motion.p
|
364 |
initial={{ opacity: 0 }}
|
365 |
animate={{ opacity: 1 }}
|
366 |
transition={{ delay: 0.3 }}
|
367 |
+
className={`text-xs text-center mt-2 md:mt-3 px-2 ${
|
368 |
darkMode ? 'text-gray-500' : 'text-gray-400'
|
369 |
}`}
|
370 |
>
|
frontend/src/components/FileUploader.js
CHANGED
@@ -110,173 +110,284 @@ const FileUploader = ({ darkMode, onClose }) => {
|
|
110 |
};
|
111 |
|
112 |
return (
|
113 |
-
<div className="space-y-4">
|
114 |
-
{/* Dropzone */}
|
115 |
<motion.div
|
116 |
{...getRootProps()}
|
117 |
-
whileHover={{ scale: 1.
|
118 |
-
whileTap={{ scale: 0.
|
119 |
-
className={`file-drop-zone border-2 border-dashed rounded-2xl p-8 text-center cursor-pointer transition-all ${
|
120 |
isDragActive
|
121 |
? darkMode
|
122 |
-
? 'border-primary-400 bg-primary-900/20'
|
123 |
-
: 'border-primary-500 bg-primary-50'
|
124 |
: darkMode
|
125 |
-
? 'border-gray-600 hover:border-gray-500 bg-gray-800'
|
126 |
-
: 'border-gray-300 hover:border-gray-400 bg-gray-50'
|
127 |
-
}`}
|
128 |
>
|
129 |
<input {...getInputProps()} />
|
130 |
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
|
137 |
-
|
|
|
138 |
darkMode ? 'text-white' : 'text-gray-900'
|
139 |
}`}>
|
140 |
-
{isDragActive ? 'Drop files here' : 'Upload study materials'}
|
141 |
</h3>
|
142 |
|
143 |
-
<p className={`mb-4 ${
|
144 |
darkMode ? 'text-gray-400' : 'text-gray-600'
|
145 |
}`}>
|
146 |
-
|
|
|
|
|
|
|
147 |
</p>
|
148 |
|
149 |
-
|
|
|
150 |
darkMode ? 'text-gray-500' : 'text-gray-500'
|
151 |
}`}>
|
152 |
-
Maximum file size: 100MB
|
153 |
</p>
|
154 |
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
174 |
}`}>
|
175 |
-
|
176 |
-
</
|
177 |
</div>
|
178 |
</motion.div>
|
179 |
|
180 |
-
{/* Upload Progress */}
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
<
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
|
198 |
-
{/* Uploaded Files List */}
|
199 |
<AnimatePresence>
|
200 |
{uploadedFiles.length > 0 && (
|
201 |
<motion.div
|
202 |
initial={{ opacity: 0, height: 0 }}
|
203 |
animate={{ opacity: 1, height: 'auto' }}
|
204 |
exit={{ opacity: 0, height: 0 }}
|
205 |
-
className="space-y-
|
206 |
>
|
207 |
-
<h4 className={`font-
|
208 |
-
darkMode ? 'text-gray-
|
209 |
}`}>
|
210 |
-
Uploaded Files
|
211 |
</h4>
|
212 |
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
<
|
229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
230 |
}`}>
|
231 |
-
|
232 |
-
</
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
{
|
237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
238 |
</div>
|
239 |
-
</div>
|
240 |
-
|
241 |
-
<div className="flex items-center space-x-2">
|
242 |
-
{file.status === 'success' ? (
|
243 |
-
<CheckCircleIcon className="w-5 h-5 text-green-500" />
|
244 |
-
) : (
|
245 |
-
<XCircleIcon className="w-5 h-5 text-red-500" />
|
246 |
-
)}
|
247 |
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
</motion.div>
|
262 |
)}
|
263 |
</AnimatePresence>
|
264 |
|
265 |
-
{/*
|
266 |
-
|
267 |
-
|
268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
269 |
onClick={onClose}
|
270 |
-
className={`px-
|
271 |
darkMode
|
272 |
-
? 'bg-gray-700 hover:bg-gray-600 text-
|
273 |
-
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
274 |
-
}`}
|
275 |
>
|
276 |
-
|
277 |
-
</button>
|
278 |
-
|
279 |
-
|
280 |
</div>
|
281 |
);
|
282 |
};
|
|
|
110 |
};
|
111 |
|
112 |
return (
|
113 |
+
<div className="space-y-4 md:space-y-6">
|
114 |
+
{/* Dropzone - Mobile optimized */}
|
115 |
<motion.div
|
116 |
{...getRootProps()}
|
117 |
+
whileHover={{ scale: 1.01 }}
|
118 |
+
whileTap={{ scale: 0.99 }}
|
119 |
+
className={`file-drop-zone border-2 border-dashed rounded-2xl md:rounded-3xl p-6 md:p-8 text-center cursor-pointer transition-all touch-manipulation ${
|
120 |
isDragActive
|
121 |
? darkMode
|
122 |
+
? 'border-primary-400 bg-primary-900/20 scale-[1.02]'
|
123 |
+
: 'border-primary-500 bg-primary-50 scale-[1.02]'
|
124 |
: darkMode
|
125 |
+
? 'border-gray-600 hover:border-gray-500 active:border-primary-400 bg-gray-800/50 hover:bg-gray-800'
|
126 |
+
: 'border-gray-300 hover:border-gray-400 active:border-primary-300 bg-gray-50 hover:bg-gray-100'
|
127 |
+
} min-h-[200px] md:min-h-[240px] flex flex-col justify-center`}
|
128 |
>
|
129 |
<input {...getInputProps()} />
|
130 |
|
131 |
+
{/* Upload Icon - Mobile optimized */}
|
132 |
+
<motion.div
|
133 |
+
animate={isDragActive ? { y: -5, scale: 1.1 } : { y: 0, scale: 1 }}
|
134 |
+
transition={{ duration: 0.2 }}
|
135 |
+
>
|
136 |
+
<CloudArrowUpIcon className={`w-16 h-16 md:w-20 md:h-20 mx-auto mb-4 md:mb-6 ${
|
137 |
+
isDragActive
|
138 |
+
? darkMode ? 'text-primary-400' : 'text-primary-500'
|
139 |
+
: darkMode ? 'text-gray-400' : 'text-gray-500'
|
140 |
+
}`} />
|
141 |
+
</motion.div>
|
142 |
|
143 |
+
{/* Upload Text - Mobile optimized */}
|
144 |
+
<h3 className={`text-xl md:text-2xl font-bold mb-3 md:mb-4 ${
|
145 |
darkMode ? 'text-white' : 'text-gray-900'
|
146 |
}`}>
|
147 |
+
{isDragActive ? 'Drop files here!' : 'Upload study materials'}
|
148 |
</h3>
|
149 |
|
150 |
+
<p className={`mb-4 md:mb-6 text-base md:text-lg px-2 ${
|
151 |
darkMode ? 'text-gray-400' : 'text-gray-600'
|
152 |
}`}>
|
153 |
+
{isDragActive
|
154 |
+
? 'Release to upload your files'
|
155 |
+
: 'Drag & drop files here, or tap to browse'
|
156 |
+
}
|
157 |
</p>
|
158 |
|
159 |
+
{/* File size info - Mobile optimized */}
|
160 |
+
<p className={`text-sm md:text-base mb-4 md:mb-6 ${
|
161 |
darkMode ? 'text-gray-500' : 'text-gray-500'
|
162 |
}`}>
|
163 |
+
Maximum file size: <span className="font-semibold">100MB</span>
|
164 |
</p>
|
165 |
|
166 |
+
{/* File type badges - Mobile optimized */}
|
167 |
+
<div className="flex flex-wrap justify-center gap-2 md:gap-3">
|
168 |
+
<motion.span
|
169 |
+
whileHover={{ scale: 1.05 }}
|
170 |
+
className={`px-4 py-2 md:px-5 md:py-2.5 rounded-full text-sm md:text-base font-semibold shadow-md ${
|
171 |
+
darkMode
|
172 |
+
? 'bg-blue-900/40 text-blue-300 border border-blue-700/50'
|
173 |
+
: 'bg-blue-100 text-blue-800 border border-blue-200'
|
174 |
+
}`}
|
175 |
+
>
|
176 |
+
📄 PDF
|
177 |
+
</motion.span>
|
178 |
+
<motion.span
|
179 |
+
whileHover={{ scale: 1.05 }}
|
180 |
+
className={`px-4 py-2 md:px-5 md:py-2.5 rounded-full text-sm md:text-base font-semibold shadow-md ${
|
181 |
+
darkMode
|
182 |
+
? 'bg-green-900/40 text-green-300 border border-green-700/50'
|
183 |
+
: 'bg-green-100 text-green-800 border border-green-200'
|
184 |
+
}`}
|
185 |
+
>
|
186 |
+
📝 DOCX
|
187 |
+
</motion.span>
|
188 |
+
<motion.span
|
189 |
+
whileHover={{ scale: 1.05 }}
|
190 |
+
className={`px-4 py-2 md:px-5 md:py-2.5 rounded-full text-sm md:text-base font-semibold shadow-md ${
|
191 |
+
darkMode
|
192 |
+
? 'bg-purple-900/40 text-purple-300 border border-purple-700/50'
|
193 |
+
: 'bg-purple-100 text-purple-800 border border-purple-200'
|
194 |
+
}`}
|
195 |
+
>
|
196 |
+
📋 TXT
|
197 |
+
</motion.span>
|
198 |
+
</div>
|
199 |
+
|
200 |
+
{/* Mobile-specific help text */}
|
201 |
+
<div className="md:hidden mt-4">
|
202 |
+
<p className={`text-xs ${
|
203 |
+
darkMode ? 'text-gray-500' : 'text-gray-400'
|
204 |
}`}>
|
205 |
+
💡 Tip: You can select multiple files at once
|
206 |
+
</p>
|
207 |
</div>
|
208 |
</motion.div>
|
209 |
|
210 |
+
{/* Upload Progress - Mobile optimized */}
|
211 |
+
<AnimatePresence>
|
212 |
+
{uploading && (
|
213 |
+
<motion.div
|
214 |
+
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
215 |
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
216 |
+
exit={{ opacity: 0, y: -20, scale: 0.95 }}
|
217 |
+
className={`p-4 md:p-6 rounded-2xl ${
|
218 |
+
darkMode ? 'bg-gray-800/50 border border-gray-700/50' : 'bg-gray-100/50 border border-gray-200/50'
|
219 |
+
} backdrop-blur-sm`}
|
220 |
+
>
|
221 |
+
<div className="flex items-center space-x-3 md:space-x-4">
|
222 |
+
<div className="relative">
|
223 |
+
<div className="animate-spin rounded-full h-6 w-6 md:h-8 md:w-8 border-b-2 border-primary-500"></div>
|
224 |
+
<div className="absolute inset-0 rounded-full border-2 border-primary-500/20"></div>
|
225 |
+
</div>
|
226 |
+
<div>
|
227 |
+
<span className={`font-medium text-base md:text-lg ${
|
228 |
+
darkMode ? 'text-gray-200' : 'text-gray-800'
|
229 |
+
}`}>
|
230 |
+
Uploading files...
|
231 |
+
</span>
|
232 |
+
<p className={`text-sm ${
|
233 |
+
darkMode ? 'text-gray-400' : 'text-gray-600'
|
234 |
+
}`}>
|
235 |
+
Please wait while we process your documents
|
236 |
+
</p>
|
237 |
+
</div>
|
238 |
+
</div>
|
239 |
+
</motion.div>
|
240 |
+
)}
|
241 |
+
</AnimatePresence>
|
242 |
|
243 |
+
{/* Uploaded Files List - Mobile optimized */}
|
244 |
<AnimatePresence>
|
245 |
{uploadedFiles.length > 0 && (
|
246 |
<motion.div
|
247 |
initial={{ opacity: 0, height: 0 }}
|
248 |
animate={{ opacity: 1, height: 'auto' }}
|
249 |
exit={{ opacity: 0, height: 0 }}
|
250 |
+
className="space-y-3 md:space-y-4"
|
251 |
>
|
252 |
+
<h4 className={`font-bold text-lg md:text-xl ${
|
253 |
+
darkMode ? 'text-gray-200' : 'text-gray-800'
|
254 |
}`}>
|
255 |
+
Uploaded Files ({uploadedFiles.length})
|
256 |
</h4>
|
257 |
|
258 |
+
<div className="space-y-2 md:space-y-3">
|
259 |
+
{uploadedFiles.map((file, index) => (
|
260 |
+
<motion.div
|
261 |
+
key={index}
|
262 |
+
initial={{ opacity: 0, x: -20 }}
|
263 |
+
animate={{ opacity: 1, x: 0 }}
|
264 |
+
transition={{ delay: index * 0.1 }}
|
265 |
+
className={`flex items-center justify-between p-4 md:p-5 rounded-xl md:rounded-2xl transition-all ${
|
266 |
+
darkMode
|
267 |
+
? 'bg-gray-800/70 border border-gray-700/50 hover:bg-gray-800'
|
268 |
+
: 'bg-gray-50 border border-gray-200/50 hover:bg-gray-100'
|
269 |
+
} shadow-sm hover:shadow-md`}
|
270 |
+
>
|
271 |
+
<div className="flex items-center space-x-3 md:space-x-4 flex-1 min-w-0">
|
272 |
+
{/* File Icon */}
|
273 |
+
<div className={`p-2 md:p-3 rounded-lg flex-shrink-0 ${
|
274 |
+
file.status === 'success'
|
275 |
+
? darkMode
|
276 |
+
? 'bg-green-900/30 text-green-400'
|
277 |
+
: 'bg-green-100 text-green-700'
|
278 |
+
: darkMode
|
279 |
+
? 'bg-red-900/30 text-red-400'
|
280 |
+
: 'bg-red-100 text-red-700'
|
281 |
}`}>
|
282 |
+
<DocumentIcon className="w-5 h-5 md:w-6 md:h-6" />
|
283 |
+
</div>
|
284 |
+
|
285 |
+
{/* File Info */}
|
286 |
+
<div className="flex-1 min-w-0">
|
287 |
+
<p className={`font-medium text-sm md:text-base truncate ${
|
288 |
+
darkMode ? 'text-gray-100' : 'text-gray-900'
|
289 |
+
}`}>
|
290 |
+
{file.name}
|
291 |
+
</p>
|
292 |
+
<div className="flex items-center space-x-2 md:space-x-3 mt-1">
|
293 |
+
<p className={`text-xs md:text-sm ${
|
294 |
+
darkMode ? 'text-gray-400' : 'text-gray-500'
|
295 |
+
}`}>
|
296 |
+
{formatFileSize(file.size)}
|
297 |
+
</p>
|
298 |
+
{file.status === 'error' && file.error && (
|
299 |
+
<>
|
300 |
+
<span className={`text-xs ${
|
301 |
+
darkMode ? 'text-gray-600' : 'text-gray-400'
|
302 |
+
}`}>•</span>
|
303 |
+
<p className={`text-xs truncate ${
|
304 |
+
darkMode ? 'text-red-400' : 'text-red-600'
|
305 |
+
}`}>
|
306 |
+
{file.error}
|
307 |
+
</p>
|
308 |
+
</>
|
309 |
+
)}
|
310 |
+
</div>
|
311 |
+
</div>
|
312 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
313 |
|
314 |
+
{/* Status and Actions */}
|
315 |
+
<div className="flex items-center space-x-2 md:space-x-3 flex-shrink-0">
|
316 |
+
{/* Status Icon */}
|
317 |
+
{file.status === 'success' ? (
|
318 |
+
<motion.div
|
319 |
+
initial={{ scale: 0 }}
|
320 |
+
animate={{ scale: 1 }}
|
321 |
+
transition={{ type: "spring", delay: 0.2 }}
|
322 |
+
>
|
323 |
+
<CheckCircleIcon className="w-6 h-6 md:w-7 md:h-7 text-green-500" />
|
324 |
+
</motion.div>
|
325 |
+
) : (
|
326 |
+
<motion.div
|
327 |
+
initial={{ scale: 0 }}
|
328 |
+
animate={{ scale: 1 }}
|
329 |
+
transition={{ type: "spring", delay: 0.2 }}
|
330 |
+
>
|
331 |
+
<XCircleIcon className="w-6 h-6 md:w-7 md:h-7 text-red-500" />
|
332 |
+
</motion.div>
|
333 |
+
)}
|
334 |
+
|
335 |
+
{/* Remove Button - Larger touch target */}
|
336 |
+
<motion.button
|
337 |
+
whileHover={{ scale: 1.1 }}
|
338 |
+
whileTap={{ scale: 0.9 }}
|
339 |
+
onClick={() => removeFile(index)}
|
340 |
+
className={`p-2 md:p-2.5 rounded-lg transition-colors touch-manipulation ${
|
341 |
+
darkMode
|
342 |
+
? 'hover:bg-gray-700 active:bg-gray-600 text-gray-400 hover:text-gray-200'
|
343 |
+
: 'hover:bg-gray-200 active:bg-gray-300 text-gray-500 hover:text-gray-700'
|
344 |
+
}`}
|
345 |
+
title="Remove file"
|
346 |
+
>
|
347 |
+
<XMarkIcon className="w-4 h-4 md:w-5 md:h-5" />
|
348 |
+
</motion.button>
|
349 |
+
</div>
|
350 |
+
</motion.div>
|
351 |
+
))}
|
352 |
+
</div>
|
353 |
</motion.div>
|
354 |
)}
|
355 |
</AnimatePresence>
|
356 |
|
357 |
+
{/* Action Buttons - Mobile optimized */}
|
358 |
+
<div className="flex flex-col sm:flex-row gap-3 md:gap-4 pt-2">
|
359 |
+
{/* Upload More Button */}
|
360 |
+
<motion.button
|
361 |
+
whileHover={{ scale: 1.02 }}
|
362 |
+
whileTap={{ scale: 0.98 }}
|
363 |
+
{...getRootProps()}
|
364 |
+
className={`flex-1 sm:flex-none px-6 py-3 md:py-3.5 rounded-xl md:rounded-2xl font-semibold transition-all touch-manipulation ${
|
365 |
+
darkMode
|
366 |
+
? 'bg-primary-600 hover:bg-primary-700 active:bg-primary-800 text-white shadow-lg'
|
367 |
+
: 'bg-primary-500 hover:bg-primary-600 active:bg-primary-700 text-white shadow-lg'
|
368 |
+
} hover:shadow-xl active:shadow-2xl disabled:opacity-50 disabled:cursor-not-allowed`}
|
369 |
+
disabled={uploading}
|
370 |
+
>
|
371 |
+
<input {...getInputProps()} />
|
372 |
+
{uploading ? 'Uploading...' : 'Upload More Files'}
|
373 |
+
</motion.button>
|
374 |
+
|
375 |
+
{/* Close Button */}
|
376 |
+
{onClose && (
|
377 |
+
<motion.button
|
378 |
+
whileHover={{ scale: 1.02 }}
|
379 |
+
whileTap={{ scale: 0.98 }}
|
380 |
onClick={onClose}
|
381 |
+
className={`flex-1 sm:flex-none px-6 py-3 md:py-3.5 rounded-xl md:rounded-2xl font-semibold transition-all touch-manipulation ${
|
382 |
darkMode
|
383 |
+
? 'bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-gray-200 shadow-lg'
|
384 |
+
: 'bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 shadow-lg'
|
385 |
+
} hover:shadow-xl active:shadow-2xl`}
|
386 |
>
|
387 |
+
Done
|
388 |
+
</motion.button>
|
389 |
+
)}
|
390 |
+
</div>
|
391 |
</div>
|
392 |
);
|
393 |
};
|
frontend/src/components/MessageBubble.js
CHANGED
@@ -1,136 +1,285 @@
|
|
1 |
-
import React from 'react';
|
2 |
import { motion } from 'framer-motion';
|
3 |
import ReactMarkdown from 'react-markdown';
|
4 |
-
import remarkGfm from 'remark-gfm';
|
5 |
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
6 |
-
import {
|
7 |
-
import
|
|
|
8 |
|
9 |
-
const MessageBubble = ({ message, darkMode, isLast }) => {
|
10 |
-
const
|
11 |
-
|
12 |
-
const
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
ease: "easeOut"
|
20 |
-
}
|
21 |
}
|
22 |
};
|
23 |
|
24 |
-
const
|
25 |
-
|
|
|
26 |
hour: '2-digit',
|
27 |
-
minute: '2-digit'
|
|
|
28 |
});
|
29 |
};
|
30 |
|
31 |
return (
|
32 |
<motion.div
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
className={`flex
|
37 |
>
|
38 |
-
{
|
39 |
-
|
40 |
-
|
41 |
-
}
|
42 |
-
|
43 |
-
|
44 |
-
)}
|
45 |
-
|
46 |
-
<div className={`max-w-[80%] ${isUser ? 'order-first' : ''}`}>
|
47 |
-
<div className={`rounded-2xl px-4 py-3 ${
|
48 |
-
isUser
|
49 |
-
? darkMode
|
50 |
-
? 'bg-primary-600 text-white'
|
51 |
-
: 'bg-primary-500 text-white'
|
52 |
-
: darkMode
|
53 |
-
? 'bg-gray-800 border border-gray-700'
|
54 |
-
: 'bg-white border border-gray-200 shadow-sm'
|
55 |
}`}>
|
56 |
-
{
|
57 |
-
<
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
<SyntaxHighlighter
|
67 |
-
style={darkMode ? tomorrow : prism}
|
68 |
-
language={match[1]}
|
69 |
-
PreTag="div"
|
70 |
-
{...props}
|
71 |
-
>
|
72 |
-
{String(children).replace(/\n$/, '')}
|
73 |
-
</SyntaxHighlighter>
|
74 |
-
) : (
|
75 |
-
<code className={className} {...props}>
|
76 |
-
{children}
|
77 |
-
</code>
|
78 |
-
);
|
79 |
-
},
|
80 |
-
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
|
81 |
-
ul: ({ children }) => <ul className="list-disc list-inside mb-2">{children}</ul>,
|
82 |
-
ol: ({ children }) => <ol className="list-decimal list-inside mb-2">{children}</ol>,
|
83 |
-
li: ({ children }) => <li className="mb-1">{children}</li>,
|
84 |
-
h1: ({ children }) => <h1 className="text-xl font-bold mb-2">{children}</h1>,
|
85 |
-
h2: ({ children }) => <h2 className="text-lg font-semibold mb-2">{children}</h2>,
|
86 |
-
h3: ({ children }) => <h3 className="text-md font-medium mb-2">{children}</h3>,
|
87 |
-
blockquote: ({ children }) => (
|
88 |
-
<blockquote className={`border-l-4 pl-4 italic my-2 ${
|
89 |
-
darkMode ? 'border-gray-600 text-gray-300' : 'border-gray-300 text-gray-600'
|
90 |
-
}`}>
|
91 |
-
{children}
|
92 |
-
</blockquote>
|
93 |
-
),
|
94 |
-
}}
|
95 |
-
>
|
96 |
-
{message.content}
|
97 |
-
</ReactMarkdown>
|
98 |
</div>
|
99 |
)}
|
100 |
-
</div>
|
101 |
-
|
102 |
-
{/* Timestamp and Sources */}
|
103 |
-
<div className={`text-xs mt-2 ${
|
104 |
-
darkMode ? 'text-gray-500' : 'text-gray-400'
|
105 |
-
} ${isUser ? 'text-right' : 'text-left'}`}>
|
106 |
-
<span>{formatTime(message.timestamp)}</span>
|
107 |
|
108 |
-
{
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
</div>
|
121 |
)}
|
122 |
</div>
|
123 |
-
</div>
|
124 |
|
125 |
-
|
126 |
-
<div className={`
|
127 |
-
|
128 |
}`}>
|
129 |
-
<
|
130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
}`} />
|
132 |
</div>
|
133 |
-
|
134 |
</motion.div>
|
135 |
);
|
136 |
};
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
import { motion } from 'framer-motion';
|
3 |
import ReactMarkdown from 'react-markdown';
|
|
|
4 |
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
5 |
+
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
6 |
+
import remarkGfm from 'remark-gfm';
|
7 |
+
import { ClipboardIcon, CheckIcon } from '@heroicons/react/24/outline';
|
8 |
|
9 |
+
const MessageBubble = ({ message, darkMode, isLast = false }) => {
|
10 |
+
const [copied, setCopied] = useState(false);
|
11 |
+
|
12 |
+
const copyToClipboard = async (text) => {
|
13 |
+
try {
|
14 |
+
await navigator.clipboard.writeText(text);
|
15 |
+
setCopied(true);
|
16 |
+
setTimeout(() => setCopied(false), 2000);
|
17 |
+
} catch (err) {
|
18 |
+
console.error('Failed to copy text: ', err);
|
|
|
|
|
19 |
}
|
20 |
};
|
21 |
|
22 |
+
const formatTimestamp = (timestamp) => {
|
23 |
+
const date = new Date(timestamp);
|
24 |
+
return date.toLocaleTimeString('en-US', {
|
25 |
hour: '2-digit',
|
26 |
+
minute: '2-digit',
|
27 |
+
hour12: false
|
28 |
});
|
29 |
};
|
30 |
|
31 |
return (
|
32 |
<motion.div
|
33 |
+
initial={{ opacity: 0, y: 20 }}
|
34 |
+
animate={{ opacity: 1, y: 0 }}
|
35 |
+
transition={{ duration: 0.3 }}
|
36 |
+
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} mb-4 md:mb-6 px-2 md:px-4`}
|
37 |
>
|
38 |
+
<div className={`max-w-[85%] md:max-w-[80%] lg:max-w-[70%] ${
|
39 |
+
message.role === 'user' ? 'order-2' : 'order-1'
|
40 |
+
}`}>
|
41 |
+
{/* Avatar and Timestamp - Mobile optimized */}
|
42 |
+
<div className={`flex items-center mb-2 ${
|
43 |
+
message.role === 'user' ? 'justify-end' : 'justify-start'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
}`}>
|
45 |
+
{message.role === 'assistant' && (
|
46 |
+
<div className={`w-6 h-6 md:w-8 md:h-8 rounded-full flex items-center justify-center mr-2 md:mr-3 flex-shrink-0 ${
|
47 |
+
darkMode
|
48 |
+
? 'bg-gradient-to-br from-primary-600 to-purple-600'
|
49 |
+
: 'bg-gradient-to-br from-primary-500 to-purple-500'
|
50 |
+
}`}>
|
51 |
+
<svg className="w-3 h-3 md:w-4 md:h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
52 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
53 |
+
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
54 |
+
</svg>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
</div>
|
56 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
|
58 |
+
<span className={`text-xs md:text-sm font-medium ${
|
59 |
+
darkMode ? 'text-gray-400' : 'text-gray-500'
|
60 |
+
} ${message.role === 'user' ? 'order-2' : 'order-1'}`}>
|
61 |
+
{message.role === 'user' ? 'You' : 'CA Assistant'}
|
62 |
+
</span>
|
63 |
+
|
64 |
+
<span className={`text-xs ${
|
65 |
+
darkMode ? 'text-gray-500' : 'text-gray-400'
|
66 |
+
} ml-2 ${message.role === 'user' ? 'order-1 mr-2 ml-0' : 'order-2'}`}>
|
67 |
+
{formatTimestamp(message.timestamp)}
|
68 |
+
</span>
|
69 |
+
|
70 |
+
{message.role === 'user' && (
|
71 |
+
<div className={`w-6 h-6 md:w-8 md:h-8 rounded-full flex items-center justify-center ml-2 md:ml-3 flex-shrink-0 ${
|
72 |
+
darkMode
|
73 |
+
? 'bg-gradient-to-br from-blue-600 to-blue-700'
|
74 |
+
: 'bg-gradient-to-br from-blue-500 to-blue-600'
|
75 |
+
}`}>
|
76 |
+
<svg className="w-3 h-3 md:w-4 md:h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
77 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
78 |
+
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
79 |
+
</svg>
|
80 |
</div>
|
81 |
)}
|
82 |
</div>
|
|
|
83 |
|
84 |
+
{/* Message Content - Mobile optimized */}
|
85 |
+
<div className={`relative group ${
|
86 |
+
message.role === 'user' ? 'text-right' : 'text-left'
|
87 |
}`}>
|
88 |
+
<div className={`inline-block p-3 md:p-4 rounded-2xl md:rounded-2xl relative shadow-md hover:shadow-lg transition-all duration-200 ${
|
89 |
+
message.role === 'user'
|
90 |
+
? darkMode
|
91 |
+
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white'
|
92 |
+
: 'bg-gradient-to-br from-blue-500 to-blue-600 text-white'
|
93 |
+
: darkMode
|
94 |
+
? 'bg-gradient-to-br from-gray-800 to-gray-850 border border-gray-700/50 text-gray-100'
|
95 |
+
: 'bg-gradient-to-br from-white to-gray-50 border border-gray-200/50 text-gray-900'
|
96 |
+
} max-w-full`}>
|
97 |
+
|
98 |
+
{/* Message Text - Mobile optimized */}
|
99 |
+
<div className={`prose prose-sm md:prose-base max-w-none ${
|
100 |
+
message.role === 'user'
|
101 |
+
? 'prose-invert'
|
102 |
+
: darkMode
|
103 |
+
? 'prose-invert prose-gray'
|
104 |
+
: 'prose-gray'
|
105 |
+
} ${message.role === 'user' ? 'text-left' : ''}`}>
|
106 |
+
{message.role === 'user' ? (
|
107 |
+
<p className="mb-0 text-sm md:text-base leading-relaxed break-words">{message.content}</p>
|
108 |
+
) : (
|
109 |
+
<ReactMarkdown
|
110 |
+
remarkPlugins={[remarkGfm]}
|
111 |
+
components={{
|
112 |
+
// Mobile-optimized code blocks
|
113 |
+
code({ node, inline, className, children, ...props }) {
|
114 |
+
const match = /language-(\w+)/.exec(className || '');
|
115 |
+
return !inline && match ? (
|
116 |
+
<div className="relative my-3 md:my-4 rounded-lg md:rounded-xl overflow-hidden">
|
117 |
+
<div className={`flex items-center justify-between px-3 md:px-4 py-2 md:py-3 text-xs md:text-sm ${
|
118 |
+
darkMode ? 'bg-gray-900 text-gray-300' : 'bg-gray-800 text-gray-200'
|
119 |
+
}`}>
|
120 |
+
<span className="font-medium">{match[1]}</span>
|
121 |
+
<button
|
122 |
+
onClick={() => copyToClipboard(String(children).replace(/\n$/, ''))}
|
123 |
+
className={`flex items-center space-x-1 md:space-x-2 px-2 md:px-3 py-1 md:py-1.5 rounded-md transition-colors touch-manipulation ${
|
124 |
+
darkMode
|
125 |
+
? 'hover:bg-gray-800 active:bg-gray-700'
|
126 |
+
: 'hover:bg-gray-700 active:bg-gray-600'
|
127 |
+
}`}
|
128 |
+
title="Copy code"
|
129 |
+
>
|
130 |
+
{copied ? (
|
131 |
+
<CheckIcon className="w-3 h-3 md:w-4 md:h-4" />
|
132 |
+
) : (
|
133 |
+
<ClipboardIcon className="w-3 h-3 md:w-4 md:h-4" />
|
134 |
+
)}
|
135 |
+
<span className="text-xs hidden md:inline">
|
136 |
+
{copied ? 'Copied!' : 'Copy'}
|
137 |
+
</span>
|
138 |
+
</button>
|
139 |
+
</div>
|
140 |
+
<SyntaxHighlighter
|
141 |
+
style={darkMode ? oneDark : oneLight}
|
142 |
+
language={match[1]}
|
143 |
+
PreTag="div"
|
144 |
+
className="!m-0 text-xs md:text-sm"
|
145 |
+
customStyle={{
|
146 |
+
fontSize: '12px',
|
147 |
+
lineHeight: '1.4',
|
148 |
+
padding: '12px 16px',
|
149 |
+
}}
|
150 |
+
{...props}
|
151 |
+
>
|
152 |
+
{String(children).replace(/\n$/, '')}
|
153 |
+
</SyntaxHighlighter>
|
154 |
+
</div>
|
155 |
+
) : (
|
156 |
+
<code
|
157 |
+
className={`px-1.5 md:px-2 py-0.5 md:py-1 rounded text-xs md:text-sm font-mono ${
|
158 |
+
darkMode
|
159 |
+
? 'bg-gray-700 text-gray-200'
|
160 |
+
: 'bg-gray-200 text-gray-800'
|
161 |
+
}`}
|
162 |
+
{...props}
|
163 |
+
>
|
164 |
+
{children}
|
165 |
+
</code>
|
166 |
+
);
|
167 |
+
},
|
168 |
+
// Mobile-optimized paragraphs
|
169 |
+
p: ({ children }) => (
|
170 |
+
<p className="mb-3 md:mb-4 last:mb-0 text-sm md:text-base leading-relaxed break-words">
|
171 |
+
{children}
|
172 |
+
</p>
|
173 |
+
),
|
174 |
+
// Mobile-optimized lists
|
175 |
+
ul: ({ children }) => (
|
176 |
+
<ul className="mb-3 md:mb-4 ml-4 md:ml-6 space-y-1 md:space-y-2 text-sm md:text-base">
|
177 |
+
{children}
|
178 |
+
</ul>
|
179 |
+
),
|
180 |
+
ol: ({ children }) => (
|
181 |
+
<ol className="mb-3 md:mb-4 ml-4 md:ml-6 space-y-1 md:space-y-2 text-sm md:text-base">
|
182 |
+
{children}
|
183 |
+
</ol>
|
184 |
+
),
|
185 |
+
li: ({ children }) => (
|
186 |
+
<li className="leading-relaxed break-words">
|
187 |
+
{children}
|
188 |
+
</li>
|
189 |
+
),
|
190 |
+
// Mobile-optimized headings
|
191 |
+
h1: ({ children }) => (
|
192 |
+
<h1 className="text-lg md:text-xl font-bold mb-2 md:mb-3 mt-4 md:mt-6 first:mt-0 break-words">
|
193 |
+
{children}
|
194 |
+
</h1>
|
195 |
+
),
|
196 |
+
h2: ({ children }) => (
|
197 |
+
<h2 className="text-base md:text-lg font-bold mb-2 md:mb-3 mt-3 md:mt-4 first:mt-0 break-words">
|
198 |
+
{children}
|
199 |
+
</h2>
|
200 |
+
),
|
201 |
+
h3: ({ children }) => (
|
202 |
+
<h3 className="text-sm md:text-base font-bold mb-1 md:mb-2 mt-2 md:mt-3 first:mt-0 break-words">
|
203 |
+
{children}
|
204 |
+
</h3>
|
205 |
+
),
|
206 |
+
// Mobile-optimized blockquotes
|
207 |
+
blockquote: ({ children }) => (
|
208 |
+
<blockquote className={`border-l-3 md:border-l-4 pl-3 md:pl-4 my-3 md:my-4 italic text-sm md:text-base ${
|
209 |
+
darkMode ? 'border-gray-600 text-gray-300' : 'border-gray-400 text-gray-600'
|
210 |
+
}`}>
|
211 |
+
{children}
|
212 |
+
</blockquote>
|
213 |
+
),
|
214 |
+
// Mobile-optimized tables
|
215 |
+
table: ({ children }) => (
|
216 |
+
<div className="overflow-x-auto my-3 md:my-4 -mx-1">
|
217 |
+
<table className={`min-w-full text-xs md:text-sm border-collapse ${
|
218 |
+
darkMode ? 'border-gray-600' : 'border-gray-300'
|
219 |
+
}`}>
|
220 |
+
{children}
|
221 |
+
</table>
|
222 |
+
</div>
|
223 |
+
),
|
224 |
+
th: ({ children }) => (
|
225 |
+
<th className={`border px-2 md:px-3 py-1 md:py-2 font-medium text-left ${
|
226 |
+
darkMode
|
227 |
+
? 'border-gray-600 bg-gray-700/50'
|
228 |
+
: 'border-gray-300 bg-gray-100'
|
229 |
+
}`}>
|
230 |
+
{children}
|
231 |
+
</th>
|
232 |
+
),
|
233 |
+
td: ({ children }) => (
|
234 |
+
<td className={`border px-2 md:px-3 py-1 md:py-2 ${
|
235 |
+
darkMode ? 'border-gray-600' : 'border-gray-300'
|
236 |
+
}`}>
|
237 |
+
{children}
|
238 |
+
</td>
|
239 |
+
),
|
240 |
+
}}
|
241 |
+
>
|
242 |
+
{message.content || '*Thinking...*'}
|
243 |
+
</ReactMarkdown>
|
244 |
+
)}
|
245 |
+
</div>
|
246 |
+
|
247 |
+
{/* Copy Button for Assistant Messages - Mobile optimized */}
|
248 |
+
{message.role === 'assistant' && message.content && (
|
249 |
+
<button
|
250 |
+
onClick={() => copyToClipboard(message.content)}
|
251 |
+
className={`absolute top-2 md:top-3 right-2 md:right-3 opacity-0 group-hover:opacity-100 transition-all duration-200 p-1.5 md:p-2 rounded-lg touch-manipulation ${
|
252 |
+
darkMode
|
253 |
+
? 'hover:bg-gray-700/70 active:bg-gray-600/70 text-gray-400 hover:text-gray-200'
|
254 |
+
: 'hover:bg-gray-200/70 active:bg-gray-300/70 text-gray-500 hover:text-gray-700'
|
255 |
+
} backdrop-blur-sm`}
|
256 |
+
title="Copy message"
|
257 |
+
>
|
258 |
+
{copied ? (
|
259 |
+
<CheckIcon className="w-3 h-3 md:w-4 md:h-4" />
|
260 |
+
) : (
|
261 |
+
<ClipboardIcon className="w-3 h-3 md:w-4 md:h-4" />
|
262 |
+
)}
|
263 |
+
</button>
|
264 |
+
)}
|
265 |
+
</div>
|
266 |
+
|
267 |
+
{/* Message tail/pointer - Mobile optimized */}
|
268 |
+
<div className={`absolute top-3 md:top-4 w-0 h-0 ${
|
269 |
+
message.role === 'user'
|
270 |
+
? 'right-0 border-l-8 md:border-l-10 border-t-8 md:border-t-10 border-transparent'
|
271 |
+
: 'left-0 border-r-8 md:border-r-10 border-t-8 md:border-t-10 border-transparent'
|
272 |
+
} ${
|
273 |
+
message.role === 'user'
|
274 |
+
? darkMode
|
275 |
+
? 'border-t-blue-600'
|
276 |
+
: 'border-t-blue-500'
|
277 |
+
: darkMode
|
278 |
+
? 'border-t-gray-800'
|
279 |
+
: 'border-t-white'
|
280 |
}`} />
|
281 |
</div>
|
282 |
+
</div>
|
283 |
</motion.div>
|
284 |
);
|
285 |
};
|
frontend/src/components/Sidebar.js
CHANGED
@@ -2,10 +2,11 @@ import React from 'react';
|
|
2 |
import { motion, AnimatePresence } from 'framer-motion';
|
3 |
import {
|
4 |
PlusIcon,
|
5 |
-
|
|
|
6 |
ChatBubbleLeftIcon,
|
7 |
-
|
8 |
-
|
9 |
} from '@heroicons/react/24/outline';
|
10 |
|
11 |
const Sidebar = ({
|
@@ -19,197 +20,271 @@ const Sidebar = ({
|
|
19 |
onBackToHome,
|
20 |
darkMode
|
21 |
}) => {
|
|
|
22 |
const formatDate = (date) => {
|
23 |
const now = new Date();
|
24 |
const messageDate = new Date(date);
|
25 |
const diffTime = Math.abs(now - messageDate);
|
26 |
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
27 |
-
|
28 |
if (diffDays === 1) return 'Today';
|
29 |
if (diffDays === 2) return 'Yesterday';
|
30 |
-
if (diffDays <= 7) return `${diffDays} days ago`;
|
31 |
return messageDate.toLocaleDateString();
|
32 |
};
|
33 |
|
34 |
-
const
|
35 |
-
|
36 |
-
|
37 |
-
transition: {
|
38 |
-
type: "spring",
|
39 |
-
stiffness: 300,
|
40 |
-
damping: 30
|
41 |
-
}
|
42 |
-
},
|
43 |
-
closed: {
|
44 |
-
x: -280,
|
45 |
-
transition: {
|
46 |
-
type: "spring",
|
47 |
-
stiffness: 300,
|
48 |
-
damping: 30
|
49 |
-
}
|
50 |
-
}
|
51 |
-
};
|
52 |
-
|
53 |
-
const overlayVariants = {
|
54 |
-
open: { opacity: 1 },
|
55 |
-
closed: { opacity: 0 }
|
56 |
};
|
57 |
|
58 |
return (
|
59 |
-
|
60 |
-
{
|
61 |
-
|
62 |
-
|
63 |
<motion.div
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
exit="closed"
|
68 |
className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
|
69 |
onClick={onClose}
|
70 |
/>
|
71 |
-
)}
|
72 |
-
</AnimatePresence>
|
73 |
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
onClick={onBackToHome}
|
120 |
-
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
|
121 |
-
darkMode
|
122 |
-
? 'bg-primary-600 hover:bg-primary-700 text-white'
|
123 |
-
: 'bg-primary-50 hover:bg-primary-100 text-primary-900 border-primary-300'
|
124 |
-
} border`}
|
125 |
-
>
|
126 |
-
<HomeIcon className="w-4 h-4" />
|
127 |
-
<span className="font-medium">Back to Home</span>
|
128 |
-
</motion.button>
|
129 |
-
</div>
|
130 |
-
</div>
|
131 |
|
132 |
-
{/* Conversations List */}
|
133 |
-
<div className="flex-1 overflow-y-auto p-2">
|
134 |
-
{conversations.length === 0 ? (
|
135 |
-
<div className={`text-center py-8 ${
|
136 |
-
darkMode ? 'text-gray-500' : 'text-gray-400'
|
137 |
-
}`}>
|
138 |
-
<ChatBubbleLeftIcon className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
139 |
-
<p className="text-sm">No conversations yet</p>
|
140 |
-
<p className="text-xs mt-1">Start a new chat to begin</p>
|
141 |
-
</div>
|
142 |
-
) : (
|
143 |
-
<div className="space-y-1">
|
144 |
-
{conversations.map((conversation) => (
|
145 |
<motion.button
|
146 |
-
|
147 |
-
|
148 |
onClick={() => {
|
149 |
-
|
150 |
onClose();
|
151 |
}}
|
152 |
-
className={`
|
153 |
-
|
154 |
-
?
|
155 |
-
|
156 |
-
|
157 |
-
: darkMode
|
158 |
-
? 'hover:bg-gray-800 text-gray-300'
|
159 |
-
: 'hover:bg-gray-50 text-gray-700'
|
160 |
-
}`}
|
161 |
>
|
162 |
-
<
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
170 |
: darkMode
|
171 |
-
? '
|
172 |
-
: '
|
173 |
-
}
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
onClick={(e) => {
|
180 |
-
e.stopPropagation();
|
181 |
-
if (window.confirm('Are you sure you want to delete this conversation?')) {
|
182 |
-
onDeleteConversation(conversation.id);
|
183 |
-
}
|
184 |
-
}}
|
185 |
-
className={`opacity-0 group-hover:opacity-100 p-1 rounded transition-opacity ${
|
186 |
-
darkMode
|
187 |
-
? 'hover:bg-gray-700 text-gray-400'
|
188 |
-
: 'hover:bg-gray-200 text-gray-500'
|
189 |
}`}
|
|
|
|
|
|
|
|
|
190 |
>
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
</div>
|
197 |
-
)}
|
198 |
-
</div>
|
199 |
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
|
|
|
|
213 |
);
|
214 |
};
|
215 |
|
|
|
2 |
import { motion, AnimatePresence } from 'framer-motion';
|
3 |
import {
|
4 |
PlusIcon,
|
5 |
+
TrashIcon,
|
6 |
+
XMarkIcon,
|
7 |
ChatBubbleLeftIcon,
|
8 |
+
HomeIcon,
|
9 |
+
Bars3Icon
|
10 |
} from '@heroicons/react/24/outline';
|
11 |
|
12 |
const Sidebar = ({
|
|
|
20 |
onBackToHome,
|
21 |
darkMode
|
22 |
}) => {
|
23 |
+
|
24 |
const formatDate = (date) => {
|
25 |
const now = new Date();
|
26 |
const messageDate = new Date(date);
|
27 |
const diffTime = Math.abs(now - messageDate);
|
28 |
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
29 |
+
|
30 |
if (diffDays === 1) return 'Today';
|
31 |
if (diffDays === 2) return 'Yesterday';
|
32 |
+
if (diffDays <= 7) return `${diffDays - 1} days ago`;
|
33 |
return messageDate.toLocaleDateString();
|
34 |
};
|
35 |
|
36 |
+
const truncateTitle = (title, maxLength = 25) => {
|
37 |
+
if (title.length <= maxLength) return title;
|
38 |
+
return title.substring(0, maxLength) + '...';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
};
|
40 |
|
41 |
return (
|
42 |
+
<AnimatePresence>
|
43 |
+
{open && (
|
44 |
+
<>
|
45 |
+
{/* Mobile: Full-screen overlay background */}
|
46 |
<motion.div
|
47 |
+
initial={{ opacity: 0 }}
|
48 |
+
animate={{ opacity: 1 }}
|
49 |
+
exit={{ opacity: 0 }}
|
|
|
50 |
className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
|
51 |
onClick={onClose}
|
52 |
/>
|
|
|
|
|
53 |
|
54 |
+
{/* Sidebar Content */}
|
55 |
+
<motion.div
|
56 |
+
initial={{
|
57 |
+
x: '-100%',
|
58 |
+
opacity: 0
|
59 |
+
}}
|
60 |
+
animate={{
|
61 |
+
x: 0,
|
62 |
+
opacity: 1
|
63 |
+
}}
|
64 |
+
exit={{
|
65 |
+
x: '-100%',
|
66 |
+
opacity: 0
|
67 |
+
}}
|
68 |
+
transition={{
|
69 |
+
type: "spring",
|
70 |
+
damping: 25,
|
71 |
+
stiffness: 200
|
72 |
+
}}
|
73 |
+
className={`fixed top-0 left-0 h-full w-full md:w-80 z-50 md:z-30 ${
|
74 |
+
darkMode
|
75 |
+
? 'bg-gray-900 border-gray-700'
|
76 |
+
: 'bg-white border-gray-200'
|
77 |
+
} border-r shadow-2xl md:shadow-xl flex flex-col`}
|
78 |
+
>
|
79 |
+
{/* Header - Mobile optimized */}
|
80 |
+
<div className={`p-4 md:p-6 border-b ${
|
81 |
+
darkMode ? 'border-gray-700/50' : 'border-gray-200/50'
|
82 |
+
} flex-shrink-0`}>
|
83 |
+
{/* Top row: Close button and title */}
|
84 |
+
<div className="flex items-center justify-between mb-4 md:mb-3">
|
85 |
+
<h2 className={`text-lg md:text-xl font-bold ${
|
86 |
+
darkMode ? 'text-white' : 'text-gray-900'
|
87 |
+
}`}>
|
88 |
+
Conversations
|
89 |
+
</h2>
|
90 |
+
|
91 |
+
<button
|
92 |
+
onClick={onClose}
|
93 |
+
className={`p-2 md:p-1.5 rounded-lg transition-colors touch-manipulation ${
|
94 |
+
darkMode
|
95 |
+
? 'hover:bg-gray-800 active:bg-gray-700 text-gray-400'
|
96 |
+
: 'hover:bg-gray-100 active:bg-gray-200 text-gray-500'
|
97 |
+
}`}
|
98 |
+
title="Close sidebar"
|
99 |
+
>
|
100 |
+
<XMarkIcon className="w-6 h-6 md:w-5 md:h-5" />
|
101 |
+
</button>
|
102 |
+
</div>
|
103 |
|
104 |
+
{/* Action buttons - Mobile optimized */}
|
105 |
+
<div className="flex flex-col sm:flex-row gap-2 md:gap-3">
|
106 |
+
<motion.button
|
107 |
+
whileHover={{ scale: 1.02 }}
|
108 |
+
whileTap={{ scale: 0.98 }}
|
109 |
+
onClick={() => {
|
110 |
+
onNewChat();
|
111 |
+
onClose();
|
112 |
+
}}
|
113 |
+
className={`flex items-center justify-center gap-2 md:gap-3 px-4 py-3 md:py-2.5 rounded-xl md:rounded-lg font-medium transition-all touch-manipulation ${
|
114 |
+
darkMode
|
115 |
+
? 'bg-primary-600 hover:bg-primary-700 active:bg-primary-800 text-white shadow-lg'
|
116 |
+
: 'bg-primary-500 hover:bg-primary-600 active:bg-primary-700 text-white shadow-lg'
|
117 |
+
} hover:shadow-xl active:shadow-2xl flex-1 sm:flex-none`}
|
118 |
+
>
|
119 |
+
<PlusIcon className="w-5 h-5 md:w-4 md:h-4" />
|
120 |
+
<span className="text-base md:text-sm">New Chat</span>
|
121 |
+
</motion.button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
<motion.button
|
124 |
+
whileHover={{ scale: 1.02 }}
|
125 |
+
whileTap={{ scale: 0.98 }}
|
126 |
onClick={() => {
|
127 |
+
onBackToHome();
|
128 |
onClose();
|
129 |
}}
|
130 |
+
className={`flex items-center justify-center gap-2 md:gap-3 px-4 py-3 md:py-2.5 rounded-xl md:rounded-lg font-medium transition-all touch-manipulation ${
|
131 |
+
darkMode
|
132 |
+
? 'bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-gray-200 shadow-lg'
|
133 |
+
: 'bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 shadow-lg'
|
134 |
+
} hover:shadow-xl active:shadow-2xl flex-1 sm:flex-none`}
|
|
|
|
|
|
|
|
|
135 |
>
|
136 |
+
<HomeIcon className="w-5 h-5 md:w-4 md:h-4" />
|
137 |
+
<span className="text-base md:text-sm">Home</span>
|
138 |
+
</motion.button>
|
139 |
+
</div>
|
140 |
+
</div>
|
141 |
+
|
142 |
+
{/* Conversations List - Mobile optimized */}
|
143 |
+
<div className="flex-1 overflow-y-auto p-3 md:p-4">
|
144 |
+
{conversations.length === 0 ? (
|
145 |
+
<div className="flex flex-col items-center justify-center h-full text-center px-4">
|
146 |
+
<ChatBubbleLeftIcon className={`w-12 h-12 md:w-16 md:h-16 mb-4 ${
|
147 |
+
darkMode ? 'text-gray-600' : 'text-gray-400'
|
148 |
+
}`} />
|
149 |
+
<p className={`text-base md:text-lg font-medium mb-2 ${
|
150 |
+
darkMode ? 'text-gray-400' : 'text-gray-500'
|
151 |
+
}`}>
|
152 |
+
No conversations yet
|
153 |
+
</p>
|
154 |
+
<p className={`text-sm md:text-base ${
|
155 |
+
darkMode ? 'text-gray-500' : 'text-gray-400'
|
156 |
+
}`}>
|
157 |
+
Start a new chat to begin your CA study session
|
158 |
+
</p>
|
159 |
+
</div>
|
160 |
+
) : (
|
161 |
+
<div className="space-y-2 md:space-y-1">
|
162 |
+
{conversations.map((conv) => (
|
163 |
+
<motion.div
|
164 |
+
key={conv.id}
|
165 |
+
initial={{ opacity: 0, x: -20 }}
|
166 |
+
animate={{ opacity: 1, x: 0 }}
|
167 |
+
whileHover={{ x: 4 }}
|
168 |
+
className={`group relative p-3 md:p-3 rounded-xl md:rounded-lg cursor-pointer transition-all touch-manipulation ${
|
169 |
+
activeConversationId === conv.id
|
170 |
+
? darkMode
|
171 |
+
? 'bg-primary-600/20 border-primary-500/30 shadow-lg'
|
172 |
+
: 'bg-primary-50 border-primary-200 shadow-lg'
|
173 |
: darkMode
|
174 |
+
? 'hover:bg-gray-800 active:bg-gray-700'
|
175 |
+
: 'hover:bg-gray-50 active:bg-gray-100'
|
176 |
+
} border ${
|
177 |
+
activeConversationId === conv.id
|
178 |
+
? ''
|
179 |
+
: darkMode
|
180 |
+
? 'border-transparent'
|
181 |
+
: 'border-transparent'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
}`}
|
183 |
+
onClick={() => {
|
184 |
+
onConversationSelect(conv.id);
|
185 |
+
onClose();
|
186 |
+
}}
|
187 |
>
|
188 |
+
{/* Conversation Content */}
|
189 |
+
<div className="flex items-start justify-between">
|
190 |
+
<div className="flex-1 min-w-0 mr-2">
|
191 |
+
{/* Title */}
|
192 |
+
<h3 className={`font-medium text-sm md:text-sm mb-1 truncate ${
|
193 |
+
activeConversationId === conv.id
|
194 |
+
? darkMode
|
195 |
+
? 'text-primary-300'
|
196 |
+
: 'text-primary-700'
|
197 |
+
: darkMode
|
198 |
+
? 'text-gray-200'
|
199 |
+
: 'text-gray-900'
|
200 |
+
}`}>
|
201 |
+
{truncateTitle(conv.title || 'New Conversation')}
|
202 |
+
</h3>
|
203 |
+
|
204 |
+
{/* Message count and date */}
|
205 |
+
<div className="flex items-center space-x-2">
|
206 |
+
<span className={`text-xs ${
|
207 |
+
activeConversationId === conv.id
|
208 |
+
? darkMode
|
209 |
+
? 'text-primary-400/80'
|
210 |
+
: 'text-primary-600/80'
|
211 |
+
: darkMode
|
212 |
+
? 'text-gray-500'
|
213 |
+
: 'text-gray-500'
|
214 |
+
}`}>
|
215 |
+
{conv.messages?.length || 0} messages
|
216 |
+
</span>
|
217 |
+
<span className={`text-xs ${
|
218 |
+
darkMode ? 'text-gray-600' : 'text-gray-400'
|
219 |
+
}`}>
|
220 |
+
•
|
221 |
+
</span>
|
222 |
+
<span className={`text-xs ${
|
223 |
+
darkMode ? 'text-gray-500' : 'text-gray-400'
|
224 |
+
}`}>
|
225 |
+
{formatDate(conv.createdAt)}
|
226 |
+
</span>
|
227 |
+
</div>
|
228 |
+
|
229 |
+
{/* Last message preview - Mobile optimized */}
|
230 |
+
{conv.messages && conv.messages.length > 0 && (
|
231 |
+
<p className={`text-xs mt-1 truncate ${
|
232 |
+
darkMode ? 'text-gray-500' : 'text-gray-500'
|
233 |
+
}`}>
|
234 |
+
{conv.messages[conv.messages.length - 1].content.substring(0, 40)}...
|
235 |
+
</p>
|
236 |
+
)}
|
237 |
+
</div>
|
238 |
+
|
239 |
+
{/* Delete Button - Larger touch target for mobile */}
|
240 |
+
<motion.button
|
241 |
+
whileHover={{ scale: 1.1 }}
|
242 |
+
whileTap={{ scale: 0.9 }}
|
243 |
+
onClick={(e) => {
|
244 |
+
e.stopPropagation();
|
245 |
+
onDeleteConversation(conv.id);
|
246 |
+
}}
|
247 |
+
className={`p-2 md:p-1.5 rounded-lg opacity-0 md:group-hover:opacity-100 transition-all touch-manipulation ${
|
248 |
+
darkMode
|
249 |
+
? 'hover:bg-red-600/20 active:bg-red-600/30 text-red-400'
|
250 |
+
: 'hover:bg-red-50 active:bg-red-100 text-red-500'
|
251 |
+
} flex-shrink-0 md:opacity-100 sm:opacity-100`}
|
252 |
+
title="Delete conversation"
|
253 |
+
>
|
254 |
+
<TrashIcon className="w-4 h-4 md:w-4 md:h-4" />
|
255 |
+
</motion.button>
|
256 |
+
</div>
|
257 |
+
|
258 |
+
{/* Active indicator */}
|
259 |
+
{activeConversationId === conv.id && (
|
260 |
+
<motion.div
|
261 |
+
layoutId="activeConversation"
|
262 |
+
className={`absolute left-0 top-0 bottom-0 w-1 rounded-r ${
|
263 |
+
darkMode ? 'bg-primary-500' : 'bg-primary-500'
|
264 |
+
}`}
|
265 |
+
/>
|
266 |
+
)}
|
267 |
+
</motion.div>
|
268 |
+
))}
|
269 |
+
</div>
|
270 |
+
)}
|
271 |
</div>
|
|
|
|
|
272 |
|
273 |
+
{/* Footer - Mobile optimized */}
|
274 |
+
<div className={`p-4 md:p-6 border-t ${
|
275 |
+
darkMode ? 'border-gray-700/50' : 'border-gray-200/50'
|
276 |
+
} flex-shrink-0`}>
|
277 |
+
<div className={`text-center text-xs ${
|
278 |
+
darkMode ? 'text-gray-500' : 'text-gray-400'
|
279 |
+
}`}>
|
280 |
+
<p className="mb-1">📚 CA Study Assistant</p>
|
281 |
+
<p>{conversations.length} conversation{conversations.length !== 1 ? 's' : ''}</p>
|
282 |
+
</div>
|
283 |
+
</div>
|
284 |
+
</motion.div>
|
285 |
+
</>
|
286 |
+
)}
|
287 |
+
</AnimatePresence>
|
288 |
);
|
289 |
};
|
290 |
|