Spaces:
Sleeping
Sleeping
Update web/src/App.tsx
Browse files- web/src/App.tsx +97 -25
web/src/App.tsx
CHANGED
|
@@ -15,12 +15,13 @@ export interface Message {
|
|
| 15 |
content: string;
|
| 16 |
timestamp: Date;
|
| 17 |
references?: string[];
|
| 18 |
-
sender?: GroupMember;
|
| 19 |
}
|
| 20 |
|
| 21 |
export interface User {
|
| 22 |
name: string;
|
| 23 |
-
email: string;
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
export interface GroupMember {
|
|
@@ -32,7 +33,6 @@ export interface GroupMember {
|
|
| 32 |
}
|
| 33 |
|
| 34 |
export type SpaceType = 'individual' | 'group';
|
| 35 |
-
|
| 36 |
export type FileType = 'syllabus' | 'lecture-slides' | 'literature-review' | 'other';
|
| 37 |
|
| 38 |
export interface UploadedFile {
|
|
@@ -58,6 +58,11 @@ type UploadApiResp = {
|
|
| 58 |
status_md: string;
|
| 59 |
};
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
function mapFileTypeToDocType(t: FileType): string {
|
| 62 |
switch (t) {
|
| 63 |
case 'syllabus':
|
|
@@ -106,7 +111,7 @@ function App() {
|
|
| 106 |
id: '1',
|
| 107 |
role: 'assistant',
|
| 108 |
content:
|
| 109 |
-
"Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI.
|
| 110 |
timestamp: new Date(),
|
| 111 |
},
|
| 112 |
]);
|
|
@@ -138,16 +143,11 @@ function App() {
|
|
| 138 |
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
|
| 139 |
}, [isDarkMode]);
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
return (user?.email || '0405').trim();
|
| 144 |
-
}, [user]);
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
// 目前为了便于调试后端,允许未登录也走通(user_id=0405)
|
| 149 |
-
return true;
|
| 150 |
-
}, []);
|
| 151 |
|
| 152 |
const currentDocTypeForChat = useMemo(() => {
|
| 153 |
const hasSyllabus = uploadedFiles.some((f) => f.type === 'syllabus' && f.uploaded);
|
|
@@ -159,14 +159,56 @@ function App() {
|
|
| 159 |
return 'Other';
|
| 160 |
}, [uploadedFiles]);
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
const handleSendMessage = async (content: string) => {
|
| 163 |
if (!content.trim()) return;
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
const shouldAIRespond = spaceType === 'individual' || content.toLowerCase().includes('@clare');
|
| 166 |
|
| 167 |
const sender: GroupMember | undefined =
|
| 168 |
spaceType === 'group' && user
|
| 169 |
-
? { id: user.
|
| 170 |
: undefined;
|
| 171 |
|
| 172 |
const userMessage: Message = {
|
|
@@ -220,6 +262,10 @@ function App() {
|
|
| 220 |
|
| 221 |
// ✅ 选文件:只入库,不上传
|
| 222 |
const handleFileUpload = (files: File[]) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
const newFiles: UploadedFile[] = files.map((file) => ({
|
| 224 |
file,
|
| 225 |
type: 'other',
|
|
@@ -237,8 +283,13 @@ function App() {
|
|
| 237 |
setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f)));
|
| 238 |
};
|
| 239 |
|
| 240 |
-
// ✅
|
| 241 |
const handleUploadSingle = async (index: number) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
const target = uploadedFiles[index];
|
| 243 |
if (!target) return;
|
| 244 |
|
|
@@ -264,8 +315,12 @@ function App() {
|
|
| 264 |
}
|
| 265 |
};
|
| 266 |
|
| 267 |
-
// ✅ 上传所有未上传
|
| 268 |
const handleUploadAllPending = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
const pendingIdx = uploadedFiles
|
| 270 |
.map((f, i) => ({ f, i }))
|
| 271 |
.filter((x) => !x.f.uploaded)
|
|
@@ -277,7 +332,6 @@ function App() {
|
|
| 277 |
}
|
| 278 |
|
| 279 |
for (const idx of pendingIdx) {
|
| 280 |
-
// 顺序上传便于 debug
|
| 281 |
// eslint-disable-next-line no-await-in-loop
|
| 282 |
await handleUploadSingle(idx);
|
| 283 |
}
|
|
@@ -289,13 +343,17 @@ function App() {
|
|
| 289 |
id: '1',
|
| 290 |
role: 'assistant',
|
| 291 |
content:
|
| 292 |
-
"Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI.
|
| 293 |
timestamp: new Date(),
|
| 294 |
},
|
| 295 |
]);
|
| 296 |
};
|
| 297 |
|
| 298 |
const handleExport = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
try {
|
| 300 |
const r = await apiPostJson<{ markdown: string }>('/api/export', {
|
| 301 |
user_id: userId,
|
|
@@ -310,6 +368,10 @@ function App() {
|
|
| 310 |
};
|
| 311 |
|
| 312 |
const handleSummary = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
try {
|
| 314 |
const r = await apiPostJson<{ markdown: string }>('/api/summary', {
|
| 315 |
user_id: userId,
|
|
@@ -339,6 +401,8 @@ D) Cost reduction
|
|
| 339 |
};
|
| 340 |
|
| 341 |
useEffect(() => {
|
|
|
|
|
|
|
| 342 |
const run = async () => {
|
| 343 |
try {
|
| 344 |
const res = await fetch(`/api/memoryline?user_id=${encodeURIComponent(userId)}`);
|
|
@@ -351,7 +415,7 @@ D) Cost reduction
|
|
| 351 |
}
|
| 352 |
};
|
| 353 |
run();
|
| 354 |
-
}, [userId]);
|
| 355 |
|
| 356 |
return (
|
| 357 |
<div className="min-h-screen bg-background flex flex-col">
|
|
@@ -366,7 +430,10 @@ D) Cost reduction
|
|
| 366 |
|
| 367 |
<div className="flex-1 flex overflow-hidden">
|
| 368 |
{leftSidebarOpen && (
|
| 369 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 370 |
)}
|
| 371 |
|
| 372 |
<aside
|
|
@@ -401,6 +468,7 @@ D) Cost reduction
|
|
| 401 |
<main className="flex-1 flex flex-col min-w-0">
|
| 402 |
<ChatArea
|
| 403 |
userId={userId}
|
|
|
|
| 404 |
messages={messages}
|
| 405 |
onSendMessage={handleSendMessage}
|
| 406 |
uploadedFiles={uploadedFiles}
|
|
@@ -412,6 +480,7 @@ D) Cost reduction
|
|
| 412 |
memoryProgress={memoryProgress}
|
| 413 |
isLoggedIn={isLoggedIn}
|
| 414 |
learningMode={learningMode}
|
|
|
|
| 415 |
onClearConversation={handleClearConversation}
|
| 416 |
onLearningModeChange={setLearningMode}
|
| 417 |
spaceType={spaceType}
|
|
@@ -419,7 +488,10 @@ D) Cost reduction
|
|
| 419 |
</main>
|
| 420 |
|
| 421 |
{rightPanelOpen && (
|
| 422 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 423 |
)}
|
| 424 |
|
| 425 |
{rightPanelVisible && (
|
|
@@ -443,9 +515,9 @@ D) Cost reduction
|
|
| 443 |
|
| 444 |
<RightPanel
|
| 445 |
user={user}
|
| 446 |
-
onLogin={
|
| 447 |
-
onLogout={
|
| 448 |
-
isLoggedIn={
|
| 449 |
onClose={() => setRightPanelVisible(false)}
|
| 450 |
exportResult={exportResult}
|
| 451 |
setExportResult={setExportResult}
|
|
@@ -470,7 +542,7 @@ D) Cost reduction
|
|
| 470 |
{!rightPanelVisible && (
|
| 471 |
<FloatingActionButtons
|
| 472 |
user={user}
|
| 473 |
-
isLoggedIn={
|
| 474 |
onOpenPanel={() => setRightPanelVisible(true)}
|
| 475 |
onExport={handleExport}
|
| 476 |
onQuiz={handleQuiz}
|
|
|
|
| 15 |
content: string;
|
| 16 |
timestamp: Date;
|
| 17 |
references?: string[];
|
| 18 |
+
sender?: GroupMember;
|
| 19 |
}
|
| 20 |
|
| 21 |
export interface User {
|
| 22 |
name: string;
|
| 23 |
+
email: string; // login 输入
|
| 24 |
+
user_id: string; // 实际传后端的 user_id(这里等同 email/ID)
|
| 25 |
}
|
| 26 |
|
| 27 |
export interface GroupMember {
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
export type SpaceType = 'individual' | 'group';
|
|
|
|
| 36 |
export type FileType = 'syllabus' | 'lecture-slides' | 'literature-review' | 'other';
|
| 37 |
|
| 38 |
export interface UploadedFile {
|
|
|
|
| 58 |
status_md: string;
|
| 59 |
};
|
| 60 |
|
| 61 |
+
type LoginApiResp = {
|
| 62 |
+
ok: boolean;
|
| 63 |
+
user: { name: string; user_id: string };
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
function mapFileTypeToDocType(t: FileType): string {
|
| 67 |
switch (t) {
|
| 68 |
case 'syllabus':
|
|
|
|
| 111 |
id: '1',
|
| 112 |
role: 'assistant',
|
| 113 |
content:
|
| 114 |
+
"Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Please log in on the right, upload materials (optional), and ask me anything.",
|
| 115 |
timestamp: new Date(),
|
| 116 |
},
|
| 117 |
]);
|
|
|
|
| 143 |
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
|
| 144 |
}, [isDarkMode]);
|
| 145 |
|
| 146 |
+
// ✅ 彻底去掉 hardcode:未登录 userId 为空
|
| 147 |
+
const userId = useMemo(() => (user?.user_id || '').trim(), [user]);
|
|
|
|
|
|
|
| 148 |
|
| 149 |
+
// ✅ 未登录不可聊天/上传/feedback
|
| 150 |
+
const isLoggedIn = useMemo(() => !!user && !!userId, [user, userId]);
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
const currentDocTypeForChat = useMemo(() => {
|
| 153 |
const hasSyllabus = uploadedFiles.some((f) => f.type === 'syllabus' && f.uploaded);
|
|
|
|
| 159 |
return 'Other';
|
| 160 |
}, [uploadedFiles]);
|
| 161 |
|
| 162 |
+
// ✅ 登录必须打后端 /api/login,确保 server session 有 name
|
| 163 |
+
const handleLogin = async (name: string, emailOrId: string) => {
|
| 164 |
+
const nameTrim = name.trim();
|
| 165 |
+
const idTrim = emailOrId.trim();
|
| 166 |
+
|
| 167 |
+
if (!nameTrim || !idTrim) {
|
| 168 |
+
toast.error('Please fill in both name and Email/ID');
|
| 169 |
+
return;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
try {
|
| 173 |
+
const resp = await apiPostJson<LoginApiResp>('/api/login', {
|
| 174 |
+
name: nameTrim,
|
| 175 |
+
user_id: idTrim,
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
if (!resp?.ok) throw new Error('Login failed (ok=false)');
|
| 179 |
+
|
| 180 |
+
// 后端回来的 user_id 为准(保持一致)
|
| 181 |
+
setUser({
|
| 182 |
+
name: resp.user.name,
|
| 183 |
+
email: idTrim,
|
| 184 |
+
user_id: resp.user.user_id,
|
| 185 |
+
});
|
| 186 |
+
|
| 187 |
+
toast.success('Logged in');
|
| 188 |
+
} catch (e: any) {
|
| 189 |
+
console.error(e);
|
| 190 |
+
toast.error(`Login failed: ${e?.message || 'unknown error'}`);
|
| 191 |
+
}
|
| 192 |
+
};
|
| 193 |
+
|
| 194 |
+
const handleLogout = () => {
|
| 195 |
+
setUser(null);
|
| 196 |
+
toast.message('Logged out');
|
| 197 |
+
};
|
| 198 |
+
|
| 199 |
const handleSendMessage = async (content: string) => {
|
| 200 |
if (!content.trim()) return;
|
| 201 |
|
| 202 |
+
if (!isLoggedIn) {
|
| 203 |
+
toast.error('Please log in first');
|
| 204 |
+
return;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
const shouldAIRespond = spaceType === 'individual' || content.toLowerCase().includes('@clare');
|
| 208 |
|
| 209 |
const sender: GroupMember | undefined =
|
| 210 |
spaceType === 'group' && user
|
| 211 |
+
? { id: user.user_id, name: user.name, email: user.email }
|
| 212 |
: undefined;
|
| 213 |
|
| 214 |
const userMessage: Message = {
|
|
|
|
| 262 |
|
| 263 |
// ✅ 选文件:只入库,不上传
|
| 264 |
const handleFileUpload = (files: File[]) => {
|
| 265 |
+
if (!isLoggedIn) {
|
| 266 |
+
toast.error('Please log in first');
|
| 267 |
+
return;
|
| 268 |
+
}
|
| 269 |
const newFiles: UploadedFile[] = files.map((file) => ({
|
| 270 |
file,
|
| 271 |
type: 'other',
|
|
|
|
| 283 |
setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f)));
|
| 284 |
};
|
| 285 |
|
| 286 |
+
// ✅ 上传单个文件
|
| 287 |
const handleUploadSingle = async (index: number) => {
|
| 288 |
+
if (!isLoggedIn) {
|
| 289 |
+
toast.error('Please log in first');
|
| 290 |
+
return;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
const target = uploadedFiles[index];
|
| 294 |
if (!target) return;
|
| 295 |
|
|
|
|
| 315 |
}
|
| 316 |
};
|
| 317 |
|
|
|
|
| 318 |
const handleUploadAllPending = async () => {
|
| 319 |
+
if (!isLoggedIn) {
|
| 320 |
+
toast.error('Please log in first');
|
| 321 |
+
return;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
const pendingIdx = uploadedFiles
|
| 325 |
.map((f, i) => ({ f, i }))
|
| 326 |
.filter((x) => !x.f.uploaded)
|
|
|
|
| 332 |
}
|
| 333 |
|
| 334 |
for (const idx of pendingIdx) {
|
|
|
|
| 335 |
// eslint-disable-next-line no-await-in-loop
|
| 336 |
await handleUploadSingle(idx);
|
| 337 |
}
|
|
|
|
| 343 |
id: '1',
|
| 344 |
role: 'assistant',
|
| 345 |
content:
|
| 346 |
+
"Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Please log in on the right, upload materials (optional), and ask me anything.",
|
| 347 |
timestamp: new Date(),
|
| 348 |
},
|
| 349 |
]);
|
| 350 |
};
|
| 351 |
|
| 352 |
const handleExport = async () => {
|
| 353 |
+
if (!isLoggedIn) {
|
| 354 |
+
toast.error('Please log in first');
|
| 355 |
+
return;
|
| 356 |
+
}
|
| 357 |
try {
|
| 358 |
const r = await apiPostJson<{ markdown: string }>('/api/export', {
|
| 359 |
user_id: userId,
|
|
|
|
| 368 |
};
|
| 369 |
|
| 370 |
const handleSummary = async () => {
|
| 371 |
+
if (!isLoggedIn) {
|
| 372 |
+
toast.error('Please log in first');
|
| 373 |
+
return;
|
| 374 |
+
}
|
| 375 |
try {
|
| 376 |
const r = await apiPostJson<{ markdown: string }>('/api/summary', {
|
| 377 |
user_id: userId,
|
|
|
|
| 401 |
};
|
| 402 |
|
| 403 |
useEffect(() => {
|
| 404 |
+
if (!isLoggedIn) return;
|
| 405 |
+
|
| 406 |
const run = async () => {
|
| 407 |
try {
|
| 408 |
const res = await fetch(`/api/memoryline?user_id=${encodeURIComponent(userId)}`);
|
|
|
|
| 415 |
}
|
| 416 |
};
|
| 417 |
run();
|
| 418 |
+
}, [isLoggedIn, userId]);
|
| 419 |
|
| 420 |
return (
|
| 421 |
<div className="min-h-screen bg-background flex flex-col">
|
|
|
|
| 430 |
|
| 431 |
<div className="flex-1 flex overflow-hidden">
|
| 432 |
{leftSidebarOpen && (
|
| 433 |
+
<div
|
| 434 |
+
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
| 435 |
+
onClick={() => setLeftSidebarOpen(false)}
|
| 436 |
+
/>
|
| 437 |
)}
|
| 438 |
|
| 439 |
<aside
|
|
|
|
| 468 |
<main className="flex-1 flex flex-col min-w-0">
|
| 469 |
<ChatArea
|
| 470 |
userId={userId}
|
| 471 |
+
user={user} // ✅ 让 ChatArea/Message 能拿到 name/id 做 feedback
|
| 472 |
messages={messages}
|
| 473 |
onSendMessage={handleSendMessage}
|
| 474 |
uploadedFiles={uploadedFiles}
|
|
|
|
| 480 |
memoryProgress={memoryProgress}
|
| 481 |
isLoggedIn={isLoggedIn}
|
| 482 |
learningMode={learningMode}
|
| 483 |
+
currentDocTypeForChat={currentDocTypeForChat} // ✅ 反馈也需要
|
| 484 |
onClearConversation={handleClearConversation}
|
| 485 |
onLearningModeChange={setLearningMode}
|
| 486 |
spaceType={spaceType}
|
|
|
|
| 488 |
</main>
|
| 489 |
|
| 490 |
{rightPanelOpen && (
|
| 491 |
+
<div
|
| 492 |
+
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
| 493 |
+
onClick={() => setRightPanelOpen(false)}
|
| 494 |
+
/>
|
| 495 |
)}
|
| 496 |
|
| 497 |
{rightPanelVisible && (
|
|
|
|
| 515 |
|
| 516 |
<RightPanel
|
| 517 |
user={user}
|
| 518 |
+
onLogin={handleLogin}
|
| 519 |
+
onLogout={handleLogout}
|
| 520 |
+
isLoggedIn={isLoggedIn}
|
| 521 |
onClose={() => setRightPanelVisible(false)}
|
| 522 |
exportResult={exportResult}
|
| 523 |
setExportResult={setExportResult}
|
|
|
|
| 542 |
{!rightPanelVisible && (
|
| 543 |
<FloatingActionButtons
|
| 544 |
user={user}
|
| 545 |
+
isLoggedIn={isLoggedIn}
|
| 546 |
onOpenPanel={() => setRightPanelVisible(true)}
|
| 547 |
onExport={handleExport}
|
| 548 |
onQuiz={handleQuiz}
|