chih.yikuan Claude Opus 4.6 (1M context) commited on
Commit
e1ed51d
·
1 Parent(s): 30ff567

Add data preview modal before report generation

Browse files

Users can now review and edit parsed JSON data (questions, student answers,
teacher answers) in a tabbed modal before generating the report.

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

chatkit/frontend/src/App.tsx CHANGED
@@ -200,6 +200,8 @@ export default function App() {
200
  sessionId={sessionId}
201
  onRunSummary={handleRunSummary}
202
  isGenerating={isGenerating}
 
 
203
  />
204
  )}
205
  </div>
 
200
  sessionId={sessionId}
201
  onRunSummary={handleRunSummary}
202
  isGenerating={isGenerating}
203
+ parsedData={parsedData}
204
+ onParsedDataUpdate={handleParsedDataUpdate}
205
  />
206
  )}
207
  </div>
chatkit/frontend/src/components/step2/DataPreviewModal.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+
3
+ interface DataPreviewModalProps {
4
+ parsedData: Record<string, unknown>;
5
+ onParsedDataUpdate: (dataType: string, data: unknown) => void;
6
+ onGenerate: () => void;
7
+ onClose: () => void;
8
+ isGenerating: boolean;
9
+ }
10
+
11
+ type TabKey = "questions" | "student_answers" | "teacher_answers";
12
+
13
+ const TABS: { key: TabKey; label: string; icon: string }[] = [
14
+ { key: "questions", label: "考試題目", icon: "📝" },
15
+ { key: "student_answers", label: "學生答案", icon: "👨‍🎓" },
16
+ { key: "teacher_answers", label: "標準答案", icon: "✅" },
17
+ ];
18
+
19
+ export function DataPreviewModal({
20
+ parsedData,
21
+ onParsedDataUpdate,
22
+ onGenerate,
23
+ onClose,
24
+ isGenerating,
25
+ }: DataPreviewModalProps) {
26
+ const [activeTab, setActiveTab] = useState<TabKey>("questions");
27
+ const [editTexts, setEditTexts] = useState<Record<TabKey, string>>({
28
+ questions: "",
29
+ student_answers: "",
30
+ teacher_answers: "",
31
+ });
32
+ const [parseErrors, setParseErrors] = useState<Record<string, string>>({});
33
+
34
+ // Initialize edit texts from parsedData
35
+ useEffect(() => {
36
+ const texts = {} as Record<TabKey, string>;
37
+ for (const tab of TABS) {
38
+ const data = parsedData[tab.key];
39
+ texts[tab.key] = data ? JSON.stringify(data, null, 2) : "";
40
+ }
41
+ setEditTexts(texts);
42
+ }, []);
43
+
44
+ const handleTextChange = (key: TabKey, value: string) => {
45
+ setEditTexts((prev) => ({ ...prev, [key]: value }));
46
+ // Clear error on edit
47
+ setParseErrors((prev) => {
48
+ const next = { ...prev };
49
+ delete next[key];
50
+ return next;
51
+ });
52
+ };
53
+
54
+ const handleSave = (key: TabKey) => {
55
+ try {
56
+ const parsed = JSON.parse(editTexts[key]);
57
+ onParsedDataUpdate(key, parsed);
58
+ setParseErrors((prev) => {
59
+ const next = { ...prev };
60
+ delete next[key];
61
+ return next;
62
+ });
63
+ } catch {
64
+ setParseErrors((prev) => ({ ...prev, [key]: "JSON 格式錯誤,請檢查語法" }));
65
+ }
66
+ };
67
+
68
+ const handleGenerate = () => {
69
+ // Try saving all tabs first
70
+ let hasError = false;
71
+ for (const tab of TABS) {
72
+ if (!editTexts[tab.key].trim()) continue;
73
+ try {
74
+ const parsed = JSON.parse(editTexts[tab.key]);
75
+ onParsedDataUpdate(tab.key, parsed);
76
+ } catch {
77
+ setParseErrors((prev) => ({ ...prev, [tab.key]: "JSON 格式錯誤,請檢查語法" }));
78
+ setActiveTab(tab.key);
79
+ hasError = true;
80
+ break;
81
+ }
82
+ }
83
+ if (!hasError) onGenerate();
84
+ };
85
+
86
+ const getItemCount = (key: TabKey): number => {
87
+ try {
88
+ const data = JSON.parse(editTexts[key]);
89
+ if (key === "questions") return data?.questions?.length ?? 0;
90
+ if (key === "student_answers") return data?.students?.length ?? 0;
91
+ if (key === "teacher_answers") return data?.answers?.length ?? 0;
92
+ } catch {}
93
+ return 0;
94
+ };
95
+
96
+ return (
97
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
98
+ {/* Backdrop */}
99
+ <div
100
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
101
+ onClick={onClose}
102
+ />
103
+
104
+ {/* Modal */}
105
+ <div className="relative w-full max-w-5xl max-h-[90vh] flex flex-col card overflow-hidden">
106
+ {/* Header */}
107
+ <div className="flex items-center justify-between p-5 border-b border-[var(--color-border)]">
108
+ <h3 className="font-display text-xl font-bold text-[var(--color-text)]">
109
+ 預覽分析資料
110
+ </h3>
111
+ <button
112
+ onClick={onClose}
113
+ className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:bg-[var(--color-border)]/50 transition-colors text-lg"
114
+ >
115
+ &times;
116
+ </button>
117
+ </div>
118
+
119
+ {/* Tabs */}
120
+ <div className="flex border-b border-[var(--color-border)]">
121
+ {TABS.map((tab) => {
122
+ const count = getItemCount(tab.key);
123
+ const hasError = !!parseErrors[tab.key];
124
+ return (
125
+ <button
126
+ key={tab.key}
127
+ onClick={() => setActiveTab(tab.key)}
128
+ className={`flex-1 px-4 py-3 text-sm font-semibold transition-colors relative ${
129
+ activeTab === tab.key
130
+ ? "text-[var(--color-primary)] bg-[var(--color-primary)]/5"
131
+ : "text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:bg-[var(--color-surface)]"
132
+ } ${hasError ? "!text-red-500" : ""}`}
133
+ >
134
+ <span>{tab.icon} {tab.label}</span>
135
+ {count > 0 && (
136
+ <span className="ml-2 text-xs badge badge-success">{count}</span>
137
+ )}
138
+ {activeTab === tab.key && (
139
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--color-primary)]" />
140
+ )}
141
+ </button>
142
+ );
143
+ })}
144
+ </div>
145
+
146
+ {/* Editor */}
147
+ <div className="flex-1 overflow-hidden flex flex-col p-5 min-h-0">
148
+ <div className="flex items-center justify-between mb-3">
149
+ <p className="text-sm text-[var(--color-text-muted)]">
150
+ 可直接編輯下方 JSON 資料,修改後點擊「套用修改」儲存
151
+ </p>
152
+ <button
153
+ onClick={() => handleSave(activeTab)}
154
+ className="btn btn-outline text-sm !py-1.5 !px-3"
155
+ >
156
+ 套用修改
157
+ </button>
158
+ </div>
159
+
160
+ {parseErrors[activeTab] && (
161
+ <div className="mb-3 px-3 py-2 rounded-lg bg-red-500/10 text-red-500 text-sm">
162
+ {parseErrors[activeTab]}
163
+ </div>
164
+ )}
165
+
166
+ <textarea
167
+ value={editTexts[activeTab]}
168
+ onChange={(e) => handleTextChange(activeTab, e.target.value)}
169
+ className="input !rounded-xl font-mono text-sm leading-relaxed resize-none flex-1"
170
+ placeholder={`尚未上傳${TABS.find((t) => t.key === activeTab)?.label ?? ""}資料`}
171
+ spellCheck={false}
172
+ />
173
+ </div>
174
+
175
+ {/* Footer */}
176
+ <div className="flex items-center justify-end gap-3 p-5 border-t border-[var(--color-border)]">
177
+ <button onClick={onClose} className="btn btn-outline">
178
+ 返回編輯
179
+ </button>
180
+ <button
181
+ onClick={handleGenerate}
182
+ disabled={isGenerating}
183
+ className="btn btn-accent text-lg px-8 py-3 disabled:opacity-50 disabled:cursor-not-allowed"
184
+ >
185
+ {isGenerating ? (
186
+ <span className="flex items-center gap-2">
187
+ <span className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
188
+ 生成報告中...
189
+ </span>
190
+ ) : (
191
+ "生成報告"
192
+ )}
193
+ </button>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ );
198
+ }
chatkit/frontend/src/components/step2/PromptEditor.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import { useState, useEffect } from "react";
2
  import { apiGet, apiPost } from "../../lib/api";
