Spaces:
Sleeping
Sleeping
Update web/src/App.tsx
Browse files- web/src/App.tsx +217 -194
web/src/App.tsx
CHANGED
|
@@ -7,9 +7,10 @@ import { RightPanel } from './components/RightPanel';
|
|
| 7 |
import { FloatingActionButtons } from './components/FloatingActionButtons';
|
| 8 |
import { LoginScreen } from './components/LoginScreen';
|
| 9 |
import { ProfileEditor } from './components/ProfileEditor';
|
| 10 |
-
import { X
|
| 11 |
import { Button } from './components/ui/button';
|
| 12 |
import { Toaster } from './components/ui/sonner';
|
|
|
|
| 13 |
import { toast } from 'sonner';
|
| 14 |
|
| 15 |
import {
|
|
@@ -30,7 +31,7 @@ export interface Message {
|
|
| 30 |
content: string;
|
| 31 |
timestamp: Date;
|
| 32 |
references?: string[];
|
| 33 |
-
sender?: GroupMember;
|
| 34 |
}
|
| 35 |
|
| 36 |
export interface User {
|
|
@@ -82,6 +83,8 @@ function App() {
|
|
| 82 |
const [learningMode, setLearningMode] = useState<LearningMode>('concept');
|
| 83 |
const [language, setLanguage] = useState<Language>('auto');
|
| 84 |
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
|
|
|
|
|
|
| 85 |
const [memoryProgress] = useState(36);
|
| 86 |
|
| 87 |
const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
|
|
@@ -95,6 +98,7 @@ function App() {
|
|
| 95 |
const [exportResult, setExportResult] = useState('');
|
| 96 |
const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
|
| 97 |
|
|
|
|
| 98 |
const [groupMembers] = useState<GroupMember[]>([
|
| 99 |
{ id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
|
| 100 |
{ id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
|
|
@@ -109,7 +113,12 @@ function App() {
|
|
| 109 |
if (user) {
|
| 110 |
const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
|
| 111 |
setWorkspaces([
|
| 112 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
{
|
| 114 |
id: 'group-1',
|
| 115 |
name: 'CS 101 Study Group',
|
|
@@ -142,7 +151,9 @@ function App() {
|
|
| 142 |
if (!content.trim() || !user) return;
|
| 143 |
|
| 144 |
const sender: GroupMember | undefined =
|
| 145 |
-
spaceType === 'group'
|
|
|
|
|
|
|
| 146 |
|
| 147 |
const userMessage: Message = {
|
| 148 |
id: crypto.randomUUID(),
|
|
@@ -158,16 +169,14 @@ function App() {
|
|
| 158 |
if (!shouldAIRespond) return;
|
| 159 |
|
| 160 |
const assistantId = crypto.randomUUID();
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
},
|
| 170 |
-
]);
|
| 171 |
|
| 172 |
try {
|
| 173 |
const data = await apiChat({
|
|
@@ -186,14 +195,20 @@ function App() {
|
|
| 186 |
setMessages((prev) =>
|
| 187 |
prev.map((m) =>
|
| 188 |
m.id === assistantId
|
| 189 |
-
? {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
: m
|
| 191 |
)
|
| 192 |
);
|
| 193 |
} catch (err: any) {
|
| 194 |
setMessages((prev) =>
|
| 195 |
prev.map((m) =>
|
| 196 |
-
m.id === assistantId
|
|
|
|
|
|
|
| 197 |
)
|
| 198 |
);
|
| 199 |
}
|
|
@@ -202,7 +217,11 @@ function App() {
|
|
| 202 |
const handleFileUpload = async (files: File[]) => {
|
| 203 |
if (!user) return;
|
| 204 |
|
| 205 |
-
const newFiles: UploadedFile[] = files.map((file) => ({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
setUploadedFiles((prev) => [...prev, ...newFiles]);
|
| 207 |
|
| 208 |
for (const f of files) {
|
|
@@ -227,7 +246,11 @@ function App() {
|
|
| 227 |
if (!target) return;
|
| 228 |
|
| 229 |
try {
|
| 230 |
-
const r = await apiUpload({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
toast.success(r.status_md || `Updated type: ${target.file.name}`);
|
| 232 |
} catch (e: any) {
|
| 233 |
toast.error(e?.message ?? `Failed to update type: ${target.file.name}`);
|
|
@@ -287,9 +310,8 @@ function App() {
|
|
| 287 |
}
|
| 288 |
|
| 289 |
return (
|
| 290 |
-
<div className="h-
|
| 291 |
<Toaster />
|
| 292 |
-
|
| 293 |
<Header
|
| 294 |
user={user}
|
| 295 |
onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
|
|
@@ -307,199 +329,200 @@ function App() {
|
|
| 307 |
<ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />
|
| 308 |
)}
|
| 309 |
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
{
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
className="absolute top-4 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
|
| 327 |
-
style={{ right: '-10px' }}
|
| 328 |
-
title="Close panel"
|
| 329 |
-
>
|
| 330 |
-
<ChevronLeft className="h-3 w-3" />
|
| 331 |
-
</Button>
|
| 332 |
-
|
| 333 |
-
<div className="flex-1 min-h-0 overflow-hidden">
|
| 334 |
-
<LeftSidebar
|
| 335 |
-
learningMode={learningMode}
|
| 336 |
-
language={language}
|
| 337 |
-
onLearningModeChange={setLearningMode}
|
| 338 |
-
onLanguageChange={setLanguage}
|
| 339 |
-
spaceType={spaceType}
|
| 340 |
-
groupMembers={groupMembers}
|
| 341 |
-
user={user}
|
| 342 |
-
onLogin={setUser}
|
| 343 |
-
onLogout={() => setUser(null)}
|
| 344 |
-
isLoggedIn={!!user}
|
| 345 |
-
onEditProfile={() => setShowProfileEditor(true)}
|
| 346 |
-
/>
|
| 347 |
-
</div>
|
| 348 |
-
</aside>
|
| 349 |
-
) : (
|
| 350 |
<Button
|
| 351 |
variant="secondary"
|
| 352 |
size="icon"
|
| 353 |
-
onClick={() => setLeftPanelVisible(
|
| 354 |
-
className="
|
| 355 |
-
|
|
|
|
| 356 |
>
|
| 357 |
-
<
|
| 358 |
</Button>
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
{/* Left (mobile) */}
|
| 362 |
-
<aside
|
| 363 |
-
className={`
|
| 364 |
-
fixed lg:hidden top-16 bottom-0 left-0 z-50
|
| 365 |
-
w-80 bg-card border-r border-border
|
| 366 |
-
transform transition-transform duration-300 ease-in-out
|
| 367 |
-
${leftSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
| 368 |
-
flex flex-col min-h-0 overflow-hidden
|
| 369 |
-
`}
|
| 370 |
-
>
|
| 371 |
-
<div className="p-4 border-b border-border flex justify-between items-center">
|
| 372 |
-
<h3>Settings & Guide</h3>
|
| 373 |
-
<Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
|
| 374 |
-
<X className="h-5 w-5" />
|
| 375 |
-
</Button>
|
| 376 |
-
</div>
|
| 377 |
-
<div className="flex-1 min-h-0 overflow-hidden">
|
| 378 |
-
<LeftSidebar
|
| 379 |
-
learningMode={learningMode}
|
| 380 |
-
language={language}
|
| 381 |
-
onLearningModeChange={setLearningMode}
|
| 382 |
-
onLanguageChange={setLanguage}
|
| 383 |
-
spaceType={spaceType}
|
| 384 |
-
groupMembers={groupMembers}
|
| 385 |
-
user={user}
|
| 386 |
-
onLogin={setUser}
|
| 387 |
-
onLogout={() => setUser(null)}
|
| 388 |
-
isLoggedIn={!!user}
|
| 389 |
-
onEditProfile={() => setShowProfileEditor(true)}
|
| 390 |
-
/>
|
| 391 |
-
</div>
|
| 392 |
-
</aside>
|
| 393 |
-
|
| 394 |
-
{/* ✅ Center:必须 min-h-0 + overflow-hidden(不滚),滚动交给 ChatArea */}
|
| 395 |
-
<main className="min-w-0 min-h-0 overflow-hidden flex">
|
| 396 |
-
<ChatArea
|
| 397 |
-
user={asApiUser(user)}
|
| 398 |
-
messages={messages}
|
| 399 |
-
onSendMessage={handleSendMessage}
|
| 400 |
-
uploadedFiles={uploadedFiles}
|
| 401 |
-
onFileUpload={handleFileUpload}
|
| 402 |
-
onRemoveFile={handleRemoveFile}
|
| 403 |
-
onFileTypeChange={handleFileTypeChange}
|
| 404 |
-
memoryProgress={memoryProgress}
|
| 405 |
-
isLoggedIn={!!user}
|
| 406 |
learningMode={learningMode}
|
| 407 |
-
|
| 408 |
onLearningModeChange={setLearningMode}
|
|
|
|
| 409 |
spaceType={spaceType}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
/>
|
| 411 |
-
</
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
<
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
<Button
|
| 450 |
variant="secondary"
|
| 451 |
size="icon"
|
| 452 |
-
onClick={() => setRightPanelVisible(
|
| 453 |
-
className="
|
| 454 |
-
|
|
|
|
| 455 |
>
|
| 456 |
-
<
|
| 457 |
</Button>
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
{/* Right (mobile) */}
|
| 461 |
-
<aside
|
| 462 |
-
className={`
|
| 463 |
-
fixed lg:hidden top-16 bottom-0 right-0 z-50
|
| 464 |
-
w-80 bg-card border-l border-border
|
| 465 |
-
transform transition-transform duration-300 ease-in-out
|
| 466 |
-
${rightPanelOpen ? 'translate-x-0' : 'translate-x-full'}
|
| 467 |
-
flex flex-col min-h-0 overflow-hidden
|
| 468 |
-
`}
|
| 469 |
-
>
|
| 470 |
-
<div className="p-4 border-b border-border flex justify-between items-center">
|
| 471 |
-
<h3>Account & Actions</h3>
|
| 472 |
-
<Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
|
| 473 |
-
<X className="h-5 w-5" />
|
| 474 |
-
</Button>
|
| 475 |
-
</div>
|
| 476 |
-
<div className="flex-1 min-h-0 overflow-hidden">
|
| 477 |
-
<RightPanel
|
| 478 |
-
user={user}
|
| 479 |
-
onLogin={setUser}
|
| 480 |
-
onLogout={() => setUser(null)}
|
| 481 |
-
isLoggedIn={!!user}
|
| 482 |
-
onClose={() => setRightPanelVisible(false)}
|
| 483 |
-
exportResult={exportResult}
|
| 484 |
-
setExportResult={setExportResult}
|
| 485 |
-
resultType={resultType}
|
| 486 |
-
setResultType={setResultType}
|
| 487 |
-
onExport={handleExport}
|
| 488 |
-
onSummary={handleSummary}
|
| 489 |
-
/>
|
| 490 |
-
</div>
|
| 491 |
-
</aside>
|
| 492 |
-
|
| 493 |
-
{!rightPanelVisible && (
|
| 494 |
-
<FloatingActionButtons
|
| 495 |
user={user}
|
|
|
|
|
|
|
| 496 |
isLoggedIn={!!user}
|
| 497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
onExport={handleExport}
|
| 499 |
onSummary={handleSummary}
|
| 500 |
/>
|
| 501 |
-
|
| 502 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
</div>
|
| 504 |
</div>
|
| 505 |
);
|
|
|
|
| 7 |
import { FloatingActionButtons } from './components/FloatingActionButtons';
|
| 8 |
import { LoginScreen } from './components/LoginScreen';
|
| 9 |
import { ProfileEditor } from './components/ProfileEditor';
|
| 10 |
+
import { X } from 'lucide-react';
|
| 11 |
import { Button } from './components/ui/button';
|
| 12 |
import { Toaster } from './components/ui/sonner';
|
| 13 |
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
| 14 |
import { toast } from 'sonner';
|
| 15 |
|
| 16 |
import {
|
|
|
|
| 31 |
content: string;
|
| 32 |
timestamp: Date;
|
| 33 |
references?: string[];
|
| 34 |
+
sender?: GroupMember; // For group chat
|
| 35 |
}
|
| 36 |
|
| 37 |
export interface User {
|
|
|
|
| 83 |
const [learningMode, setLearningMode] = useState<LearningMode>('concept');
|
| 84 |
const [language, setLanguage] = useState<Language>('auto');
|
| 85 |
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
| 86 |
+
|
| 87 |
+
// You can later wire this to /api/memoryline
|
| 88 |
const [memoryProgress] = useState(36);
|
| 89 |
|
| 90 |
const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
|
|
|
|
| 98 |
const [exportResult, setExportResult] = useState('');
|
| 99 |
const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
|
| 100 |
|
| 101 |
+
// Mock group members (still fine; AI responder uses backend now)
|
| 102 |
const [groupMembers] = useState<GroupMember[]>([
|
| 103 |
{ id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
|
| 104 |
{ id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
|
|
|
|
| 113 |
if (user) {
|
| 114 |
const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
|
| 115 |
setWorkspaces([
|
| 116 |
+
{
|
| 117 |
+
id: 'individual',
|
| 118 |
+
name: 'My Space',
|
| 119 |
+
type: 'individual',
|
| 120 |
+
avatar: userAvatar,
|
| 121 |
+
},
|
| 122 |
{
|
| 123 |
id: 'group-1',
|
| 124 |
name: 'CS 101 Study Group',
|
|
|
|
| 151 |
if (!content.trim() || !user) return;
|
| 152 |
|
| 153 |
const sender: GroupMember | undefined =
|
| 154 |
+
spaceType === 'group'
|
| 155 |
+
? { id: user.email, name: user.name, email: user.email }
|
| 156 |
+
: undefined;
|
| 157 |
|
| 158 |
const userMessage: Message = {
|
| 159 |
id: crypto.randomUUID(),
|
|
|
|
| 169 |
if (!shouldAIRespond) return;
|
| 170 |
|
| 171 |
const assistantId = crypto.randomUUID();
|
| 172 |
+
const assistantPlaceholder: Message = {
|
| 173 |
+
id: assistantId,
|
| 174 |
+
role: 'assistant',
|
| 175 |
+
content: 'Thinking...',
|
| 176 |
+
timestamp: new Date(),
|
| 177 |
+
sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
|
| 178 |
+
};
|
| 179 |
+
setMessages((prev) => [...prev, assistantPlaceholder]);
|
|
|
|
|
|
|
| 180 |
|
| 181 |
try {
|
| 182 |
const data = await apiChat({
|
|
|
|
| 195 |
setMessages((prev) =>
|
| 196 |
prev.map((m) =>
|
| 197 |
m.id === assistantId
|
| 198 |
+
? {
|
| 199 |
+
...m,
|
| 200 |
+
content: data.reply || '',
|
| 201 |
+
references: references.length ? references : undefined,
|
| 202 |
+
}
|
| 203 |
: m
|
| 204 |
)
|
| 205 |
);
|
| 206 |
} catch (err: any) {
|
| 207 |
setMessages((prev) =>
|
| 208 |
prev.map((m) =>
|
| 209 |
+
m.id === assistantId
|
| 210 |
+
? { ...m, content: `Sorry — request failed.\n${err?.message ?? String(err)}` }
|
| 211 |
+
: m
|
| 212 |
)
|
| 213 |
);
|
| 214 |
}
|
|
|
|
| 217 |
const handleFileUpload = async (files: File[]) => {
|
| 218 |
if (!user) return;
|
| 219 |
|
| 220 |
+
const newFiles: UploadedFile[] = files.map((file) => ({
|
| 221 |
+
file,
|
| 222 |
+
type: 'other',
|
| 223 |
+
}));
|
| 224 |
+
|
| 225 |
setUploadedFiles((prev) => [...prev, ...newFiles]);
|
| 226 |
|
| 227 |
for (const f of files) {
|
|
|
|
| 246 |
if (!target) return;
|
| 247 |
|
| 248 |
try {
|
| 249 |
+
const r = await apiUpload({
|
| 250 |
+
user: asApiUser(user),
|
| 251 |
+
file: target.file,
|
| 252 |
+
fileType: type,
|
| 253 |
+
});
|
| 254 |
toast.success(r.status_md || `Updated type: ${target.file.name}`);
|
| 255 |
} catch (e: any) {
|
| 256 |
toast.error(e?.message ?? `Failed to update type: ${target.file.name}`);
|
|
|
|
| 310 |
}
|
| 311 |
|
| 312 |
return (
|
| 313 |
+
<div className="min-h-screen bg-background flex flex-col">
|
| 314 |
<Toaster />
|
|
|
|
| 315 |
<Header
|
| 316 |
user={user}
|
| 317 |
onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
|
|
|
|
| 329 |
<ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />
|
| 330 |
)}
|
| 331 |
|
| 332 |
+
<div
|
| 333 |
+
className="flex-1 flex overflow-hidden"
|
| 334 |
+
onWheel={(e) => e.stopPropagation()}
|
| 335 |
+
style={{ overscrollBehavior: 'none' }}
|
| 336 |
+
>
|
| 337 |
+
{/* Mobile Sidebar Toggle - Left */}
|
| 338 |
+
{leftSidebarOpen && (
|
| 339 |
+
<div
|
| 340 |
+
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
| 341 |
+
onClick={() => setLeftSidebarOpen(false)}
|
| 342 |
+
/>
|
| 343 |
+
)}
|
| 344 |
+
|
| 345 |
+
{/* Left Sidebar */}
|
| 346 |
+
{leftPanelVisible ? (
|
| 347 |
+
<aside className="hidden lg:flex w-80 bg-card border-r border-border flex-col h-full min-h-0 relative">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
<Button
|
| 349 |
variant="secondary"
|
| 350 |
size="icon"
|
| 351 |
+
onClick={() => setLeftPanelVisible(false)}
|
| 352 |
+
className="absolute top-4 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
|
| 353 |
+
style={{ right: '-10px' }}
|
| 354 |
+
title="Close panel"
|
| 355 |
>
|
| 356 |
+
<ChevronLeft className="h-3 w-3" />
|
| 357 |
</Button>
|
| 358 |
+
<LeftSidebar
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
learningMode={learningMode}
|
| 360 |
+
language={language}
|
| 361 |
onLearningModeChange={setLearningMode}
|
| 362 |
+
onLanguageChange={setLanguage}
|
| 363 |
spaceType={spaceType}
|
| 364 |
+
groupMembers={groupMembers}
|
| 365 |
+
user={user}
|
| 366 |
+
onLogin={setUser}
|
| 367 |
+
onLogout={() => setUser(null)}
|
| 368 |
+
isLoggedIn={!!user}
|
| 369 |
+
onEditProfile={() => setShowProfileEditor(true)}
|
| 370 |
/>
|
| 371 |
+
</aside>
|
| 372 |
+
) : (
|
| 373 |
+
<Button
|
| 374 |
+
variant="secondary"
|
| 375 |
+
size="icon"
|
| 376 |
+
onClick={() => setLeftPanelVisible(true)}
|
| 377 |
+
className="hidden lg:flex fixed top-20 left-0 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
|
| 378 |
+
title="Open panel"
|
| 379 |
+
>
|
| 380 |
+
<ChevronRight className="h-3 w-3" />
|
| 381 |
+
</Button>
|
| 382 |
+
)}
|
| 383 |
+
|
| 384 |
+
{/* Left Sidebar - Mobile */}
|
| 385 |
+
<aside
|
| 386 |
+
className={`
|
| 387 |
+
fixed lg:hidden inset-y-0 left-0 z-50
|
| 388 |
+
w-80 bg-card border-r border-border
|
| 389 |
+
transform transition-transform duration-300 ease-in-out
|
| 390 |
+
${leftSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
| 391 |
+
flex flex-col
|
| 392 |
+
mt-16
|
| 393 |
+
h-[calc(100vh-4rem)]
|
| 394 |
+
min-h-0
|
| 395 |
+
`}
|
| 396 |
+
>
|
| 397 |
+
<div className="p-4 border-b border-border flex justify-between items-center">
|
| 398 |
+
<h3>Settings & Guide</h3>
|
| 399 |
+
<Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
|
| 400 |
+
<X className="h-5 w-5" />
|
| 401 |
+
</Button>
|
| 402 |
+
</div>
|
| 403 |
+
<LeftSidebar
|
| 404 |
+
learningMode={learningMode}
|
| 405 |
+
language={language}
|
| 406 |
+
onLearningModeChange={setLearningMode}
|
| 407 |
+
onLanguageChange={setLanguage}
|
| 408 |
+
spaceType={spaceType}
|
| 409 |
+
groupMembers={groupMembers}
|
| 410 |
+
user={user}
|
| 411 |
+
onLogin={setUser}
|
| 412 |
+
onLogout={() => setUser(null)}
|
| 413 |
+
isLoggedIn={!!user}
|
| 414 |
+
onEditProfile={() => setShowProfileEditor(true)}
|
| 415 |
+
/>
|
| 416 |
+
</aside>
|
| 417 |
+
|
| 418 |
+
{/* Main Chat Area */}
|
| 419 |
+
<main className="flex-1 flex flex-col min-w-0 min-h-0 h-full">
|
| 420 |
+
<ChatArea
|
| 421 |
+
// ✅ NEW: pass ApiUser down so Message can submit feedback
|
| 422 |
+
user={asApiUser(user)}
|
| 423 |
+
messages={messages}
|
| 424 |
+
onSendMessage={handleSendMessage}
|
| 425 |
+
uploadedFiles={uploadedFiles}
|
| 426 |
+
onFileUpload={handleFileUpload}
|
| 427 |
+
onRemoveFile={handleRemoveFile}
|
| 428 |
+
onFileTypeChange={handleFileTypeChange}
|
| 429 |
+
memoryProgress={memoryProgress}
|
| 430 |
+
isLoggedIn={!!user}
|
| 431 |
+
learningMode={learningMode}
|
| 432 |
+
onClearConversation={handleClearConversation}
|
| 433 |
+
onLearningModeChange={setLearningMode}
|
| 434 |
+
spaceType={spaceType}
|
| 435 |
+
/>
|
| 436 |
+
</main>
|
| 437 |
+
|
| 438 |
+
{/* Mobile Sidebar Toggle - Right */}
|
| 439 |
+
{rightPanelOpen && (
|
| 440 |
+
<div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
|
| 441 |
+
)}
|
| 442 |
+
|
| 443 |
+
{/* Right Panel */}
|
| 444 |
+
{rightPanelVisible ? (
|
| 445 |
+
<aside className="hidden lg:flex w-80 bg-card border-l border-border flex-col min-h-0 relative" style={{ height: 'calc(100vh - 4rem)' }}>
|
| 446 |
<Button
|
| 447 |
variant="secondary"
|
| 448 |
size="icon"
|
| 449 |
+
onClick={() => setRightPanelVisible(false)}
|
| 450 |
+
className="absolute top-4 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
|
| 451 |
+
style={{ left: '-10px' }}
|
| 452 |
+
title="Close panel"
|
| 453 |
>
|
| 454 |
+
<ChevronRight className="h-3 w-3" />
|
| 455 |
</Button>
|
| 456 |
+
<RightPanel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
user={user}
|
| 458 |
+
onLogin={setUser}
|
| 459 |
+
onLogout={() => setUser(null)}
|
| 460 |
isLoggedIn={!!user}
|
| 461 |
+
onClose={() => setRightPanelVisible(false)}
|
| 462 |
+
exportResult={exportResult}
|
| 463 |
+
setExportResult={setExportResult}
|
| 464 |
+
resultType={resultType}
|
| 465 |
+
setResultType={setResultType}
|
| 466 |
onExport={handleExport}
|
| 467 |
onSummary={handleSummary}
|
| 468 |
/>
|
| 469 |
+
</aside>
|
| 470 |
+
) : (
|
| 471 |
+
<Button
|
| 472 |
+
variant="secondary"
|
| 473 |
+
size="icon"
|
| 474 |
+
onClick={() => setRightPanelVisible(true)}
|
| 475 |
+
className="hidden lg:flex fixed top-20 right-0 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
|
| 476 |
+
title="Open panel"
|
| 477 |
+
>
|
| 478 |
+
<ChevronLeft className="h-3 w-3" />
|
| 479 |
+
</Button>
|
| 480 |
+
)}
|
| 481 |
+
|
| 482 |
+
{/* Right Panel - Mobile */}
|
| 483 |
+
<aside
|
| 484 |
+
className={`
|
| 485 |
+
fixed lg:hidden inset-y-0 right-0 z-50
|
| 486 |
+
w-80 bg-card border-l border-border
|
| 487 |
+
transform transition-transform duration-300 ease-in-out
|
| 488 |
+
${rightPanelOpen ? 'translate-x-0' : 'translate-x-full'}
|
| 489 |
+
flex flex-col
|
| 490 |
+
mt-16
|
| 491 |
+
h-[calc(100vh-4rem)]
|
| 492 |
+
min-h-0
|
| 493 |
+
`}
|
| 494 |
+
>
|
| 495 |
+
<div className="p-4 border-b border-border flex justify-between items-center">
|
| 496 |
+
<h3>Account & Actions</h3>
|
| 497 |
+
<Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
|
| 498 |
+
<X className="h-5 w-5" />
|
| 499 |
+
</Button>
|
| 500 |
+
</div>
|
| 501 |
+
<RightPanel
|
| 502 |
+
user={user}
|
| 503 |
+
onLogin={setUser}
|
| 504 |
+
onLogout={() => setUser(null)}
|
| 505 |
+
isLoggedIn={!!user}
|
| 506 |
+
onClose={() => setRightPanelVisible(false)}
|
| 507 |
+
exportResult={exportResult}
|
| 508 |
+
setExportResult={setExportResult}
|
| 509 |
+
resultType={resultType}
|
| 510 |
+
setResultType={setResultType}
|
| 511 |
+
onExport={handleExport}
|
| 512 |
+
onSummary={handleSummary}
|
| 513 |
+
/>
|
| 514 |
+
</aside>
|
| 515 |
+
|
| 516 |
+
{/* Floating Action Buttons - Desktop only, when panel is closed */}
|
| 517 |
+
{!rightPanelVisible && (
|
| 518 |
+
<FloatingActionButtons
|
| 519 |
+
user={user}
|
| 520 |
+
isLoggedIn={!!user}
|
| 521 |
+
onOpenPanel={() => setRightPanelVisible(true)}
|
| 522 |
+
onExport={handleExport}
|
| 523 |
+
onSummary={handleSummary}
|
| 524 |
+
/>
|
| 525 |
+
)}
|
| 526 |
</div>
|
| 527 |
</div>
|
| 528 |
);
|