chih.yikuan Claude Opus 4.6 (1M context) commited on
Commit
40aa09c
·
1 Parent(s): f002ae5

Add prompt editor and data preview back to Step 2

Browse files

Step 2 now has: student checkboxes → prompt editor (save/load) →
"預覽分析資料" button → DataPreviewModal → "生成報告".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

chatkit/frontend/src/App.tsx CHANGED
@@ -6,6 +6,7 @@ import { StepIndicator } from "./components/StepIndicator";
6
  import { FileUploadPanel } from "./components/step1/FileUploadPanel";
7
  import { ParsedDataSummary } from "./components/step2/ParsedDataSummary";
8
  import { StudentSelector } from "./components/step2/StudentSelector";
 
9
  import { ReportViewer } from "./components/step3/ReportViewer";
10
  import { apiGet, apiPost, getToken, clearToken } from "./lib/api";
11
 
@@ -38,7 +39,9 @@ export default function App() {
38
  const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1);
39
  const [sessionId, setSessionId] = useState<number | null>(null);
40
  const [parsedData, setParsedData] = useState<Record<string, unknown>>({});
 
41
  const [students, setStudents] = useState<StudentInfo[]>([]);
 
42
  const [reports, setReports] = useState<StudentReport[]>([]);
43
  const [isGenerating, setIsGenerating] = useState(false);
44
 
@@ -55,6 +58,15 @@ export default function App() {
55
  }
56
  }, []);
57
 
 
 
 
 
 
 
 
 
 
58
  // Create session when user is authenticated and no session exists