3
  import { ModelSelector } from "../ModelSelector";
 
4
 
5
  interface Prompt {
6
  id: number;
@@ -16,6 +17,8 @@ interface PromptEditorProps {
16
  sessionId: number;
17
  onRunSummary: (model: string) => void;
18
  isGenerating: boolean;
 
 
19
  }
20
 
21
  export function PromptEditor({
@@ -24,6 +27,8 @@ export function PromptEditor({
24
  sessionId,
25
  onRunSummary,
26
  isGenerating,
 
 
27
  }: PromptEditorProps) {
28
  const [prompts, setPrompts] = useState<Prompt[]>([]);
29
  const [allPrompts, setAllPrompts] = useState<Prompt[]>([]);
@@ -32,6 +37,7 @@ export function PromptEditor({
32
  const [showOthers, setShowOthers] = useState(false);
33
  const [saving, setSaving] = useState(false);
34
  const [model, setModel] = useState("gpt-5.4");
 
35
 
36
  useEffect(() => {
37
  loadPrompts();
@@ -164,23 +170,30 @@ export function PromptEditor({
164
  placeholder="輸入分析提示詞...使用 {questions}, {student_answers}, {teacher_answers} 作為資料佔位符"
165
  />
166
 
167
- {/* Run summary button */}
168
  <div className="text-center">
169
  <button
170
- onClick={() => onRunSummary(model)}
171
  disabled={isGenerating || !promptContent.trim()}
172
  className="btn btn-accent text-lg px-8 py-4 disabled:opacity-50 disabled:cursor-not-allowed"
173
  >
174
- {isGenerating ? (
175
- <span className="flex items-center gap-2">
176
- <span className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
177
- 生成報告中...
178
- </span>
179
- ) : (
180
- "執行摘要分析 →"
181
- )}
182
  </button>
183
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  </div>
185
  );
186
  }
 
1
  import { useState, useEffect } from "react";
2
  import { apiGet, apiPost } from "../../lib/api";
3
  import { ModelSelector } from "../ModelSelector";
4
+ import { DataPreviewModal } from "./DataPreviewModal";
5
 
6
  interface Prompt {
7
  id: number;
 
17
  sessionId: number;
18
  onRunSummary: (model: string) => void;
19
  isGenerating: boolean;
20
+ parsedData: Record<string, unknown>;
21
+ onParsedDataUpdate: (dataType: string, data: unknown) => void;
22
  }
23
 
24
  export function PromptEditor({
 
27
  sessionId,
28
  onRunSummary,
29
  isGenerating,
30
+ parsedData,
31
+ onParsedDataUpdate,
32
  }: PromptEditorProps) {
33
  const [prompts, setPrompts] = useState<Prompt[]>([]);
34
  const [allPrompts, setAllPrompts] = useState<Prompt[]>([]);
 
37
  const [showOthers, setShowOthers] = useState(false);
38
  const [saving, setSaving] = useState(false);
39
  const [model, setModel] = useState("gpt-5.4");
40
+ const [showPreview, setShowPreview] = useState(false);
41
 
42
  useEffect(() => {
43
  loadPrompts();
 
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
+
184
+ {/* Data preview modal */}
185
+ {showPreview && (
186
+ <DataPreviewModal
187
+ parsedData={parsedData}
188
+ onParsedDataUpdate={onParsedDataUpdate}
189
+ onGenerate={() => {
190
+ setShowPreview(false);
191
+ onRunSummary(model);
192
+ }}
193
+ onClose={() => setShowPreview(false)}
194
+ isGenerating={isGenerating}
195
+ />
196
+ )}
197
  </div>
198
  );
199
  }