dvc890 commited on
Commit
ea14047
·
verified ·
1 Parent(s): b06457b

Update components/ai/CommentGeneratorPanel.tsx

Browse files
Files changed (1) hide show
  1. components/ai/CommentGeneratorPanel.tsx +173 -121
components/ai/CommentGeneratorPanel.tsx CHANGED
@@ -1,7 +1,6 @@
1
-
2
  import React, { useState, useEffect, useRef } from 'react';
3
- import { User, StudentProfile, SavedRecord, Zodiac, ClassRole, Discipline, AcademicQuality, Personality, Hobby, Labor } from '../../types';
4
- import { FileSpreadsheet, Loader2, Sparkles, Copy, Save, Trash2, Download, RefreshCw, PenTool, CheckCircle, Circle, Edit, Box, Upload } from 'lucide-react';
5
  import { Toast, ToastState } from '../Toast';
6
  import { ConfirmModal } from '../ConfirmModal';
7
 
@@ -16,6 +15,9 @@ const HOBBIES: Hobby[] = ['体育', '音乐', '舞蹈', '绘画', '书法', '手
16
  export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ currentUser }) => {
17
  // --- State ---
18
  const [viewMode, setViewMode] = useState<'GENERATOR' | 'MANAGER'>('GENERATOR');
 
 
 
19
  const [importedNames, setImportedNames] = useState<string[]>([]);
20
  const [savedRecords, setSavedRecords] = useState<SavedRecord[]>([]);
21
 
@@ -169,6 +171,11 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
169
  const handleGenerate = async () => {
170
  if (!profile.name) return setToast({ show: true, message: '请输入或选择学生姓名', type: 'error' });
171
 
 
 
 
 
 
172
  setIsGenerating(true);
173
  setGeneratedComment('');
174
  abortControllerRef.current = new AbortController();
@@ -207,7 +214,7 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
207
  body: JSON.stringify({
208
  text: userPrompt,
209
  overrideSystemPrompt: systemPrompt,
210
- enableThinking: false, // Turn off thinking for faster generation
211
  disableAudio: true
212
  }),
213
  signal: abortControllerRef.current.signal
@@ -223,7 +230,7 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
223
  if (done) break;
224
  const chunk = decoder.decode(value, { stream: true });
225
  const lines = chunk.split('\n\n');
226
- for (const line of lines) {
227
  if (line.startsWith('data: ')) {
228
  try {
229
  const data = JSON.parse(line.replace('data: ', ''));
@@ -257,12 +264,18 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
257
  setSavedRecords([newRecord, ...filtered]);
258
  setToast({ show: true, message: '已保存到管理箱', type: 'success' });
259
 
260
- // Auto-advance logic: If importing, pick next name?
261
  if (importedNames.length > 0) {
262
  const idx = importedNames.indexOf(profile.name);
263
  if (idx !== -1 && idx < importedNames.length - 1) {
264
  setProfile(p => ({ ...p, name: importedNames[idx + 1] }));
265
  setGeneratedComment(''); // Clear for next
 
 
 
 
 
 
266
  }
267
  }
268
  };
@@ -348,34 +361,41 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
348
  <ConfirmModal isOpen={confirmModal.isOpen} title={confirmModal.title} message={confirmModal.message} onClose={()=>setConfirmModal({...confirmModal, isOpen: false})} onConfirm={confirmModal.onConfirm}/>
349
 
350
  {/* Top Bar */}
351
- <div className="bg-white border-b border-amber-100 px-6 py-3 flex justify-between items-center shrink-0 shadow-sm z-10">
352
- <div className="flex items-center gap-2 text-amber-700 font-bold text-lg">
353
- <PenTool className="fill-amber-500 text-amber-600" />
354
- 暖心评语助手
 
355
  </div>
356
  <div className="flex bg-amber-100/50 p-1 rounded-lg">
357
- <button onClick={()=>setViewMode('GENERATOR')} className={`px-4 py-1.5 text-sm font-bold rounded-md transition-all flex items-center gap-2 ${viewMode==='GENERATOR' ? 'bg-white text-amber-600 shadow-sm' : 'text-amber-800/60'}`}>
358
- <Sparkles size={16}/> 生成器
359
  </button>
360
- <button onClick={()=>setViewMode('MANAGER')} className={`px-4 py-1.5 text-sm font-bold rounded-md transition-all flex items-center gap-2 ${viewMode==='MANAGER' ? 'bg-white text-amber-600 shadow-sm' : 'text-amber-800/60'}`}>
361
- <Box size={16}/> 管理箱
362
- {savedRecords.length > 0 && <span className="bg-red-500 text-white text-[10px] px-1.5 rounded-full">{savedRecords.length}</span>}
363
  </button>
364
  </div>
365
  </div>
366
 
367
  {/* GENERATOR MODE */}
368
  {viewMode === 'GENERATOR' && (
369
- <div className="flex-1 overflow-hidden flex flex-col md:flex-row">
370
- {/* Left: Controls */}
371
- <div className="w-full md:w-80 lg:w-96 border-r border-amber-100 bg-white overflow-y-auto p-6 custom-scrollbar flex flex-col gap-6">
372
- {/* 1. Name Input */}
373
- <div className="space-y-3">
374
- <label className="text-xs font-bold text-gray-500 uppercase block">学生姓名</label>
 
 
 
 
 
 
375
  {importedNames.length > 0 ? (
376
  <div className="relative">
377
  <select
378
- className="w-full border border-amber-200 rounded-lg p-2.5 bg-amber-50/50 text-sm appearance-none focus:ring-2 focus:ring-amber-400 outline-none"
379
  value={profile.name}
380
  onChange={e => setProfile({...profile, name: e.target.value})}
381
  >
@@ -385,38 +405,40 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
385
  </option>
386
  ))}
387
  </select>
 
388
  </div>
389
  ) : (
390
  <div className="flex gap-2">
391
  <input
392
- className="flex-1 border border-gray-200 rounded-lg p-2 text-sm focus:ring-2 focus:ring-amber-400 outline-none"
393
  placeholder="输入姓名..."
394
  value={profile.name}
395
  onChange={e => setProfile({...profile, name: e.target.value})}
396
  />
397
  <button
398
  onClick={() => fileInputRef.current?.click()}
399
- className="px-3 bg-green-50 text-green-600 rounded-lg border border-green-200 hover:bg-green-100 flex items-center"
400
  title="Excel 导入"
401
  >
402
- <FileSpreadsheet size={18}/>
403
  </button>
404
  <input type="file" accept=".xlsx,.xls,.csv" ref={fileInputRef} className="hidden" onChange={handleFileUpload} />
405
  </div>
406
  )}
407
- {isImporting && <div className="text-xs text-gray-400 flex items-center gap-1"><Loader2 size={10} className="animate-spin"/> 正在解析名单...</div>}
408
  </div>
409
 
410
- {/* 2. Attributes */}
411
- <div className="space-y-4">
 
412
  <div>
413
- <label className="text-xs font-bold text-gray-500 uppercase block mb-1">生肖年份</label>
414
- <div className="flex flex-wrap gap-2">
415
  {ZODIACS.map(z => (
416
  <button
417
  key={z}
418
  onClick={() => setProfile({...profile, zodiacYear: z})}
419
- className={`text-xs px-2 py-1 rounded border ${profile.zodiacYear === z ? 'bg-red-50 text-red-600 border-red-200 font-bold' : 'bg-gray-50 text-gray-500 border-gray-100 hover:bg-gray-100'}`}
420
  >
421
  {z}
422
  </button>
@@ -427,25 +449,22 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
427
  <div className="grid grid-cols-2 gap-4">
428
  <div>
429
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">班干部</label>
430
- <select className="w-full text-sm border border-gray-200 rounded p-2" value={profile.classRole} onChange={e=>setProfile({...profile, classRole: e.target.value as any})}>
431
  <option value="NO">否</option>
432
  <option value="YES">是 (重点表扬)</option>
433
  </select>
434
  </div>
435
  <div>
436
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">纪律表现</label>
437
- <select className="w-full text-sm border border-gray-200 rounded p-2" value={profile.discipline} onChange={e=>setProfile({...profile, discipline: e.target.value as any})}>
438
  <option value="YES">遵守纪律</option>
439
  <option value="AVERAGE">一般</option>
440
  <option value="NO">较差 (需提醒)</option>
441
  </select>
442
  </div>
443
- </div>
444
-
445
- <div className="grid grid-cols-2 gap-4">
446
  <div>
447
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">学业质量</label>
448
- <select className="w-full text-sm border border-gray-200 rounded p-2" value={profile.academic} onChange={e=>setProfile({...profile, academic: e.target.value as any})}>
449
  <option value="EXCELLENT">优秀</option>
450
  <option value="GOOD">良好</option>
451
  <option value="PASS">合格</option>
@@ -454,7 +473,7 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
454
  </div>
455
  <div>
456
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">劳动表现</label>
457
- <select className="w-full text-sm border border-gray-200 rounded p-2" value={profile.labor} onChange={e=>setProfile({...profile, labor: e.target.value as any})}>
458
  <option value="热爱">热爱劳动</option>
459
  <option value="一般">一般</option>
460
  <option value="较少参与">较少参与</option>
@@ -462,14 +481,15 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
462
  </div>
463
  </div>
464
 
 
465
  <div>
466
- <label className="text-xs font-bold text-gray-500 uppercase block mb-1">性格特点</label>
467
- <div className="flex flex-wrap gap-2">
468
  {PERSONALITIES.map(p => (
469
  <button
470
  key={p}
471
  onClick={() => setProfile({...profile, personality: p})}
472
- className={`text-xs px-2 py-1 rounded border transition-colors ${profile.personality === p ? 'bg-blue-50 text-blue-600 border-blue-200 font-bold' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}
473
  >
474
  {p}
475
  </button>
@@ -477,73 +497,105 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
477
  </div>
478
  </div>
479
 
480
- <div>
481
- <label className="text-xs font-bold text-gray-500 uppercase block mb-1">兴趣爱好</label>
482
- <select className="w-full text-sm border border-gray-200 rounded p-2" value={profile.hobby} onChange={e=>setProfile({...profile, hobby: e.target.value as any})}>
483
- {HOBBIES.map(h => <option key={h} value={h}>{h}</option>)}
484
- </select>
485
- </div>
486
-
487
- <div>
488
- <label className="text-xs font-bold text-gray-500 uppercase block mb-1">字数控制: {profile.wordCount}</label>
489
- <input type="range" min="50" max="300" step="10" className="w-full accent-amber-500" value={profile.wordCount} onChange={e=>setProfile({...profile, wordCount: Number(e.target.value)})}/>
 
 
490
  </div>
491
  </div>
492
 
493
- <button
494
- onClick={handleGenerate}
495
- disabled={isGenerating || !profile.name}
496
- className="w-full py-3 bg-gradient-to-r from-amber-500 to-orange-500 text-white rounded-xl font-bold shadow-md hover:shadow-lg transition-all flex items-center justify-center gap-2 disabled:opacity-50"
497
- >
498
- {isGenerating ? <Loader2 className="animate-spin" size={20}/> : <Sparkles size={20}/>}
499
- {isGenerating ? 'AI 正在撰写...' : '生成评语'}
500
- </button>
 
 
 
501
  </div>
502
 
503
- {/* Right: Preview */}
504
- <div className="flex-1 p-6 flex flex-col items-center justify-center bg-amber-50/50 relative">
505
- {generatedComment ? (
506
- <div className="w-full max-w-2xl bg-white rounded-2xl shadow-xl border border-amber-100 p-8 relative animate-in fade-in zoom-in-95">
507
- <div className="absolute -top-4 left-1/2 -translate-x-1/2 bg-amber-100 text-amber-800 px-4 py-1 rounded-full text-xs font-bold shadow-sm border border-amber-200">
508
- {profile.name} 的评语
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  </div>
510
- <textarea
511
- className="w-full min-h-[200px] text-lg leading-loose text-gray-700 outline-none resize-none bg-transparent font-medium"
512
- value={generatedComment}
513
- onChange={(e) => setGeneratedComment(e.target.value)}
514
- />
515
- <div className="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-100">
516
- <button onClick={() => { navigator.clipboard.writeText(generatedComment); setToast({show:true, message:'已复制', type:'success'}); }} className="flex items-center gap-1 text-gray-500 hover:text-blue-600 px-3 py-1.5 rounded hover:bg-gray-50 transition-colors">
517
- <Copy size={16}/> 复制
518
- </button>
519
- <button onClick={handleSave} className="flex items-center gap-2 bg-amber-500 text-white px-6 py-2 rounded-lg font-bold hover:bg-amber-600 shadow-md transition-transform active:scale-95">
520
- <Save size={18}/> 保存到管理箱
521
  </button>
522
  </div>
523
- </div>
524
- ) : (
525
- <div className="text-center text-amber-800/40">
526
- <Sparkles size={64} className="mx-auto mb-4 opacity-50"/>
527
- <p className="text-lg font-bold">等待生成...</p>
528
- <p className="text-sm mt-2">请在左侧配置学生信息并点击生成</p>
529
- </div>
530
- )}
531
  </div>
532
  </div>
533
  )}
534
 
535
  {/* MANAGER MODE */}
536
  {viewMode === 'MANAGER' && (
537
- <div className="flex-1 overflow-hidden flex flex-col p-6">
538
- <div className="flex justify-between items-center mb-6">
539
- <div className="flex gap-2">
540
- <button onClick={handleExportWord} disabled={isExporting} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-bold flex items-center gap-2 hover:bg-blue-700 shadow-sm disabled:opacity-50 transition-colors">
541
  {isExporting ? <Loader2 className="animate-spin" size={18}/> : <Download size={18}/>}
542
- 导出 Word 文档
543
  </button>
544
- <div className="relative group">
545
- <button className="bg-white text-gray-600 border border-gray-200 px-4 py-2 rounded-lg font-bold flex items-center gap-2 hover:bg-gray-50 shadow-sm">
546
- <RefreshCw size={16}/> 批量同步年份
547
  </button>
548
  <div className="absolute top-full left-0 mt-2 w-32 bg-white border border-gray-200 rounded-lg shadow-xl hidden group-hover:block z-20 py-1">
549
  {ZODIACS.map(z => (
@@ -554,36 +606,36 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
554
  </div>
555
  </div>
556
  </div>
557
- <div className="text-sm text-gray-500 font-bold">
558
- 已保存 {savedRecords.length} 条记录
559
  </div>
560
  </div>
561
 
562
- <div className="flex-1 overflow-y-auto custom-scrollbar">
563
  {savedRecords.length === 0 ? (
564
  <div className="h-full flex flex-col items-center justify-center text-gray-400 border-2 border-dashed border-gray-200 rounded-2xl bg-white/50">
565
  <Box size={48} className="mb-4 opacity-50"/>
566
  <p>管理箱是空的</p>
567
  </div>
568
  ) : (
569
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pb-10">
570
  {savedRecords.map(record => (
571
- <div key={record.id} className="bg-white rounded-xl shadow-sm border border-gray-100 p-5 flex flex-col hover:shadow-md transition-shadow group">
572
  <div className="flex justify-between items-center mb-3">
573
  <div className="font-bold text-lg text-gray-800 flex items-center gap-2">
574
  {record.name}
575
- <span className="text-xs bg-amber-100 text-amber-800 px-2 py-0.5 rounded-full border border-amber-200">{record.zodiac}年</span>
576
  </div>
577
- <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
578
  <button
579
  onClick={() => { setEditingRecordId(record.id); setEditForm(record); }}
580
- className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded"
581
  >
582
  <Edit size={16}/>
583
  </button>
584
  <button
585
  onClick={() => handleDeleteRecord(record.id)}
586
- className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded"
587
  >
588
  <Trash2 size={16}/>
589
  </button>
@@ -591,41 +643,41 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
591
  </div>
592
 
593
  {editingRecordId === record.id && editForm ? (
594
- <div className="flex-1 flex flex-col gap-2">
 
 
 
 
595
  <input
596
- className="w-full border rounded p-1 text-sm font-bold"
597
  value={editForm.name}
598
  onChange={e => setEditForm({...editForm, name: e.target.value})}
599
  />
600
  <textarea
601
- className="w-full border rounded p-2 text-sm resize-none flex-1 focus:ring-1 focus:ring-blue-500 outline-none"
602
- rows={5}
603
  value={editForm.comment}
604
  onChange={e => setEditForm({...editForm, comment: e.target.value})}
605
  />
606
- <div className="flex justify-end gap-2 mt-2">
607
- <button onClick={() => setEditingRecordId(null)} className="text-xs text-gray-500 px-2 py-1">取消</button>
608
- <button
609
- onClick={() => {
610
- setSavedRecords(prev => prev.map(r => r.id === record.id ? editForm! : r));
611
- setEditingRecordId(null);
612
- setToast({ show: true, message: '修改已保存', type: 'success' });
613
- }}
614
- className="text-xs bg-blue-600 text-white px-3 py-1 rounded"
615
- >
616
- 保存
617
- </button>
618
- </div>
619
  </div>
620
  ) : (
621
- <div className="flex-1 text-sm text-gray-600 leading-relaxed overflow-hidden text-ellipsis line-clamp-6 bg-gray-50 p-3 rounded-lg border border-gray-50">
622
  {record.comment}
623
  </div>
624
  )}
625
 
626
  <div className="mt-3 pt-3 border-t border-gray-100 flex justify-between items-center text-xs text-gray-400">
627
  <span>{new Date(record.timestamp).toLocaleDateString()}</span>
628
- <span className="font-mono">{record.comment.length}字</span>
629
  </div>
630
  </div>
631
  ))}
@@ -636,4 +688,4 @@ export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ cu
636
  )}
637
  </div>
638
  );
639
- };
 
 
1
  import React, { useState, useEffect, useRef } from 'react';
2
+ import { User, StudentProfile, SavedRecord, Zodiac, Personality, Hobby } from '../../types';
3
+ import { FileSpreadsheet, Loader2, Sparkles, Copy, Save, Trash2, Download, RefreshCw, PenTool, Edit, Box, ChevronLeft, Settings2, Check, X } from 'lucide-react';
4
  import { Toast, ToastState } from '../Toast';
5
  import { ConfirmModal } from '../ConfirmModal';
6
 
 
15
  export const CommentGeneratorPanel: React.FC<CommentGeneratorPanelProps> = ({ currentUser }) => {
16
  // --- State ---
17
  const [viewMode, setViewMode] = useState<'GENERATOR' | 'MANAGER'>('GENERATOR');
18
+ // Mobile View State: 'CONFIG' (Left) or 'RESULT' (Right)
19
+ const [mobileTab, setMobileTab] = useState<'CONFIG' | 'RESULT'>('CONFIG');
20
+
21
  const [importedNames, setImportedNames] = useState<string[]>([]);
22
  const [savedRecords, setSavedRecords] = useState<SavedRecord[]>([]);
23
 
 
171
  const handleGenerate = async () => {
172
  if (!profile.name) return setToast({ show: true, message: '请输入或选择学生姓名', type: 'error' });
173
 
174
+ // Mobile: Switch to result tab automatically when generating
175
+ if (window.innerWidth < 768) {
176
+ setMobileTab('RESULT');
177
+ }
178
+
179
  setIsGenerating(true);
180
  setGeneratedComment('');
181
  abortControllerRef.current = new AbortController();
 
214
  body: JSON.stringify({
215
  text: userPrompt,
216
  overrideSystemPrompt: systemPrompt,
217
+ enableThinking: false,
218
  disableAudio: true
219
  }),
220
  signal: abortControllerRef.current.signal
 
230
  if (done) break;
231
  const chunk = decoder.decode(value, { stream: true });
232
  const lines = chunk.split('\n\n');
233
+ for (const line of parts) {
234
  if (line.startsWith('data: ')) {
235
  try {
236
  const data = JSON.parse(line.replace('data: ', ''));
 
264
  setSavedRecords([newRecord, ...filtered]);
265
  setToast({ show: true, message: '已保存到管理箱', type: 'success' });
266
 
267
+ // Auto-advance logic
268
  if (importedNames.length > 0) {
269
  const idx = importedNames.indexOf(profile.name);
270
  if (idx !== -1 && idx < importedNames.length - 1) {
271
  setProfile(p => ({ ...p, name: importedNames[idx + 1] }));
272
  setGeneratedComment(''); // Clear for next
273
+
274
+ // If mobile, ask if they want to go back to config to generate next
275
+ if (window.innerWidth < 768) {
276
+ // Stay on result for a moment or give a toast hint
277
+ setToast({ show: true, message: `已保存。已切换到下一位:${importedNames[idx + 1]}`, type: 'success' });
278
+ }
279
  }
280
  }
281
  };
 
361
  <ConfirmModal isOpen={confirmModal.isOpen} title={confirmModal.title} message={confirmModal.message} onClose={()=>setConfirmModal({...confirmModal, isOpen: false})} onConfirm={confirmModal.onConfirm}/>
362
 
363
  {/* Top Bar */}
364
+ <div className="bg-white border-b border-amber-100 px-4 md:px-6 py-3 flex justify-between items-center shrink-0 shadow-sm z-20">
365
+ <div className="flex items-center gap-2 text-amber-700 font-bold text-lg truncate">
366
+ <PenTool className="fill-amber-500 text-amber-600 shrink-0" />
367
+ <span className="hidden md:inline">暖心评语助手</span>
368
+ <span className="md:hidden">��语助手</span>
369
  </div>
370
  <div className="flex bg-amber-100/50 p-1 rounded-lg">
371
+ <button onClick={()=>setViewMode('GENERATOR')} className={`px-3 md:px-4 py-1.5 text-xs md:text-sm font-bold rounded-md transition-all flex items-center gap-1 md:gap-2 ${viewMode==='GENERATOR' ? 'bg-white text-amber-600 shadow-sm' : 'text-amber-800/60'}`}>
372
+ <Sparkles size={14}/> 生成器
373
  </button>
374
+ <button onClick={()=>setViewMode('MANAGER')} className={`px-3 md:px-4 py-1.5 text-xs md:text-sm font-bold rounded-md transition-all flex items-center gap-1 md:gap-2 ${viewMode==='MANAGER' ? 'bg-white text-amber-600 shadow-sm' : 'text-amber-800/60'}`}>
375
+ <Box size={14}/> 管理箱
376
+ {savedRecords.length > 0 && <span className="bg-red-500 text-white text-[10px] px-1.5 rounded-full ml-1">{savedRecords.length}</span>}
377
  </button>
378
  </div>
379
  </div>
380
 
381
  {/* GENERATOR MODE */}
382
  {viewMode === 'GENERATOR' && (
383
+ <div className="flex-1 overflow-hidden flex relative">
384
+
385
+ {/* --- Left Panel: Controls --- */}
386
+ {/* Mobile: Shown only when mobileTab is CONFIG */}
387
+ {/* Desktop: Always shown, fixed width 450px */}
388
+ <div className={`
389
+ flex flex-col bg-white border-r border-amber-100 h-full transition-all duration-300 absolute md:relative z-10 w-full md:w-[450px] shrink-0
390
+ ${mobileTab === 'CONFIG' ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
391
+ `}>
392
+ {/* 1. Header (Name Input) */}
393
+ <div className="p-4 border-b border-gray-100 bg-gray-50/50 shrink-0">
394
+ <label className="text-xs font-bold text-gray-500 uppercase block mb-2">当前学生</label>
395
  {importedNames.length > 0 ? (
396
  <div className="relative">
397
  <select
398
+ className="w-full border border-amber-300 rounded-xl p-3 bg-white text-base font-bold text-gray-800 shadow-sm focus:ring-2 focus:ring-amber-400 outline-none appearance-none"
399
  value={profile.name}
400
  onChange={e => setProfile({...profile, name: e.target.value})}
401
  >
 
405
  </option>
406
  ))}
407
  </select>
408
+ <div className="absolute right-3 top-3.5 text-gray-400 pointer-events-none text-xs">▼</div>
409
  </div>
410
  ) : (
411
  <div className="flex gap-2">
412
  <input
413
+ className="flex-1 border border-gray-300 rounded-xl p-3 text-sm focus:ring-2 focus:ring-amber-400 outline-none shadow-sm"
414
  placeholder="输入姓名..."
415
  value={profile.name}
416
  onChange={e => setProfile({...profile, name: e.target.value})}
417
  />
418
  <button
419
  onClick={() => fileInputRef.current?.click()}
420
+ className="px-4 bg-green-50 text-green-600 rounded-xl border border-green-200 hover:bg-green-100 flex items-center shadow-sm"
421
  title="Excel 导入"
422
  >
423
+ <FileSpreadsheet size={20}/>
424
  </button>
425
  <input type="file" accept=".xlsx,.xls,.csv" ref={fileInputRef} className="hidden" onChange={handleFileUpload} />
426
  </div>
427
  )}
428
+ {isImporting && <div className="text-xs text-gray-400 mt-2 flex items-center gap-1"><Loader2 size={10} className="animate-spin"/> 正在解析名单...</div>}
429
  </div>
430
 
431
+ {/* 2. Scrollable Config Body */}
432
+ <div className="flex-1 overflow-y-auto p-4 custom-scrollbar space-y-5 pb-24 md:pb-6">
433
+ {/* Zodiac */}
434
  <div>
435
+ <label className="text-xs font-bold text-gray-500 uppercase block mb-2">生肖年份 (隐喻主题)</label>
436
+ <div className="grid grid-cols-6 gap-2">
437
  {ZODIACS.map(z => (
438
  <button
439
  key={z}
440
  onClick={() => setProfile({...profile, zodiacYear: z})}
441
+ className={`text-sm py-1.5 rounded-lg border transition-all ${profile.zodiacYear === z ? 'bg-red-50 text-red-600 border-red-300 font-bold shadow-sm' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'}`}
442
  >
443
  {z}
444
  </button>
 
449
  <div className="grid grid-cols-2 gap-4">
450
  <div>
451
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">班干部</label>
452
+ <select className="w-full text-sm border border-gray-300 rounded-lg p-2.5 bg-white" value={profile.classRole} onChange={e=>setProfile({...profile, classRole: e.target.value as any})}>
453
  <option value="NO">否</option>
454
  <option value="YES">是 (重点表扬)</option>
455
  </select>
456
  </div>
457
  <div>
458
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">纪律表现</label>
459
+ <select className="w-full text-sm border border-gray-300 rounded-lg p-2.5 bg-white" value={profile.discipline} onChange={e=>setProfile({...profile, discipline: e.target.value as any})}>
460
  <option value="YES">遵守纪律</option>
461
  <option value="AVERAGE">一般</option>
462
  <option value="NO">较差 (需提醒)</option>
463
  </select>
464
  </div>
 
 
 
465
  <div>
466
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">学业质量</label>
467
+ <select className="w-full text-sm border border-gray-300 rounded-lg p-2.5 bg-white" value={profile.academic} onChange={e=>setProfile({...profile, academic: e.target.value as any})}>
468
  <option value="EXCELLENT">优秀</option>
469
  <option value="GOOD">良好</option>
470
  <option value="PASS">合格</option>
 
473
  </div>
474
  <div>
475
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">劳动表现</label>
476
+ <select className="w-full text-sm border border-gray-300 rounded-lg p-2.5 bg-white" value={profile.labor} onChange={e=>setProfile({...profile, labor: e.target.value as any})}>
477
  <option value="热爱">热爱劳动</option>
478
  <option value="一般">一般</option>
479
  <option value="较少参与">较少参与</option>
 
481
  </div>
482
  </div>
483
 
484
+ {/* Personality */}
485
  <div>
486
+ <label className="text-xs font-bold text-gray-500 uppercase block mb-2">性格特点</label>
487
+ <div className="grid grid-cols-3 gap-2">
488
  {PERSONALITIES.map(p => (
489
  <button
490
  key={p}
491
  onClick={() => setProfile({...profile, personality: p})}
492
+ className={`text-xs py-1.5 rounded border transition-colors ${profile.personality === p ? 'bg-blue-50 text-blue-600 border-blue-300 font-bold' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}
493
  >
494
  {p}
495
  </button>
 
497
  </div>
498
  </div>
499
 
500
+ {/* Hobby & Length */}
501
+ <div className="grid grid-cols-2 gap-4">
502
+ <div>
503
+ <label className="text-xs font-bold text-gray-500 uppercase block mb-1">兴趣爱好</label>
504
+ <select className="w-full text-sm border border-gray-300 rounded-lg p-2.5 bg-white" value={profile.hobby} onChange={e=>setProfile({...profile, hobby: e.target.value as any})}>
505
+ {HOBBIES.map(h => <option key={h} value={h}>{h}</option>)}
506
+ </select>
507
+ </div>
508
+ <div>
509
+ <label className="text-xs font-bold text-gray-500 uppercase block mb-1">字数: {profile.wordCount}</label>
510
+ <input type="range" min="50" max="300" step="10" className="w-full accent-amber-500 h-9" value={profile.wordCount} onChange={e=>setProfile({...profile, wordCount: Number(e.target.value)})}/>
511
+ </div>
512
  </div>
513
  </div>
514
 
515
+ {/* 3. Fixed Footer Button (Left Panel) */}
516
+ <div className="p-4 border-t border-gray-200 bg-white z-20 shrink-0">
517
+ <button
518
+ onClick={handleGenerate}
519
+ disabled={isGenerating || !profile.name}
520
+ className="w-full py-3 bg-gradient-to-r from-amber-500 to-orange-500 text-white rounded-xl font-bold shadow-lg hover:shadow-orange-200 transition-all flex items-center justify-center gap-2 disabled:opacity-50 transform active:scale-95"
521
+ >
522
+ {isGenerating ? <Loader2 className="animate-spin" size={20}/> : <Sparkles size={20}/>}
523
+ {isGenerating ? 'AI 正在撰写...' : '立即生成评语'}
524
+ </button>
525
+ </div>
526
  </div>
527
 
528
+ {/* --- Right Panel: Result Preview --- */}
529
+ {/* Mobile: Shown only when mobileTab is RESULT */}
530
+ {/* Desktop: Always shown, takes remaining space */}
531
+ <div className={`
532
+ flex-1 flex flex-col items-center justify-center bg-amber-50/50 relative overflow-hidden transition-all duration-300 absolute md:relative inset-0 md:inset-auto z-20
533
+ ${mobileTab === 'RESULT' ? 'translate-x-0' : 'translate-x-full md:translate-x-0'}
534
+ bg-white md:bg-amber-50/50
535
+ `}>
536
+ {/* Mobile Header for Result */}
537
+ <div className="md:hidden w-full p-4 border-b bg-white flex justify-between items-center shrink-0">
538
+ <button onClick={() => setMobileTab('CONFIG')} className="flex items-center text-gray-600 font-bold">
539
+ <ChevronLeft size={20}/> 返回配置
540
+ </button>
541
+ <span className="text-sm font-bold text-gray-800">生成结果</span>
542
+ <div className="w-6"></div>
543
+ </div>
544
+
545
+ <div className="w-full h-full p-4 md:p-8 overflow-y-auto flex flex-col items-center">
546
+ {generatedComment ? (
547
+ <div className="w-full max-w-2xl bg-white rounded-2xl shadow-xl border border-amber-100 p-6 md:p-8 relative animate-in fade-in zoom-in-95 flex flex-col h-full md:h-auto">
548
+ <div className="absolute -top-3 left-6 bg-amber-100 text-amber-800 px-4 py-1 rounded-full text-xs font-bold shadow-sm border border-amber-200 hidden md:block">
549
+ {profile.name} 的专属评语
550
+ </div>
551
+
552
+ <div className="flex justify-between items-center mb-4 md:hidden">
553
+ <span className="font-bold text-lg text-gray-800">{profile.name}</span>
554
+ <span className="text-xs bg-amber-100 text-amber-800 px-2 py-1 rounded">{profile.zodiacYear}年主题</span>
555
+ </div>
556
+
557
+ <textarea
558
+ className="w-full flex-1 md:min-h-[250px] text-base md:text-lg leading-loose text-gray-700 outline-none resize-none bg-transparent font-medium border-none p-0"
559
+ value={generatedComment}
560
+ onChange={(e) => setGeneratedComment(e.target.value)}
561
+ />
562
+
563
+ <div className="flex flex-col md:flex-row justify-end gap-3 mt-6 pt-4 border-t border-gray-100 shrink-0">
564
+ <button onClick={() => { navigator.clipboard.writeText(generatedComment); setToast({show:true, message:'已复制', type:'success'}); }} className="flex items-center justify-center gap-2 text-gray-600 hover:text-blue-600 px-4 py-2 rounded-lg bg-gray-50 hover:bg-blue-50 transition-colors font-bold border border-gray-200">
565
+ <Copy size={18}/> 复制文本
566
+ </button>
567
+ <button onClick={handleSave} className="flex items-center justify-center gap-2 bg-amber-500 text-white px-6 py-3 md:py-2 rounded-lg font-bold hover:bg-amber-600 shadow-md transition-transform active:scale-95">
568
+ <Save size={18}/> 保存到管理箱
569
+ </button>
570
+ </div>
571
  </div>
572
+ ) : (
573
+ <div className="text-center text-amber-800/40 flex flex-col items-center justify-center h-full">
574
+ <Settings2 size={64} className="mb-4 opacity-30"/>
575
+ <p className="text-lg font-bold">准备就绪</p>
576
+ <p className="text-sm mt-2 max-w-xs">在{window.innerWidth<768?'配置页':'左侧'}完善学生信息,AI 将为您生成暖心评语</p>
577
+ <button onClick={() => setMobileTab('CONFIG')} className="mt-6 md:hidden px-6 py-2 bg-amber-100 text-amber-700 rounded-full font-bold text-sm">
578
+ 去配置
 
 
 
 
579
  </button>
580
  </div>
581
+ )}
582
+ </div>
 
 
 
 
 
 
583
  </div>
584
  </div>
585
  )}
586
 
587
  {/* MANAGER MODE */}
588
  {viewMode === 'MANAGER' && (
589
+ <div className="flex-1 overflow-hidden flex flex-col p-4 md:p-6 bg-gray-50">
590
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 md:mb-6 gap-4">
591
+ <div className="flex gap-2 w-full md:w-auto">
592
+ <button onClick={handleExportWord} disabled={isExporting} className="flex-1 md:flex-none bg-blue-600 text-white px-4 py-2.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-700 shadow-sm disabled:opacity-50 transition-colors text-sm">
593
  {isExporting ? <Loader2 className="animate-spin" size={18}/> : <Download size={18}/>}
594
+ 导出 Word
595
  </button>
596
+ <div className="relative group flex-1 md:flex-none">
597
+ <button className="w-full bg-white text-gray-600 border border-gray-200 px-4 py-2.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-gray-50 shadow-sm text-sm">
598
+ <RefreshCw size={16}/> 批量改年份
599
  </button>
600
  <div className="absolute top-full left-0 mt-2 w-32 bg-white border border-gray-200 rounded-lg shadow-xl hidden group-hover:block z-20 py-1">
601
  {ZODIACS.map(z => (
 
606
  </div>
607
  </div>
608
  </div>
609
+ <div className="text-xs text-gray-500 font-bold bg-white px-3 py-1 rounded-full border">
610
+ 已保存 {savedRecords.length}
611
  </div>
612
  </div>
613
 
614
+ <div className="flex-1 overflow-y-auto custom-scrollbar pb-20">
615
  {savedRecords.length === 0 ? (
616
  <div className="h-full flex flex-col items-center justify-center text-gray-400 border-2 border-dashed border-gray-200 rounded-2xl bg-white/50">
617
  <Box size={48} className="mb-4 opacity-50"/>
618
  <p>管理箱是空的</p>
619
  </div>
620
  ) : (
621
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
622
  {savedRecords.map(record => (
623
+ <div key={record.id} className="bg-white rounded-xl shadow-sm border border-gray-100 p-5 flex flex-col hover:shadow-md transition-shadow group relative">
624
  <div className="flex justify-between items-center mb-3">
625
  <div className="font-bold text-lg text-gray-800 flex items-center gap-2">
626
  {record.name}
627
+ <span className="text-xs bg-amber-50 text-amber-700 px-2 py-0.5 rounded border border-amber-100">{record.zodiac}年</span>
628
  </div>
629
+ <div className="flex gap-1">
630
  <button
631
  onClick={() => { setEditingRecordId(record.id); setEditForm(record); }}
632
+ className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
633
  >
634
  <Edit size={16}/>
635
  </button>
636
  <button
637
  onClick={() => handleDeleteRecord(record.id)}
638
+ className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
639
  >
640
  <Trash2 size={16}/>
641
  </button>
 
643
  </div>
644
 
645
  {editingRecordId === record.id && editForm ? (
646
+ <div className="flex-1 flex flex-col gap-2 z-10 bg-white absolute inset-2 p-2 shadow-lg border rounded-lg">
647
+ <div className="flex justify-between items-center mb-1">
648
+ <span className="text-xs font-bold text-gray-500">编辑模式</span>
649
+ <button onClick={() => setEditingRecordId(null)} className="text-gray-400 hover:text-gray-600"><X size={16}/></button>
650
+ </div>
651
  <input
652
+ className="w-full border rounded p-1.5 text-sm font-bold bg-gray-50"
653
  value={editForm.name}
654
  onChange={e => setEditForm({...editForm, name: e.target.value})}
655
  />
656
  <textarea
657
+ className="w-full border rounded p-2 text-sm resize-none flex-1 focus:ring-2 focus:ring-blue-500 outline-none"
 
658
  value={editForm.comment}
659
  onChange={e => setEditForm({...editForm, comment: e.target.value})}
660
  />
661
+ <button
662
+ onClick={() => {
663
+ setSavedRecords(prev => prev.map(r => r.id === record.id ? editForm! : r));
664
+ setEditingRecordId(null);
665
+ setToast({ show: true, message: '修改已保存', type: 'success' });
666
+ }}
667
+ className="w-full bg-blue-600 text-white px-3 py-2 rounded font-bold text-xs"
668
+ >
669
+ 保存修改
670
+ </button>
 
 
 
671
  </div>
672
  ) : (
673
+ <div className="flex-1 text-sm text-gray-600 leading-relaxed overflow-hidden text-ellipsis line-clamp-6 bg-gray-50 p-3 rounded-lg border border-gray-50/50">
674
  {record.comment}
675
  </div>
676
  )}
677
 
678
  <div className="mt-3 pt-3 border-t border-gray-100 flex justify-between items-center text-xs text-gray-400">
679
  <span>{new Date(record.timestamp).toLocaleDateString()}</span>
680
+ <span className="font-mono bg-gray-100 px-1.5 rounded">{record.comment.length}字</span>
681
  </div>
682
  </div>
683
  ))}
 
688
  )}
689
  </div>
690
  );
691
+ };