59
  useEffect(() => {
60
  if (user && !sessionId) {
@@ -82,7 +94,9 @@ export default function App() {
82
  setUser(null);
83
  setSessionId(null);
84
  setParsedData({});
 
85
  setStudents([]);
 
86
  setReports([]);
87
  setCurrentStep(1);
88
  };
@@ -96,10 +110,9 @@ export default function App() {
96
  }, []);
97
 
98
  // Sequential per-student report generation
99
- const handleGenerateReports = useCallback(async (selectedIndices: number[], model: string) => {
100
- if (!sessionId) return;
101
 
102
- // Initialize report slots
103
  const initial: StudentReport[] = selectedIndices.map((idx) => {
104
  const s = students.find((s) => s.index === idx);
105
  return {
@@ -113,11 +126,9 @@ export default function App() {
113
  setIsGenerating(true);
114
  setCurrentStep(3);
115
 
116
- // Generate sequentially
117
  for (let i = 0; i < selectedIndices.length; i++) {
118
  const idx = selectedIndices[i];
119
 
120
- // Mark current as generating
121
  setReports((prev) =>
122
  prev.map((r, j) => (j === i ? { ...r, status: "generating" } : r))
123
  );
@@ -148,13 +159,12 @@ export default function App() {
148
  }
149
 
150
  setIsGenerating(false);
151
- }, [sessionId, students]);
152
 
153
  const handleExportAllHtml = useCallback(() => {
154
  const doneReports = reports.filter((r) => r.status === "done");
155
  if (doneReports.length === 0) return;
156
 
157
- // Combine all reports into one HTML file with page breaks
158
  const combined = `<!DOCTYPE html>
159
  <html lang="zh-TW"><head><meta charset="UTF-8"><title>ClassLens - Student Reports</title>
160
  <style>
@@ -182,7 +192,6 @@ ${doneReports
182
  if (step <= currentStep) setCurrentStep(step);
183
  };
184
 
185
- // Loading state
186
  if (loading) {
187
  return (
188
  <div className="min-h-screen flex items-center justify-center bg-[var(--color-background)]">
@@ -191,7 +200,6 @@ ${doneReports
191
  );
192
  }
193
 
194
- // Invite code gate
195
  if (!invited) {
196
  return (
197
  <InviteGate
@@ -204,7 +212,6 @@ ${doneReports
204
  );
205
  }
206
 
207
- // Login gate
208
  if (!user) {
209
  return <LoginForm onLogin={setUser} />;
210
  }
@@ -242,19 +249,34 @@ ${doneReports
242
  <div className="space-y-6 animate-fade-in-up">
243
  <div className="text-center mb-4">
244
  <h2 className="font-display text-2xl font-bold text-[var(--color-text)]">
245
- 選擇學生
246
  </h2>
247
  <p className="text-[var(--color-text-muted)] mt-2">
248
- 勾選要生成個人錯題分析報告的學生,系統會逐一分析每位學生的答錯題目
249
  </p>
250
  </div>
 
251
  <ParsedDataSummary parsedData={parsedData} />
 
252
  {students.length > 0 ? (
253
- <StudentSelector
254
- students={students}
255
- onGenerate={handleGenerateReports}
256
- isGenerating={isGenerating}
257
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  ) : (
259
  <div className="card p-8 text-center">
260
  <p className="text-[var(--color-text-muted)]">
 
6
  import { FileUploadPanel } from "./components/step1/FileUploadPanel";
7
  import { ParsedDataSummary } from "./components/step2/ParsedDataSummary";
8
  import { StudentSelector } from "./components/step2/StudentSelector";
9
+ import { PromptEditor } from "./components/step2/PromptEditor";
10
  import { ReportViewer } from "./components/step3/ReportViewer";
11
  import { apiGet, apiPost, getToken, clearToken } from "./lib/api";
12
 
 
39
  const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1);
40
  const [sessionId, setSessionId] = useState<number | null>(null);
41
  const [parsedData, setParsedData] = useState<Record<string, unknown>>({});
42
+ const [promptContent, setPromptContent] = useState("");
43
  const [students, setStudents] = useState<StudentInfo[]>([]);
44
+ const [selectedIndices, setSelectedIndices] = useState<number[]>([]);
45
  const [reports, setReports] = useState<StudentReport[]>([]);
46
  const [isGenerating, setIsGenerating] = useState(false);
47
 
 
58
  }
59
  }, []);
60
 
61
+ // Load default prompt
62
+ useEffect(() => {
63
+ if (user && !promptContent) {
64
+ apiGet<{ content: string }>("/api/prompts/default")
65
+ .then((res) => setPromptContent(res.content))
66
+ .catch(() => {});
67
+ }
68
+ }, [user]);
69
+
70
  // Create session when user is authenticated and no session exists
71
  useEffect(() => {
72
  if (user && !sessionId) {
 
94
  setUser(null);
95
  setSessionId(null);
96
  setParsedData({});
97
+ setPromptContent("");
98
  setStudents([]);
99
+ setSelectedIndices([]);
100
  setReports([]);
101
  setCurrentStep(1);
102
  };
 
110
  }, []);
111
 
112
  // Sequential per-student report generation
113
+ const handleGenerateReports = useCallback(async (model: string) => {
114
+ if (!sessionId || selectedIndices.length === 0) return;
115
 
 
116
  const initial: StudentReport[] = selectedIndices.map((idx) => {
117
  const s = students.find((s) => s.index === idx);
118
  return {
 
126
  setIsGenerating(true);
127
  setCurrentStep(3);
128
 
 
129
  for (let i = 0; i < selectedIndices.length; i++) {
130
  const idx = selectedIndices[i];
131
 
 
132
  setReports((prev) =>
133
  prev.map((r, j) => (j === i ? { ...r, status: "generating" } : r))
134
  );
 
159
  }
160
 
161
  setIsGenerating(false);
162
+ }, [sessionId, selectedIndices, students]);
163
 
164
  const handleExportAllHtml = useCallback(() => {
165
  const doneReports = reports.filter((r) => r.status === "done");
166
  if (doneReports.length === 0) return;
167
 
 
168
  const combined = `<!DOCTYPE html>
169
  <html lang="zh-TW"><head><meta charset="UTF-8"><title>ClassLens - Student Reports</title>
170
  <style>
 
192
  if (step <= currentStep) setCurrentStep(step);
193
  };
194
 
 
195
  if (loading) {
196
  return (
197
  <div className="min-h-screen flex items-center justify-center bg-[var(--color-background)]">
 
200
  );
201
  }
202
 
 
203
  if (!invited) {
204
  return (
205
  <InviteGate
 
212
  );
213
  }
214
 
 
215
  if (!user) {
216
  return <LoginForm onLogin={setUser} />;
217
  }
 
249
  <div className="space-y-6 animate-fade-in-up">
250
  <div className="text-center mb-4">
251
  <h2 className="font-display text-2xl font-bold text-[var(--color-text)]">
252
+ 選擇學生 & 編輯提示詞
253
  </h2>
254
  <p className="text-[var(--color-text-muted)] mt-2">
255
+ 勾選要生成報告的學生,編輯提示詞,預覽資料後生成報告
256
  </p>
257
  </div>
258
+
259
  <ParsedDataSummary parsedData={parsedData} />
260
+
261
  {students.length > 0 ? (
262
+ <>
263
+ <StudentSelector
264
+ students={students}
265
+ onSelectionChange={setSelectedIndices}
266
+ />
267
+ {sessionId && (
268
+ <PromptEditor
269
+ promptContent={promptContent}
270
+ onPromptChange={setPromptContent}
271
+ sessionId={sessionId}
272
+ onGenerate={handleGenerateReports}
273
+ isGenerating={isGenerating}
274
+ selectedCount={selectedIndices.length}
275
+ parsedData={parsedData}
276
+ onParsedDataUpdate={handleParsedDataUpdate}
277
+ />
278
+ )}
279
+ </>
280
  ) : (
281
  <div className="card p-8 text-center">
282
  <p className="text-[var(--color-text-muted)]">
chatkit/frontend/src/components/step2/PromptEditor.tsx CHANGED
@@ -15,8 +15,9 @@ interface PromptEditorProps {
15
  promptContent: string;
16
  onPromptChange: (content: string) => void;
17
  sessionId: number;
18
- onRunSummary: (model: string) => void;
19
  isGenerating: boolean;
 
20
  parsedData: Record<string, unknown>;
21
  onParsedDataUpdate: (dataType: string, data: unknown) => void;
22
  }
@@ -25,8 +26,9 @@ export function PromptEditor({
25
  promptContent,
26
  onPromptChange,
27
  sessionId,
28
- onRunSummary,
29
  isGenerating,
 
30
  parsedData,
31
  onParsedDataUpdate,
32
  }: PromptEditorProps) {
@@ -165,19 +167,21 @@ export function PromptEditor({
165
  <textarea
166
  value={promptContent}
167
  onChange={(e) => onPromptChange(e.target.value)}
168
- rows={12}
169
  className="input !rounded-xl font-mono text-sm leading-relaxed resize-y"
170
- placeholder="輸入分析提示詞...使用 {questions}, {student_answers}, {teacher_answers} 作為資料佔位符"
171
  />
172
 
173
  {/* Preview button */}
174
  <div className="text-center">
175
  <button
176
  onClick={() => setShowPreview(true)}
177
- disabled={isGenerating || !promptContent.trim()}
178
  className="btn btn-accent text-lg px-8 py-4 disabled:opacity-50 disabled:cursor-not-allowed"
179
  >
180
- 預覽分析資料 →
 
 
181
  </button>
182
  </div>
183
 
@@ -188,7 +192,7 @@ export function PromptEditor({
188
  onParsedDataUpdate={onParsedDataUpdate}
189
  onGenerate={() => {
190
  setShowPreview(false);
191
- onRunSummary(model);
192
  }}
193
  onClose={() => setShowPreview(false)}
194
  isGenerating={isGenerating}
 
15
  promptContent: string;
16
  onPromptChange: (content: string) => void;
17
  sessionId: number;
18
+ onGenerate: (model: string) => void;
19
  isGenerating: boolean;
20
+ selectedCount: number;
21
  parsedData: Record<string, unknown>;
22
  onParsedDataUpdate: (dataType: string, data: unknown) => void;
23
  }
 
26
  promptContent,
27
  onPromptChange,
28
  sessionId,
29
+ onGenerate,
30
  isGenerating,
31
+ selectedCount,
32
  parsedData,
33
  onParsedDataUpdate,
34
  }: PromptEditorProps) {
 
167
  <textarea
168
  value={promptContent}
169
  onChange={(e) => onPromptChange(e.target.value)}
170
+ rows={10}
171
  className="input !rounded-xl font-mono text-sm leading-relaxed resize-y"
172
+ placeholder="輸入分析提示詞..."
173
  />
174
 
175
  {/* Preview button */}
176
  <div className="text-center">
177
  <button
178
  onClick={() => setShowPreview(true)}
179
+ disabled={isGenerating || selectedCount === 0}
180
  className="btn btn-accent text-lg px-8 py-4 disabled:opacity-50 disabled:cursor-not-allowed"
181
  >
182
+ {selectedCount === 0
183
+ ? "請先選擇學生"
184
+ : `預覽分析資料(${selectedCount} 位學生)→`}
185
  </button>
186
  </div>
187
 
 
192
  onParsedDataUpdate={onParsedDataUpdate}
193
  onGenerate={() => {
194
  setShowPreview(false);
195
+ onGenerate(model);
196
  }}
197
  onClose={() => setShowPreview(false)}
198
  isGenerating={isGenerating}
chatkit/frontend/src/components/step2/StudentSelector.tsx CHANGED
@@ -1,5 +1,4 @@
1
- import { useState } from "react";
2
- import { ModelSelector } from "../ModelSelector";
3
 
4
  interface Student {
5
  index: number;
@@ -9,19 +8,20 @@ interface Student {
9
 
10
  interface StudentSelectorProps {
11
  students: Student[];
12
- onGenerate: (selectedIndices: number[], model: string) => void;
13
- isGenerating: boolean;
14
  }
15
 
16
  export function StudentSelector({
17
  students,
18
- onGenerate,
19
- isGenerating,
20
  }: StudentSelectorProps) {
21
  const [selected, setSelected] = useState<Set<number>>(
22
  () => new Set(students.map((s) => s.index))
23
  );
24
- const [model, setModel] = useState("gpt-5.4");
 
 
 
25
 
26
  const toggleAll = () => {
27
  if (selected.size === students.length) {
@@ -46,42 +46,40 @@ export function StudentSelector({
46
  const allSelected = selected.size === students.length;
47
 
48
  return (
49
- <div className="space-y-4">
50
- {/* Header bar */}
51
- <div className="flex flex-wrap items-center justify-between gap-3">
52
- <div className="flex items-center gap-3">
53
- <button
54
- onClick={toggleAll}
55
- className="btn btn-outline text-sm !py-2"
56
- >
57
- {allSelected ? "取消全選" : "全選"}
58
- </button>
59
- <span className="text-sm text-[var(--color-text-muted)]">
60
- 已選 {selected.size} / {students.length} 位學生
61
- </span>
62
- </div>
63
- <ModelSelector value={model} onChange={setModel} label="報告模型:" />
64
  </div>
65
 
66
- {/* Student list */}
67
- <div className="card p-4 max-h-[50vh] overflow-y-auto">
68
- <div className="grid gap-2">
69
  {students.map((s) => (
70
  <label
71
  key={s.index}
72
- className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer transition-colors ${
73
  selected.has(s.index)
74
- ? "bg-[var(--color-primary)]/10 border border-[var(--color-primary)]/30"
75
- : "hover:bg-[var(--color-surface)] border border-transparent"
76
  }`}
77
  >
78
  <input
79
  type="checkbox"
80
  checked={selected.has(s.index)}
81
  onChange={() => toggle(s.index)}
82
- className="w-5 h-5 rounded accent-[var(--color-primary)]"
83
  />
84
- <span className="font-semibold text-[var(--color-text)]">
85
  {s.name}
86
  </span>
87
  {s.id && (
@@ -93,24 +91,6 @@ export function StudentSelector({
93
  ))}
94
  </div>
95
  </div>
96
-
97
- {/* Generate button */}
98
- <div className="text-center">
99
- <button
100
- onClick={() => onGenerate(Array.from(selected).sort((a, b) => a - b), model)}
101
- disabled={isGenerating || selected.size === 0}
102
- className="btn btn-accent text-lg px-8 py-4 disabled:opacity-50 disabled:cursor-not-allowed"
103
- >
104
- {isGenerating ? (
105
- <span className="flex items-center gap-2">
106
- <span className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
107
- 生成報告中...
108
- </span>
109
- ) : (
110
- `為 ${selected.size} 位學生生成報告 →`
111
- )}
112
- </button>
113
- </div>
114
  </div>
115
  );
116
  }
 
1
+ import { useState, useEffect } from "react";
 
2
 
3
  interface Student {
4
  index: number;
 
8
 
9
  interface StudentSelectorProps {
10
  students: Student[];
11
+ onSelectionChange: (selectedIndices: number[]) => void;
 
12
  }
13
 
14
  export function StudentSelector({
15
  students,
16
+ onSelectionChange,
 
17
  }: StudentSelectorProps) {
18
  const [selected, setSelected] = useState<Set<number>>(
19
  () => new Set(students.map((s) => s.index))
20
  );
21
+
22
+ useEffect(() => {
23
+ onSelectionChange(Array.from(selected).sort((a, b) => a - b));
24
+ }, [selected]);
25
 
26
  const toggleAll = () => {
27
  if (selected.size === students.length) {
 
46
  const allSelected = selected.size === students.length;
47
 
48
  return (
49
+ <div className="space-y-3">
50
+ <div className="flex items-center gap-3">
51
+ <h3 className="font-display text-sm font-semibold text-[var(--color-text)]">
52
+ 選擇學生
53
+ </h3>
54
+ <button
55
+ onClick={toggleAll}
56
+ className="btn btn-outline text-xs !py-1 !px-3"
57
+ >
58
+ {allSelected ? "取消全選" : "全選"}
59
+ </button>
60
+ <span className="text-sm text-[var(--color-text-muted)]">
61
+ 已選 {selected.size} / {students.length} 位學生
62
+ </span>
 
63
  </div>
64
 
65
+ <div className="card p-4 max-h-64 overflow-y-auto">
66
+ <div className="grid gap-1.5 sm:grid-cols-2">
 
67
  {students.map((s) => (
68
  <label
69
  key={s.index}
70
+ className={`flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-colors ${
71
  selected.has(s.index)
72
+ ? "bg-[var(--color-primary)]/10"
73
+ : "hover:bg-[var(--color-surface)]"
74
  }`}
75
  >
76
  <input
77
  type="checkbox"
78
  checked={selected.has(s.index)}
79
  onChange={() => toggle(s.index)}
80
+ className="w-4 h-4 rounded accent-[var(--color-primary)]"
81
  />
82
+ <span className="text-sm font-medium text-[var(--color-text)]">
83
  {s.name}
84
  </span>
85
  {s.id && (
 
91
  ))}
92
  </div>
93
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  </div>
95
  );
96
  }