Spaces:
Sleeping
Sleeping
Update web/src/App.tsx
Browse files- web/src/App.tsx +43 -65
web/src/App.tsx
CHANGED
|
@@ -126,7 +126,6 @@ function App() {
|
|
| 126 |
const [exportResult, setExportResult] = useState('');
|
| 127 |
const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
|
| 128 |
|
| 129 |
-
// Mock group members
|
| 130 |
const [groupMembers] = useState<GroupMember[]>([
|
| 131 |
{ id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
|
| 132 |
{ id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
|
|
@@ -140,18 +139,22 @@ function App() {
|
|
| 140 |
}, [isDarkMode]);
|
| 141 |
|
| 142 |
const userId = useMemo(() => {
|
| 143 |
-
//
|
| 144 |
-
// 没登录也能测试:兜底 0405
|
| 145 |
return (user?.email || '0405').trim();
|
| 146 |
}, [user]);
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
const currentDocTypeForChat = useMemo(() => {
|
| 149 |
-
|
| 150 |
-
const hasSyllabus = uploadedFiles.some(f => f.type === 'syllabus' && f.uploaded);
|
| 151 |
if (hasSyllabus) return 'Syllabus';
|
| 152 |
-
const hasSlides = uploadedFiles.some(f => f.type === 'lecture-slides' && f.uploaded);
|
| 153 |
if (hasSlides) return 'Lecture Slides';
|
| 154 |
-
const hasLit = uploadedFiles.some(f => f.type === 'literature-review' && f.uploaded);
|
| 155 |
if (hasLit) return 'Literature Review / Paper';
|
| 156 |
return 'Other';
|
| 157 |
}, [uploadedFiles]);
|
|
@@ -159,11 +162,8 @@ function App() {
|
|
| 159 |
const handleSendMessage = async (content: string) => {
|
| 160 |
if (!content.trim()) return;
|
| 161 |
|
| 162 |
-
|
| 163 |
-
const shouldAIRespond =
|
| 164 |
-
spaceType === 'individual' || content.toLowerCase().includes('@clare');
|
| 165 |
|
| 166 |
-
// Sender info in group mode
|
| 167 |
const sender: GroupMember | undefined =
|
| 168 |
spaceType === 'group' && user
|
| 169 |
? { id: user.email, name: user.name, email: user.email }
|
|
@@ -176,7 +176,7 @@ function App() {
|
|
| 176 |
timestamp: new Date(),
|
| 177 |
sender,
|
| 178 |
};
|
| 179 |
-
setMessages(prev => [...prev, userMessage]);
|
| 180 |
|
| 181 |
if (!shouldAIRespond) return;
|
| 182 |
|
|
@@ -190,7 +190,7 @@ function App() {
|
|
| 190 |
});
|
| 191 |
|
| 192 |
const refs = (resp.refs || [])
|
| 193 |
-
.map(r => [r.source_file, r.section].filter(Boolean).join(' / '))
|
| 194 |
.filter(Boolean);
|
| 195 |
|
| 196 |
const assistantMessage: Message = {
|
|
@@ -199,45 +199,45 @@ function App() {
|
|
| 199 |
content: resp.reply || '(empty reply)',
|
| 200 |
timestamp: new Date(),
|
| 201 |
references: refs.length ? refs : undefined,
|
| 202 |
-
sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
|
| 203 |
};
|
| 204 |
|
| 205 |
-
setMessages(prev => [...prev, assistantMessage]);
|
| 206 |
} catch (e: any) {
|
| 207 |
console.error(e);
|
| 208 |
toast.error(`Chat failed: ${e?.message || 'unknown error'}`);
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
| 216 |
}
|
| 217 |
};
|
| 218 |
|
| 219 |
-
// ✅
|
| 220 |
const handleFileUpload = (files: File[]) => {
|
| 221 |
-
const newFiles: UploadedFile[] = files.map(file => ({
|
| 222 |
file,
|
| 223 |
type: 'other',
|
| 224 |
uploaded: false,
|
| 225 |
}));
|
| 226 |
-
setUploadedFiles(prev => [...prev, ...newFiles]);
|
| 227 |
toast.message('Files added. Select a File Type, then click Upload.');
|
| 228 |
};
|
| 229 |
|
| 230 |
const handleRemoveFile = (index: number) => {
|
| 231 |
-
setUploadedFiles(prev => prev.filter((_, i) => i !== index));
|
| 232 |
};
|
| 233 |
|
| 234 |
const handleFileTypeChange = (index: number, type: FileType) => {
|
| 235 |
-
setUploadedFiles(prev =>
|
| 236 |
-
prev.map((f, i) => (i === index ? { ...f, type } : f))
|
| 237 |
-
);
|
| 238 |
};
|
| 239 |
|
| 240 |
-
// ✅
|
| 241 |
const handleUploadSingle = async (index: number) => {
|
| 242 |
const target = uploadedFiles[index];
|
| 243 |
if (!target) return;
|
|
@@ -245,20 +245,15 @@ function App() {
|
|
| 245 |
try {
|
| 246 |
const form = new FormData();
|
| 247 |
form.append('user_id', userId);
|
| 248 |
-
form.append('doc_type', mapFileTypeToDocType(target.type));
|
| 249 |
form.append('file', target.file);
|
| 250 |
|
| 251 |
const r = await apiPostForm<UploadApiResp>('/api/upload', form);
|
|
|
|
| 252 |
|
| 253 |
-
|
| 254 |
-
throw new Error('Upload response ok=false');
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
setUploadedFiles(prev =>
|
| 258 |
prev.map((x, i) =>
|
| 259 |
-
i === index
|
| 260 |
-
? { ...x, uploaded: true, uploadedChunks: r.added_chunks }
|
| 261 |
-
: x
|
| 262 |
)
|
| 263 |
);
|
| 264 |
|
|
@@ -269,11 +264,12 @@ function App() {
|
|
| 269 |
}
|
| 270 |
};
|
| 271 |
|
|
|
|
| 272 |
const handleUploadAllPending = async () => {
|
| 273 |
const pendingIdx = uploadedFiles
|
| 274 |
.map((f, i) => ({ f, i }))
|
| 275 |
-
.filter(x => !x.f.uploaded)
|
| 276 |
-
.map(x => x.i);
|
| 277 |
|
| 278 |
if (!pendingIdx.length) {
|
| 279 |
toast.message('No pending files to upload.');
|
|
@@ -281,7 +277,7 @@ function App() {
|
|
| 281 |
}
|
| 282 |
|
| 283 |
for (const idx of pendingIdx) {
|
| 284 |
-
// 顺序上传
|
| 285 |
// eslint-disable-next-line no-await-in-loop
|
| 286 |
await handleUploadSingle(idx);
|
| 287 |
}
|
|
@@ -299,8 +295,6 @@ function App() {
|
|
| 299 |
]);
|
| 300 |
};
|
| 301 |
|
| 302 |
-
// 这些 action 你后端现在还没实现(/api/export /api/summary 你有)
|
| 303 |
-
// 这里先直接调后端,结果显示在 RightPanel
|
| 304 |
const handleExport = async () => {
|
| 305 |
try {
|
| 306 |
const r = await apiPostJson<{ markdown: string }>('/api/export', {
|
|
@@ -330,7 +324,6 @@ function App() {
|
|
| 330 |
}
|
| 331 |
};
|
| 332 |
|
| 333 |
-
// Quiz 你后端未必有接口,这里先保留 mock
|
| 334 |
const handleQuiz = () => {
|
| 335 |
const quiz = `# Micro-Quiz: Responsible AI
|
| 336 |
|
|
@@ -345,7 +338,6 @@ D) Cost reduction
|
|
| 345 |
toast.success('Quiz generated!');
|
| 346 |
};
|
| 347 |
|
| 348 |
-
// Memoryline:轻量拉一下(失败不影响)
|
| 349 |
useEffect(() => {
|
| 350 |
const run = async () => {
|
| 351 |
try {
|
|
@@ -353,9 +345,7 @@ D) Cost reduction
|
|
| 353 |
if (!res.ok) return;
|
| 354 |
const j = await res.json();
|
| 355 |
const pct = typeof j?.progress_pct === 'number' ? j.progress_pct : null;
|
| 356 |
-
if (pct !== null)
|
| 357 |
-
setMemoryProgress(Math.round(pct * 100));
|
| 358 |
-
}
|
| 359 |
} catch {
|
| 360 |
// ignore
|
| 361 |
}
|
|
@@ -375,15 +365,10 @@ D) Cost reduction
|
|
| 375 |
/>
|
| 376 |
|
| 377 |
<div className="flex-1 flex overflow-hidden">
|
| 378 |
-
{/* Mobile Sidebar Toggle - Left */}
|
| 379 |
{leftSidebarOpen && (
|
| 380 |
-
<div
|
| 381 |
-
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
| 382 |
-
onClick={() => setLeftSidebarOpen(false)}
|
| 383 |
-
/>
|
| 384 |
)}
|
| 385 |
|
| 386 |
-
{/* Left Sidebar */}
|
| 387 |
<aside
|
| 388 |
className={`
|
| 389 |
fixed lg:static inset-y-0 left-0 z-50
|
|
@@ -401,6 +386,7 @@ D) Cost reduction
|
|
| 401 |
<X className="h-5 w-5" />
|
| 402 |
</Button>
|
| 403 |
</div>
|
|
|
|
| 404 |
<LeftSidebar
|
| 405 |
learningMode={learningMode}
|
| 406 |
language={language}
|
|
@@ -412,7 +398,6 @@ D) Cost reduction
|
|
| 412 |
/>
|
| 413 |
</aside>
|
| 414 |
|
| 415 |
-
{/* Main Chat Area */}
|
| 416 |
<main className="flex-1 flex flex-col min-w-0">
|
| 417 |
<ChatArea
|
| 418 |
messages={messages}
|
|
@@ -424,7 +409,7 @@ D) Cost reduction
|
|
| 424 |
onUploadSingle={handleUploadSingle}
|
| 425 |
onUploadAllPending={handleUploadAllPending}
|
| 426 |
memoryProgress={memoryProgress}
|
| 427 |
-
isLoggedIn={
|
| 428 |
learningMode={learningMode}
|
| 429 |
onClearConversation={handleClearConversation}
|
| 430 |
onLearningModeChange={setLearningMode}
|
|
@@ -432,15 +417,10 @@ D) Cost reduction
|
|
| 432 |
/>
|
| 433 |
</main>
|
| 434 |
|
| 435 |
-
{/* Mobile Sidebar Toggle - Right */}
|
| 436 |
{rightPanelOpen && (
|
| 437 |
-
<div
|
| 438 |
-
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
| 439 |
-
onClick={() => setRightPanelOpen(false)}
|
| 440 |
-
/>
|
| 441 |
)}
|
| 442 |
|
| 443 |
-
{/* Right Panel */}
|
| 444 |
{rightPanelVisible && (
|
| 445 |
<aside
|
| 446 |
className={`
|
|
@@ -474,7 +454,6 @@ D) Cost reduction
|
|
| 474 |
</aside>
|
| 475 |
)}
|
| 476 |
|
| 477 |
-
{/* Toggle Right Panel Button - Desktop only */}
|
| 478 |
<Button
|
| 479 |
variant="outline"
|
| 480 |
size="icon"
|
|
@@ -487,7 +466,6 @@ D) Cost reduction
|
|
| 487 |
{rightPanelVisible ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
|
| 488 |
</Button>
|
| 489 |
|
| 490 |
-
{/* Floating Action Buttons - Desktop only, when panel is closed */}
|
| 491 |
{!rightPanelVisible && (
|
| 492 |
<FloatingActionButtons
|
| 493 |
user={user}
|
|
|
|
| 126 |
const [exportResult, setExportResult] = useState('');
|
| 127 |
const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
|
| 128 |
|
|
|
|
| 129 |
const [groupMembers] = useState<GroupMember[]>([
|
| 130 |
{ id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
|
| 131 |
{ id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
|
|
|
|
| 139 |
}, [isDarkMode]);
|
| 140 |
|
| 141 |
const userId = useMemo(() => {
|
| 142 |
+
// 登录时:email 作为 user_id;不登录也可跑通:0405
|
|
|
|
| 143 |
return (user?.email || '0405').trim();
|
| 144 |
}, [user]);
|
| 145 |
|
| 146 |
+
const isLoggedIn = useMemo(() => {
|
| 147 |
+
// 你如果希望“必须登录才能聊天/上传”,这里改回 !!user
|
| 148 |
+
// 目前为了便于调试后端,允许未登录也走通(user_id=0405)
|
| 149 |
+
return true;
|
| 150 |
+
}, []);
|
| 151 |
+
|
| 152 |
const currentDocTypeForChat = useMemo(() => {
|
| 153 |
+
const hasSyllabus = uploadedFiles.some((f) => f.type === 'syllabus' && f.uploaded);
|
|
|
|
| 154 |
if (hasSyllabus) return 'Syllabus';
|
| 155 |
+
const hasSlides = uploadedFiles.some((f) => f.type === 'lecture-slides' && f.uploaded);
|
| 156 |
if (hasSlides) return 'Lecture Slides';
|
| 157 |
+
const hasLit = uploadedFiles.some((f) => f.type === 'literature-review' && f.uploaded);
|
| 158 |
if (hasLit) return 'Literature Review / Paper';
|
| 159 |
return 'Other';
|
| 160 |
}, [uploadedFiles]);
|
|
|
|
| 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.email, name: user.name, email: user.email }
|
|
|
|
| 176 |
timestamp: new Date(),
|
| 177 |
sender,
|
| 178 |
};
|
| 179 |
+
setMessages((prev) => [...prev, userMessage]);
|
| 180 |
|
| 181 |
if (!shouldAIRespond) return;
|
| 182 |
|
|
|
|
| 190 |
});
|
| 191 |
|
| 192 |
const refs = (resp.refs || [])
|
| 193 |
+
.map((r) => [r.source_file, r.section].filter(Boolean).join(' / '))
|
| 194 |
.filter(Boolean);
|
| 195 |
|
| 196 |
const assistantMessage: Message = {
|
|
|
|
| 199 |
content: resp.reply || '(empty reply)',
|
| 200 |
timestamp: new Date(),
|
| 201 |
references: refs.length ? refs : undefined,
|
| 202 |
+
sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
|
| 203 |
};
|
| 204 |
|
| 205 |
+
setMessages((prev) => [...prev, assistantMessage]);
|
| 206 |
} catch (e: any) {
|
| 207 |
console.error(e);
|
| 208 |
toast.error(`Chat failed: ${e?.message || 'unknown error'}`);
|
| 209 |
+
setMessages((prev) => [
|
| 210 |
+
...prev,
|
| 211 |
+
{
|
| 212 |
+
id: (Date.now() + 1).toString(),
|
| 213 |
+
role: 'assistant',
|
| 214 |
+
content: `Sorry — chat request failed. ${e?.message || ''}`,
|
| 215 |
+
timestamp: new Date(),
|
| 216 |
+
},
|
| 217 |
+
]);
|
| 218 |
}
|
| 219 |
};
|
| 220 |
|
| 221 |
+
// ✅ 选文件:只入库,不上传
|
| 222 |
const handleFileUpload = (files: File[]) => {
|
| 223 |
+
const newFiles: UploadedFile[] = files.map((file) => ({
|
| 224 |
file,
|
| 225 |
type: 'other',
|
| 226 |
uploaded: false,
|
| 227 |
}));
|
| 228 |
+
setUploadedFiles((prev) => [...prev, ...newFiles]);
|
| 229 |
toast.message('Files added. Select a File Type, then click Upload.');
|
| 230 |
};
|
| 231 |
|
| 232 |
const handleRemoveFile = (index: number) => {
|
| 233 |
+
setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
|
| 234 |
};
|
| 235 |
|
| 236 |
const handleFileTypeChange = (index: number, type: FileType) => {
|
| 237 |
+
setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f)));
|
|
|
|
|
|
|
| 238 |
};
|
| 239 |
|
| 240 |
+
// ✅ 上传单个文件(关键:读取“最新的 type”,写入 doc_type)
|
| 241 |
const handleUploadSingle = async (index: number) => {
|
| 242 |
const target = uploadedFiles[index];
|
| 243 |
if (!target) return;
|
|
|
|
| 245 |
try {
|
| 246 |
const form = new FormData();
|
| 247 |
form.append('user_id', userId);
|
| 248 |
+
form.append('doc_type', mapFileTypeToDocType(target.type));
|
| 249 |
form.append('file', target.file);
|
| 250 |
|
| 251 |
const r = await apiPostForm<UploadApiResp>('/api/upload', form);
|
| 252 |
+
if (!r.ok) throw new Error('Upload response ok=false');
|
| 253 |
|
| 254 |
+
setUploadedFiles((prev) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
prev.map((x, i) =>
|
| 256 |
+
i === index ? { ...x, uploaded: true, uploadedChunks: r.added_chunks } : x
|
|
|
|
|
|
|
| 257 |
)
|
| 258 |
);
|
| 259 |
|
|
|
|
| 264 |
}
|
| 265 |
};
|
| 266 |
|
| 267 |
+
// ✅ 上传所有未上传
|
| 268 |
const handleUploadAllPending = async () => {
|
| 269 |
const pendingIdx = uploadedFiles
|
| 270 |
.map((f, i) => ({ f, i }))
|
| 271 |
+
.filter((x) => !x.f.uploaded)
|
| 272 |
+
.map((x) => x.i);
|
| 273 |
|
| 274 |
if (!pendingIdx.length) {
|
| 275 |
toast.message('No pending files to upload.');
|
|
|
|
| 277 |
}
|
| 278 |
|
| 279 |
for (const idx of pendingIdx) {
|
| 280 |
+
// 顺序上传便于 debug
|
| 281 |
// eslint-disable-next-line no-await-in-loop
|
| 282 |
await handleUploadSingle(idx);
|
| 283 |
}
|
|
|
|
| 295 |
]);
|
| 296 |
};
|
| 297 |
|
|
|
|
|
|
|
| 298 |
const handleExport = async () => {
|
| 299 |
try {
|
| 300 |
const r = await apiPostJson<{ markdown: string }>('/api/export', {
|
|
|
|
| 324 |
}
|
| 325 |
};
|
| 326 |
|
|
|
|
| 327 |
const handleQuiz = () => {
|
| 328 |
const quiz = `# Micro-Quiz: Responsible AI
|
| 329 |
|
|
|
|
| 338 |
toast.success('Quiz generated!');
|
| 339 |
};
|
| 340 |
|
|
|
|
| 341 |
useEffect(() => {
|
| 342 |
const run = async () => {
|
| 343 |
try {
|
|
|
|
| 345 |
if (!res.ok) return;
|
| 346 |
const j = await res.json();
|
| 347 |
const pct = typeof j?.progress_pct === 'number' ? j.progress_pct : null;
|
| 348 |
+
if (pct !== null) setMemoryProgress(Math.round(pct * 100));
|
|
|
|
|
|
|
| 349 |
} catch {
|
| 350 |
// ignore
|
| 351 |
}
|
|
|
|
| 365 |
/>
|
| 366 |
|
| 367 |
<div className="flex-1 flex overflow-hidden">
|
|
|
|
| 368 |
{leftSidebarOpen && (
|
| 369 |
+
<div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
|
|
|
|
|
|
|
|
|
|
| 370 |
)}
|
| 371 |
|
|
|
|
| 372 |
<aside
|
| 373 |
className={`
|
| 374 |
fixed lg:static inset-y-0 left-0 z-50
|
|
|
|
| 386 |
<X className="h-5 w-5" />
|
| 387 |
</Button>
|
| 388 |
</div>
|
| 389 |
+
|
| 390 |
<LeftSidebar
|
| 391 |
learningMode={learningMode}
|
| 392 |
language={language}
|
|
|
|
| 398 |
/>
|
| 399 |
</aside>
|
| 400 |
|
|
|
|
| 401 |
<main className="flex-1 flex flex-col min-w-0">
|
| 402 |
<ChatArea
|
| 403 |
messages={messages}
|
|
|
|
| 409 |
onUploadSingle={handleUploadSingle}
|
| 410 |
onUploadAllPending={handleUploadAllPending}
|
| 411 |
memoryProgress={memoryProgress}
|
| 412 |
+
isLoggedIn={isLoggedIn}
|
| 413 |
learningMode={learningMode}
|
| 414 |
onClearConversation={handleClearConversation}
|
| 415 |
onLearningModeChange={setLearningMode}
|
|
|
|
| 417 |
/>
|
| 418 |
</main>
|
| 419 |
|
|
|
|
| 420 |
{rightPanelOpen && (
|
| 421 |
+
<div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
|
|
|
|
|
|
|
|
|
|
| 422 |
)}
|
| 423 |
|
|
|
|
| 424 |
{rightPanelVisible && (
|
| 425 |
<aside
|
| 426 |
className={`
|
|
|
|
| 454 |
</aside>
|
| 455 |
)}
|
| 456 |
|
|
|
|
| 457 |
<Button
|
| 458 |
variant="outline"
|
| 459 |
size="icon"
|
|
|
|
| 466 |
{rightPanelVisible ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
|
| 467 |
</Button>
|
| 468 |
|
|
|
|
| 469 |
{!rightPanelVisible && (
|
| 470 |
<FloatingActionButtons
|
| 471 |
user={user}
|