lzl1005 commited on
Commit
d762b1b
·
verified ·
1 Parent(s): d83fc5f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +545 -243
app.py CHANGED
@@ -1,249 +1,551 @@
1
- import json
2
- import time
3
- from typing import Dict, Any
4
- import gradio as gr
5
-
6
- # --- Simulation Setup for LLM API ---
7
- # This section simulates the core AI generation logic without requiring a live API key.
8
- LLM_MODEL = "gemini-2.5-flash-preview-09-2025"
9
- API_KEY = "MOCK_KEY" # Placeholder
10
-
11
- def simulate_gemini_api_call(payload: Dict[str, Any], fields: Dict[str, Any]) -> Dict[str, Any]:
12
- """
13
- Simulates a structured response from the Gemini API based on the task type.
14
-
15
- Args:
16
- payload: The constructed API payload (used for extracting system instructions).
17
- fields: User input fields for customizing the mock output.
18
-
19
- Returns:
20
- A dictionary simulating the API's JSON response structure.
21
- """
22
-
23
- user_query = payload['contents'][0]['parts'][0]['text']
24
- system_instruction = payload.get('systemInstruction', {}).get('parts', [{}])[0].get('text', 'No system instruction')
25
-
26
- # Check system instruction to determine the output type (Admin or Teaching)
27
- if "台灣中學學務處行政書記" in system_instruction:
28
- # Simulate Admin Copilot (Meeting Minutes) output
29
- mock_text_result = json.dumps({
30
- "文件類型 (Document Type)": "學務處會議記錄 (Academic Affairs Meeting Minutes)",
31
- "meeting_info": {
32
- "date": fields.get('date', '2025-01-10'),
33
- "location": fields.get('location', '會議室'),
34
- "topic": fields.get('topic', '模擬會議主題')
35
- },
36
- "attendees": ["Principal", "Director", "All Faculty"],
37
- "key_points": [
38
- "End-of-term commendation process finalized, 30 cases approved.",
39
- "New student orientation preparation is 80% complete; venue setup confirmed."
40
- ],
41
- "resolutions": [
42
- {"item": "Issue official commendation announcement.", "responsible": "HR", "deadline": "2025-01-15"},
43
- {"item": "Venue setup for orientation completed one day prior.", "responsible": "General Affairs", "deadline": "2025-08-20"}
44
- ],
45
- "audit_note": "Document generated per internal school writing guidelines."
46
- }, ensure_ascii=False, indent=2)
47
-
48
- elif "台灣國高中資深教師與課程設計師" in system_instruction:
49
- # Simulate Teaching Designer (Lesson Plan & Rubric) output
50
- mock_text_result = json.dumps({
51
- "文件類型 (Document Type)": "單元教案與評量規準 (Lesson Plan & Rubric)",
52
- "lesson_plan_title": f"【{fields.get('subject', 'N/A')}】Exploring {fields.get('topic', 'N/A')} ({fields.get('hours', 0)} sessions)",
53
- "grade_level": fields.get('grade', 'N/A'),
54
- "curriculum_alignment": ["A2 Logical Reasoning (Curriculum Competency)", "B3 Independent Thinking"],
55
- "learning_objectives": ["Students can explain X.", "Students can apply Y."],
56
- "activities": [
57
- {"time_min": 15, "stage": "Introduction", "method": "Inquiry", "description": "Use real-life examples to introduce the core concept."},
58
- {"time_min": 30, "stage": "Activity One", "method": "Collaborative Learning", "description": "Group project and practical exercise."},
59
- ],
60
- "rubric": {
61
- "title": "Unit Assessment Rubric (4 Levels x 4 Indicators)",
62
- "criteria": [
63
- {"name": "Conceptual Understanding", "A": "Clearly and accurately explains all core concepts.", "D": "Answers only simple questions."},
64
- {"name": "Teamwork", "A": "Actively leads the team to complete the task.", "D": "Does not participate in discussion."}
65
- ]
66
- },
67
- "differentiation_advice": "Provide bilingual vocabulary cards for students with lower English proficiency."
68
- }, ensure_ascii=False, indent=2)
69
-
70
- else:
71
- mock_text_result = json.dumps({"error": "Unknown or missing task instruction."})
72
-
73
-
74
- # Return the simulated API response structure
75
- return {
76
- "candidates": [{
77
- "content": {
78
- "parts": [{ "text": mock_text_result }]
79
- },
80
- "groundingMetadata": {}
81
- }]
82
- }
83
-
84
- # --- Module A: Admin Copilot Generator (Gradio Wrapper) ---
85
-
86
- def admin_copilot_generator(template_id: str, topic: str, date: str, location: str, key_input: str) -> str:
87
- """
88
- Handles the Admin Copilot UI inputs and calls the simulation.
89
- """
90
- fields = {
91
- "topic": topic,
92
- "date": date,
93
- "location": location,
94
- "key_input": key_input
95
- }
96
-
97
- # System Prompt and Schema defined as per spec 8.A
98
- system_prompt = (
99
- "角色:台灣中學學務處行政書記\n"
100
- "輸出:JSON(會議資訊、出席、重點、決議、待辦、負責人、期限)\n"
101
- "格式規範:用詞正式、避免口語、保留專有名詞\n"
102
- "限制:所有決議必須有負責人和明確期限。"
103
- )
104
- response_schema = {
105
- "type": "OBJECT",
106
- "properties": {
107
- "文件類型 (Document Type)": {"type": "STRING"},
108
- "meeting_info": {"type": "OBJECT", "properties": {"date": {"type": "STRING"}, "location": {"type": "STRING"}, "topic": {"type": "STRING"}}},
109
- "attendees": {"type": "ARRAY", "items": {"type": "STRING"}},
110
- "key_points": {"type": "ARRAY", "items": {"type": "STRING"}},
111
- "resolutions": {
112
- "type": "ARRAY",
113
- "items": {
114
- "type": "OBJECT",
115
- "properties": {
116
- "item": {"type": "STRING"},
117
- "responsible": {"type": "STRING"},
118
- "deadline": {"type": "STRING", "format": "date"}
119
  }
120
  }
121
- },
122
- "audit_note": {"type": "STRING"}
123
  }
124
- }
125
- user_query = f"請生成一份會議記錄。主題: {topic}; 輸入重點(或逐字稿):{key_input}"
126
-
127
- payload = {
128
- "contents": [{ "parts": [{ "text": user_query }] }],
129
- "systemInstruction": { "parts": [{ "text": system_prompt }] },
130
- "generationConfig": {
131
- "responseMimeType": "application/json",
132
- "responseSchema": response_schema
133
  }
134
- }
135
-
136
- api_response = simulate_gemini_api_call(payload, fields)
137
-
138
- try:
139
- json_string = api_response['candidates'][0]['content']['parts'][0]['text']
140
- return json_string
141
- except (KeyError, json.JSONDecodeError):
142
- return "ERROR: Failed to parse LLM structured output."
143
-
144
- # --- Module B: Teaching AI Designer (Gradio Wrapper) ---
145
-
146
- def lesson_plan_designer(grade: str, subject: str, topic: str, hours: int, method: str, equipment: str, class_needs: str) -> str:
147
- """
148
- Handles the Teaching Designer UI inputs and calls the simulation.
149
- """
150
- fields = {
151
- "grade": grade,
152
- "subject": subject,
153
- "topic": topic,
154
- "hours": hours,
155
- "method": method,
156
- "equipment": equipment,
157
- "class_needs": class_needs
158
- }
159
-
160
- # System Prompt and Schema defined as per spec 8.B
161
- system_prompt = (
162
- "角色:台灣國高中資深教師與課程設計師\n"
163
- "輸出:JSON(教案標題、目標、課綱對齊、活動步驟、評量規準、差異化建議)\n"
164
- "限制:活動分鏡以 15 分鐘粒度;至少 2 項形成性評量。\n"
165
- "對齊:請將輸出中的 'curriculum_alignment' 欄位,對齊台灣課綱的關鍵能力/素養。"
166
- )
167
-
168
- response_schema = {
169
- "type": "OBJECT",
170
- "properties": {
171
- "文件類型 (Document Type)": {"type": "STRING"},
172
- "lesson_plan_title": {"type": "STRING"},
173
- "grade_level": {"type": "STRING"},
174
- "curriculum_alignment": {"type": "ARRAY", "items": {"type": "STRING"}},
175
- "learning_objectives": {"type": "ARRAY", "items": {"type": "STRING"}},
176
- "activities": {"type": "ARRAY", "items": {"type": "OBJECT"}},
177
- "rubric": {"type": "OBJECT"},
178
- "differentiation_advice": {"type": "STRING"}
179
  }
180
- }
181
-
182
- user_query = (
183
- f"請根據以下資訊設計一個單元教案、評量規準和差異化建議:\n"
184
- f"年級/學科/單元主題: {grade}/{subject}/{topic}\n"
185
- f"課時數: {hours} 節\n"
186
- f"教學法偏好: {method}\n"
187
- f"可用設備: {equipment}\n"
188
- f"班級特性: {class_needs}"
189
- )
190
-
191
- payload = {
192
- "contents": [{ "parts": [{ "text": user_query }] }],
193
- "systemInstruction": { "parts": [{ "text": system_prompt }] },
194
- "generationConfig": {
195
- "responseMimeType": "application/json",
196
- "responseSchema": response_schema
197
  }
198
- }
199
-
200
- api_response = simulate_gemini_api_call(payload, fields)
201
-
202
- try:
203
- json_string = api_response['candidates'][0]['content']['parts'][0]['text']
204
- return json_string
205
- except (KeyError, json.JSONDecodeError):
206
- return "ERROR: Failed to parse LLM structured output."
207
-
208
- # --- Gradio Interface Definition ---
209
-
210
- # Module A Interface (Admin Copilot)
211
- admin_copilot_interface = gr.Interface(
212
- fn=admin_copilot_generator,
213
- inputs=[
214
- gr.Textbox(label="模板 ID (Template ID - Fixed for MVP)", value="meeting_minutes_standard", interactive=False),
215
- gr.Textbox(label="會議主題 (Meeting Topic)", value="學務處期末獎懲與新生訓練籌備會議"),
216
- gr.Textbox(label="日期 (Date)", value="2025-01-10"),
217
- gr.Textbox(label="地點 (Location)", value="學務處會議室"),
218
- gr.Textbox(label="輸入重點/逐字稿 (Key Input/Transcript)", value="討論期末獎懲核定程序。新生訓練場地佈置、人員編組確認。", lines=5),
219
- ],
220
- outputs=gr.JSON(label="AI 生成結構化 JSON 初稿 (Structured JSON Draft for DOCX Templating)"),
221
- title="行政 Copilot:會議記錄生成 (Admin Copilot: Meeting Minutes Generation)",
222
- description="🎯 模擬一鍵生成格式嚴謹的行政文件 JSON。實際系統將此 JSON 套入 DOCX 模板匯出。"
223
- )
224
-
225
- # Module B Interface (Teaching Designer)
226
- lesson_plan_designer_interface = gr.Interface(
227
- fn=lesson_plan_designer,
228
- inputs=[
229
- gr.Dropdown(label="年級 (Grade)", choices=["國一", "高一", "小六"], value="高一"),
230
- gr.Textbox(label="學科 (Subject)", value="歷史"),
231
- gr.Textbox(label="單元主題 (Unit Topic)", value="從茶葉看全球化:17-19世紀的貿易網絡"),
232
- gr.Slider(label="課時數 (Number of Sessions)", minimum=1, maximum=10, step=1, value=4),
233
- gr.Dropdown(label="教學法偏好 (Pedagogy Preference)", choices=["探究式、PBL", "翻轉教學", "合作學習", "講述法"], value="探究式、PBL"),
234
- gr.Textbox(label="可用設備 (Available Equipment)", value="平板電腦、投影設備、網路"),
235
- gr.Textbox(label="班級特性 (Class Characteristics)", value="班級組成多元,需考慮多樣化的史料呈現方式。"),
236
- ],
237
- outputs=gr.JSON(label="AI 生成教案與評量規準 JSON (Lesson Plan & Rubric JSON)"),
238
- title="教學 AI 設計器:教案與 Rubric 生成 (Teaching AI Designer: Lesson Plan & Rubric)",
239
- description="📘 模擬生成符合課綱精神的單元教案結構和評量規準。實際系統將此 JSON 套入 DOCX 或 Google Slides 框架。"
240
- )
241
-
242
- # Integrate the two modules into a Tabbed Interface
243
- demo = gr.TabbedInterface([admin_copilot_interface, lesson_plan_designer_interface],
244
- ["模組 A: 行政 Copilot", "模組 B: 教學設計器"],
245
- title="CampusAI Suite (台灣校園 AI 文書/教學 MVP 演示)",
246
- theme=gr.themes.Soft())
247
-
248
- if __name__ == "__main__":
249
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-TW">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CampusAI Suite (台灣校園 AI 文書/教學 MVP)</title>
7
+ <!-- Load Tailwind CSS -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ theme: {
12
+ extend: {
13
+ fontFamily: {
14
+ sans: ['Inter', 'Noto Sans TC', 'sans-serif'],
15
+ },
16
+ colors: {
17
+ 'primary': '#4f46e5',
18
+ 'secondary': '#10b981',
19
+ 'bg-light': '#f9fafb',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
  }
22
+ }
 
23
  }
24
+ </script>
25
+ <style>
26
+ /* Base styles */
27
+ body {
28
+ background-color: #f3f4f6;
29
+ min-height: 100vh;
 
 
 
30
  }
31
+ /* Custom scrollbar for output area */
32
+ .custom-scroll::-webkit-scrollbar {
33
+ width: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
+ .custom-scroll::-webkit-scrollbar-track {
36
+ background: #e5e7eb;
37
+ border-radius: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
+ .custom-scroll::-webkit-scrollbar-thumb {
40
+ background: #9ca3af;
41
+ border-radius: 10px;
42
+ }
43
+ .custom-scroll::-webkit-scrollbar-thumb:hover {
44
+ background: #6b7280;
45
+ }
46
+ /* Markdown styling for document preview */
47
+ .markdown-preview h1 { font-size: 1.875rem; font-weight: 700; margin-bottom: 1rem; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.5rem; }
48
+ .markdown-preview h2 { font-size: 1.5rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.75rem; color: #4f46e5; }
49
+ .markdown-preview h3 { font-size: 1.25rem; font-weight: 600; margin-top: 1rem; margin-bottom: 0.5rem; border-left: 4px solid #10b981; padding-left: 0.5rem; }
50
+ .markdown-preview p { margin-bottom: 0.75rem; line-height: 1.6; }
51
+ .markdown-preview ul { list-style: disc; margin-left: 1.5rem; margin-bottom: 1rem; }
52
+ .markdown-preview li { margin-bottom: 0.25rem; }
53
+ .markdown-preview table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; }
54
+ .markdown-preview th, .markdown-preview td { border: 1px solid #d1d5db; padding: 0.75rem; text-align: left; }
55
+ .markdown-preview th { background-color: #eef2ff; font-weight: 600; color: #374151; }
56
+
57
+ .tab-button.active {
58
+ border-color: #4f46e5;
59
+ color: #4f46e5;
60
+ font-weight: 600;
61
+ background-color: #ffffff;
62
+ }
63
+ </style>
64
+ </head>
65
+ <body class="font-sans">
66
+
67
+ <!-- Firebase SDK Imports (Required for Canvas Environment) -->
68
+ <script type="module">
69
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
70
+ import { getAuth, signInAnonymously, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
71
+ import { getFirestore } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
72
+
73
+ // Global variables provided by the Canvas environment
74
+ const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
75
+ const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;
76
+ const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
77
+
78
+ let db, auth;
79
+
80
+ if (firebaseConfig) {
81
+ const app = initializeApp(firebaseConfig);
82
+ db = getFirestore(app);
83
+ auth = getAuth(app);
84
+ window.db = db; // Make available globally if needed
85
+ window.auth = auth; // Make available globally if needed
86
+
87
+ // Sign in using custom token or anonymously
88
+ async function authenticate() {
89
+ try {
90
+ if (initialAuthToken) {
91
+ await signInWithCustomToken(auth, initialAuthToken);
92
+ console.log("Firebase signed in with custom token.");
93
+ } else {
94
+ await signInAnonymously(auth);
95
+ console.log("Firebase signed in anonymously.");
96
+ }
97
+ } catch (error) {
98
+ console.error("Firebase authentication error:", error);
99
+ }
100
+ }
101
+ authenticate();
102
+ } else {
103
+ console.warn("Firebase config not available. Running without database features.");
104
+ }
105
+ </script>
106
+
107
+
108
+ <div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
109
+ <h1 class="text-3xl font-bold text-gray-900 mb-6">CampusAI Suite (台灣校園 AI 文書/教學 MVP 演示)</h1>
110
+
111
+ <!-- Tab Navigation -->
112
+ <div class="border-b border-gray-200 mb-6">
113
+ <nav class="-mb-px flex space-x-4" aria-label="Tabs">
114
+ <button id="tab-admin" onclick="switchTab('admin')"
115
+ class="tab-button active group inline-flex items-center py-3 px-3 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300">
116
+ 模組 A: 行政 Copilot (會議記錄)
117
+ </button>
118
+ <button id="tab-teaching" onclick="switchTab('teaching')"
119
+ class="tab-button group inline-flex items-center py-3 px-3 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300">
120
+ 模組 B: 教學設計器 (教案與 Rubric)
121
+ </button>
122
+ </nav>
123
+ </div>
124
+
125
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
126
+ <!-- Left Column: Input Forms -->
127
+ <div class="bg-white shadow-xl rounded-xl p-6 h-full">
128
+ <div id="content-admin" class="tab-content">
129
+ <h2 class="text-2xl font-semibold text-primary mb-4">行政 Copilot:會議記錄生成</h2>
130
+ <p class="text-sm text-gray-500 mb-6">🎯 模擬一鍵生成格式嚴謹的行政文件 JSON,並轉換為文件預覽。</p>
131
+
132
+ <div class="space-y-4">
133
+ <label class="block">
134
+ <span class="text-gray-700">會議主題 (Meeting Topic)</span>
135
+ <input type="text" id="admin-topic" value="學務處期末獎懲與新生訓練籌備會議" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
136
+ </label>
137
+ <div class="flex space-x-4">
138
+ <label class="block w-1/2">
139
+ <span class="text-gray-700">日期 (Date)</span>
140
+ <input type="date" id="admin-date" value="2025-01-10" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
141
+ </label>
142
+ <label class="block w-1/2">
143
+ <span class="text-gray-700">地點 (Location)</span>
144
+ <input type="text" id="admin-location" value="學務處會議室" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
145
+ </label>
146
+ </div>
147
+ <label class="block">
148
+ <span class="text-gray-700">輸入重點/逐字稿 (Key Input/Transcript)</span>
149
+ <textarea id="admin-key-input" rows="5" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">討論期末獎懲核定程序。新生訓練場地佈置、人員編組確認。</textarea>
150
+ </label>
151
+
152
+ <button onclick="generateDocument('admin')" id="admin-button"
153
+ class="w-full py-3 px-4 border border-transparent rounded-md shadow-sm text-lg font-medium text-white bg-primary hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition duration-150 ease-in-out">
154
+ 生成會議記錄預覽 (Generate Preview)
155
+ </button>
156
+ </div>
157
+ </div>
158
+
159
+ <div id="content-teaching" class="tab-content hidden">
160
+ <h2 class="text-2xl font-semibold text-secondary mb-4">教學 AI 設計器:教案與 Rubric 生成</h2>
161
+ <p class="text-sm text-gray-500 mb-6">📘 模擬生成符合課綱精神的教案結構和評量規準。</p>
162
+
163
+ <div class="space-y-4">
164
+ <div class="flex space-x-4">
165
+ <label class="block w-1/3">
166
+ <span class="text-gray-700">年級 (Grade)</span>
167
+ <select id="teaching-grade" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
168
+ <option value="國一">國一</option>
169
+ <option value="高一" selected>高一</option>
170
+ <option value="小六">小六</option>
171
+ </select>
172
+ </label>
173
+ <label class="block w-1/3">
174
+ <span class="text-gray-700">學科 (Subject)</span>
175
+ <input type="text" id="teaching-subject" value="歷史" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
176
+ </label>
177
+ <label class="block w-1/3">
178
+ <span class="text-gray-700">課時數 (Sessions)</span>
179
+ <input type="number" id="teaching-hours" value="4" min="1" max="10" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
180
+ </label>
181
+ </div>
182
+
183
+ <label class="block">
184
+ <span class="text-gray-700">單元主題 (Unit Topic)</span>
185
+ <input type="text" id="teaching-topic" value="從茶葉看全球化:17-19世紀的貿易網絡" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
186
+ </label>
187
+
188
+ <label class="block">
189
+ <span class="text-gray-700">教學法偏好 (Pedagogy Preference)</span>
190
+ <select id="teaching-method" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
191
+ <option value="探究式、PBL" selected>探究式、PBL</option>
192
+ <option value="翻轉教學">翻轉教學</option>
193
+ <option value="合作學習">合作學習</option>
194
+ </select>
195
+ </label>
196
+
197
+ <label class="block">
198
+ <span class="text-gray-700">班級特性 (Class Characteristics)</span>
199
+ <textarea id="teaching-class-needs" rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">班級組成多元,需考慮多樣化的史料呈現方式。</textarea>
200
+ </label>
201
+
202
+ <button onclick="generateDocument('teaching')" id="teaching-button"
203
+ class="w-full py-3 px-4 border border-transparent rounded-md shadow-sm text-lg font-medium text-white bg-secondary hover:bg-emerald-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-secondary transition duration-150 ease-in-out">
204
+ 生成教案預覽 (Generate Preview)
205
+ </button>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <!-- Right Column: Document Preview -->
211
+ <div class="bg-white shadow-xl rounded-xl p-6 h-full">
212
+ <h2 class="text-2xl font-semibold text-gray-800 mb-4">文件預覽 (DOCX Preview Mockup)</h2>
213
+ <div id="loading-indicator" class="hidden text-center py-8 text-lg text-gray-500">
214
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
215
+ AI 正在為您生成文件結構中...
216
+ </div>
217
+ <div id="output-preview"
218
+ class="markdown-preview custom-scroll bg-gray-50 p-6 rounded-lg border border-gray-200 h-[600px] overflow-y-auto text-gray-800">
219
+ <p class="text-gray-500 italic">請在左側輸入資料,然後點擊按鈕生成文件結構。</p>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <script>
226
+ // --- Core Simulation and Utility Functions ---
227
+
228
+ /**
229
+ * Simulates a structured response from the Gemini API.
230
+ * In a production environment, this would be a real fetch call.
231
+ */
232
+ function simulate_gemini_api_call(payload, fields) {
233
+ const systemInstruction = payload.systemInstruction.parts[0].text;
234
+ let mockTextResult;
235
+
236
+ if (systemInstruction.includes("台灣中學學務處行政書記")) {
237
+ // Simulate Admin Copilot (Meeting Minutes) output
238
+ mockTextResult = {
239
+ "文件類型 (Document Type)": "學務處會議記錄 (Academic Affairs Meeting Minutes)",
240
+ "meeting_info": {
241
+ "date": fields.date || '2025-01-10',
242
+ "location": fields.location || '會議室',
243
+ "topic": fields.topic || '模擬會議主題'
244
+ },
245
+ "attendees": ["校長 (Principal)", "學務主任 (Director of Student Affairs)", "全體學務處教職人員 (All Faculty)"],
246
+ "key_points": [
247
+ "期末獎懲核定程序已完成,共核定 30 件。建議將名單呈報校長核閱。",
248
+ "新生訓練場地佈置進度達 80%,物資清單已交付總務處採購。",
249
+ "決議於下次會議討論下學期活動預算編列。"
250
+ ],
251
+ "resolutions": [
252
+ {"item": "發布正式期末獎懲公告。", "responsible": "教務處 (Academic Affairs)", "deadline": "2025-01-15"},
253
+ {"item": "新生訓練場地佈置於活動前一天完成驗收。", "responsible": "總務處 (General Affairs)", "deadline": "2025-08-20"},
254
+ {"item": "提交下學期活動預算草案供下次會議討論。", "responsible": "會計室 (Accounting)", "deadline": "2025-01-30"}
255
+ ],
256
+ "audit_note": "文件根據校內行政公文標準格式生成。"
257
+ };
258
+ } else if (systemInstruction.includes("台灣國高中資深教師與課程設計師")) {
259
+ // Simulate Teaching Designer (Lesson Plan & Rubric) output
260
+ mockTextResult = {
261
+ "文件類型 (Document Type)": "單元教案與評量規準 (Lesson Plan & Rubric)",
262
+ "lesson_plan_title": `【${fields.subject || '歷史'}】從${fields.topic.split('看')[0]}看全球化 (${fields.hours} 課時)`,
263
+ "grade_level": fields.grade || '高一',
264
+ "curriculum_alignment": ["A2 邏輯推理與批判思辨 (Logical Reasoning)", "B3 獨立思考與探究精神 (Independent Inquiry)"],
265
+ "learning_objectives": [
266
+ "學生能解釋 17-19 世紀茶葉貿易在全球經濟中的角色。",
267
+ "學生能分析茶葉文化在不同地區的轉變與影響。",
268
+ "學生能應用史料證據論證全球化對現代生活的影響。"
269
+ ],
270
+ "activities": [
271
+ {"time_min": 15, "stage": "引導 (Introduction)", "method": "提問式教學", "description": "播放茶葉之路短片,引導學生思考『茶』如何改變世界。"},
272
+ {"time_min": 30, "stage": "活動一 (Activity 1)", "method": "小組合作學習", "description": "分組研讀不同地區(英國、中國、印度)的茶葉史料,並製作簡報。"},
273
+ {"time_min": 30, "stage": "活動二 (Activity 2)", "method": "探究與討論", "description": "以模擬國際會議形式,討論茶葉貿易中的道德與環境議題。"},
274
+ {"time_min": 15, "stage": "總結與作業 (Conclusion)", "method": "自我評量", "description": "學生完成概念圖並繳交反思報告。"}
275
+ ],
276
+ "rubric": {
277
+ "title": "單元評量規準 (四級四指標)",
278
+ "criteria": [
279
+ {"name": "概念理解 (Conceptual Understanding)", "A": "清晰、精確地解釋所有核心概念,並能融會貫通。", "D": "只能回答簡單的記憶性問題,缺乏連貫性。"},
280
+ {"name": "史料分析 (Source Analysis)", "A": "能批判性地使用多樣史料,提出獨立見解。", "D": "無法有效解讀史料,或未能在論證中引用。"},
281
+ {"name": "團隊合作 (Teamwork)", "A": "積極主導團隊討論,有效分配任務並提升整體表現。", "D": "討論中保持沉默,未對任務做出實質貢獻。"}
282
+ ]
283
+ },
284
+ "differentiation_advice": "針對閱讀障礙學生,提供圖文並茂的史料摘要卡;鼓勵資優學生擴展研究範圍至咖啡或糖的全球貿易史。"
285
+ };
286
+ } else {
287
+ return { "error": "Unknown or missing task instruction." };
288
+ }
289
+
290
+ // Simulate the final API response structure
291
+ return {
292
+ candidates: [{
293
+ content: {
294
+ parts: [{ text: JSON.stringify(mockTextResult, null, 2) }]
295
+ },
296
+ groundingMetadata: {}
297
+ }]
298
+ };
299
+ }
300
+
301
+ // --- Document Rendering Functions (JSON to Markdown Preview) ---
302
+
303
+ /**
304
+ * Renders the Admin Copilot (Meeting Minutes) JSON into Markdown.
305
+ */
306
+ function renderAdminMarkdown(data) {
307
+ const info = data.meeting_info;
308
+ let markdown = `# ${data['文件類型 (Document Type)']}\n\n`;
309
+
310
+ markdown += `## 會議資訊 (Meeting Details)\n`;
311
+ markdown += `| 項目 (Item) | 內容 (Content) |\n`;
312
+ markdown += `| :--- | :--- |\n`;
313
+ markdown += `| **主題 (Topic)** | ${info.topic} |\n`;
314
+ markdown += `| **日期 (Date)** | ${info.date} |\n`;
315
+ markdown += `| **地點 (Location)** | ${info.location} |\n`;
316
+ markdown += `| **出席人員 (Attendees)** | ${data.attendees.join('、')} |\n\n`;
317
+
318
+ markdown += `## 討論重點摘要 (Key Discussion Points)\n`;
319
+ data.key_points.forEach(point => {
320
+ markdown += `* ${point}\n`;
321
+ });
322
+ markdown += `\n`;
323
+
324
+ markdown += `## 會議決議與待辦事項 (Resolutions & Action Items)\n`;
325
+ markdown += `| 決議事項 (Item) | 負責人 (Responsible) | 期限 (Deadline) |\n`;
326
+ markdown += `| :--- | :--- | :--- |\n`;
327
+ data.resolutions.forEach(res => {
328
+ markdown += `| ${res.item} | ${res.responsible} | ${res.deadline} |\n`;
329
+ });
330
+ markdown += `\n`;
331
+
332
+ markdown += `*行政備註 (Audit Note): ${data.audit_note}*\n`;
333
+
334
+ return markdown;
335
+ }
336
+
337
+ /**
338
+ * Renders the Teaching Designer (Lesson Plan) JSON into Markdown.
339
+ */
340
+ function renderTeachingMarkdown(data) {
341
+ let markdown = `# ${data.lesson_plan_title}\n\n`;
342
+ markdown += `## 單元概覽 (Unit Overview)\n`;
343
+ markdown += `* **文件類型:** ${data['文件類型 (Document Type)']}\n`;
344
+ markdown += `* **適用年級:** ${data.grade_level}\n\n`;
345
+
346
+ markdown += `## 學習目標 (Learning Objectives)\n`;
347
+ data.learning_objectives.forEach((obj, index) => {
348
+ markdown += `* **目標 ${index + 1}:** ${obj}\n`;
349
+ });
350
+ markdown += `\n`;
351
+
352
+ markdown += `## 課綱素養對齊 (Curriculum Alignment)\n`;
353
+ markdown += `本單元主要對應以下核心素養:\n`;
354
+ data.curriculum_alignment.forEach(align => {
355
+ markdown += `* ${align}\n`;
356
+ });
357
+ markdown += `\n`;
358
+
359
+ markdown += `## 教學活動步驟 (Lesson Activities)\n`;
360
+ markdown += `| 階段 (Stage) | 耗時 (Min) | 教學法 (Method) | 內容說明 (Description) |\n`;
361
+ markdown += `| :--- | :--- | :--- | :--- |\n`;
362
+ data.activities.forEach(activity => {
363
+ markdown += `| ${activity.stage} | ${activity.time_min} | ${activity.method} | ${activity.description} |\n`;
364
+ });
365
+ markdown += `\n`;
366
+
367
+ markdown += `## 評量規準 (Assessment Rubric)\n`;
368
+ const rubric = data.rubric;
369
+ markdown += `### ${rubric.title}\n`;
370
+ markdown += `| 評量指標 (Criteria) | A (精熟) | D (待加強) |\n`;
371
+ markdown += `| :--- | :--- | :--- |\n`;
372
+ rubric.criteria.forEach(crit => {
373
+ markdown += `| ${crit.name} | ${crit.A} | ${crit.D} |\n`;
374
+ });
375
+ markdown += `\n`;
376
+
377
+ markdown += `## 差異化教學建議 (Differentiation Advice)\n`;
378
+ markdown += `<p>${data.differentiation_advice}</p>\n`;
379
+
380
+ return markdown;
381
+ }
382
+
383
+
384
+ /**
385
+ * Main function to handle input, call simulation, and display output.
386
+ */
387
+ async function generateDocument(module) {
388
+ const outputElement = document.getElementById('output-preview');
389
+ const loadingElement = document.getElementById('loading-indicator');
390
+ const button = document.getElementById(`${module}-button`);
391
+
392
+ button.disabled = true;
393
+ loadingElement.classList.remove('hidden');
394
+ outputElement.innerHTML = ''; // Clear previous output
395
+
396
+ let fields = {};
397
+ let systemPrompt = "";
398
+ let userQuery = "";
399
+ let renderFunction;
400
+
401
+ // 1. Collect inputs and define API payload based on module
402
+ if (module === 'admin') {
403
+ fields.topic = document.getElementById('admin-topic').value;
404
+ fields.date = document.getElementById('admin-date').value;
405
+ fields.location = document.getElementById('admin-location').value;
406
+ fields.key_input = document.getElementById('admin-key-input').value;
407
+ renderFunction = renderAdminMarkdown;
408
+
409
+ systemPrompt = (
410
+ "角色:台灣中學學務處行政書記\n" +
411
+ "輸出:JSON(會議資訊、出席、重點、決議、待辦、負責人、期限)\n" +
412
+ "格式規範:用詞正式、避免口語、保留專有名���\n" +
413
+ "限制:所有決議必須有負責人和明確期限。"
414
+ );
415
+ userQuery = `請生成一份會議記錄。主題: ${fields.topic}; 輸入重點(或逐字稿):${fields.key_input}`;
416
+ } else if (module === 'teaching') {
417
+ fields.grade = document.getElementById('teaching-grade').value;
418
+ fields.subject = document.getElementById('teaching-subject').value;
419
+ fields.topic = document.getElementById('teaching-topic').value;
420
+ fields.hours = parseInt(document.getElementById('teaching-hours').value);
421
+ fields.method = document.getElementById('teaching-method').value;
422
+ // equipment and class_needs are simulated implicitly
423
+ fields.class_needs = document.getElementById('teaching-class-needs').value;
424
+ renderFunction = renderTeachingMarkdown;
425
+
426
+ systemPrompt = (
427
+ "角色:台灣國高中資深教師與課程設計師\n" +
428
+ "輸出:JSON(教案標題、目標、課綱對齊、活動步驟、評量規準、差異化建議)\n" +
429
+ "限制:活動分鏡以 15 分鐘粒度;至少 2 項形成性評量。\n" +
430
+ "對齊:請將輸出中的 'curriculum_alignment' 欄位,對齊台灣課綱的關鍵能力/素養。"
431
+ );
432
+ userQuery = (
433
+ `請根據以下資訊設計一個單元教案、評量規準和差異化建議:\n` +
434
+ `年級/學科/單元主題: ${fields.grade}/${fields.subject}/${fields.topic}\n` +
435
+ `課時數: ${fields.hours} 節\n` +
436
+ `教學法偏好: ${fields.method}\n` +
437
+ `班級特性: ${fields.class_needs}`
438
+ );
439
+ }
440
+
441
+ const payload = {
442
+ contents: [{ parts: [{ text: userQuery }] }],
443
+ systemInstruction: { parts: [{ text: systemPrompt }] },
444
+ // generationConfig with schema is required for structured output, but we mock it.
445
+ generationConfig: { responseMimeType: "application/json" }
446
+ };
447
+
448
+ // 2. Call the simulated API (Wait for 1.5s to simulate network latency)
449
+ await new Promise(resolve => setTimeout(resolve, 1500));
450
+ const apiResponse = simulate_gemini_api_call(payload, fields);
451
+
452
+ // 3. Process the response
453
+ try {
454
+ const jsonString = apiResponse.candidates[0].content.parts[0].text;
455
+ const data = JSON.parse(jsonString);
456
+
457
+ const markdownOutput = renderFunction(data);
458
+
459
+ // Use a simple pre-formatted block for the markdown to simulate the document appearance
460
+ // Note: Full Markdown rendering engine is not used, just basic HTML elements styled via markdown-preview class
461
+ outputElement.innerHTML = `
462
+ <div class="space-y-6">
463
+ <div class="markdown-preview">${convertMarkdownToHTML(markdownOutput)}</div>
464
+ <h3 class="text-xl font-semibold text-gray-700 pt-4 border-t">JSON 原始輸出 (For DOCX Templating)</h3>
465
+ <pre class="bg-gray-100 p-4 rounded-md text-xs overflow-x-auto border border-gray-300">${jsonString}</pre>
466
+ </div>
467
+ `;
468
+
469
+ } catch (error) {
470
+ console.error("Processing error:", error);
471
+ outputElement.innerHTML = `<div class="p-4 bg-red-100 text-red-700 rounded-md">文件生成失敗:無法解析 AI 結構化輸出。請檢查輸入內容是否完整。</div>`;
472
+ } finally {
473
+ loadingElement.classList.add('hidden');
474
+ button.disabled = false;
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Simple function to convert basic Markdown (headers, lists, tables) to HTML.
480
+ * Note: This is a minimal conversion, not a full MD parser.
481
+ */
482
+ function convertMarkdownToHTML(md) {
483
+ // Replace headers
484
+ md = md.replace(/^#\s*(.*)$/gm, '<h1>$1</h1>');
485
+ md = md.replace(/^##\s*(.*)$/gm, '<h2>$1</h2>');
486
+ md = md.replace(/^###\s*(.*)$/gm, '<h3>$1</h3>');
487
+
488
+ // Replace lists (simple * to <li>)
489
+ md = md.replace(/^\*\s*(.*)$/gm, '<li>$1</li>');
490
+ // Wrap lists in <ul>
491
+ md = md.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
492
+ md = md.replace(/<\/li>\s*<ul>/g, '</li><ul>');
493
+
494
+ // Handle tables (very basic, requires specific MD format from renderer)
495
+ const tableRegex = /^(?:\|.*?\|(?:.*?\|)*\n)+/gm;
496
+ md = md.replace(tableRegex, (tableMatch) => {
497
+ let html = '<table>';
498
+ const rows = tableMatch.trim().split('\n').filter(r => r.trim() !== '');
499
+
500
+ // Header (1st row)
501
+ const headerCells = rows[0].match(/\|(.*?)(?=\|)/g).map(c => c.slice(1).trim());
502
+ html += '<thead><tr>' + headerCells.map(c => `<th>${c}</th>`).join('') + '</tr></thead><tbody>';
503
+
504
+ // Data rows (skip 2nd row which is separator)
505
+ for (let i = 2; i < rows.length; i++) {
506
+ const dataCells = rows[i].match(/\|(.*?)(?=\|)/g).map(c => c.slice(1).trim());
507
+ html += '<tr>' + dataCells.map(c => `<td>${c}</td>`).join('') + '</tr>';
508
+ }
509
+ html += '</tbody></table>';
510
+ return html;
511
+ });
512
+
513
+ // Convert remaining lines to paragraphs
514
+ md = md.split('\n\n').map(p => {
515
+ // Ignore lines already processed as headers/lists/tables or empty
516
+ if (p.startsWith('<h') || p.startsWith('<ul>') || p.startsWith('<table') || p.trim() === '') {
517
+ return p;
518
+ }
519
+ // Handle specific P tags from renderer
520
+ if (p.startsWith('<p>') && p.endsWith('</p>')) {
521
+ return p;
522
+ }
523
+ // Wrap plain text in paragraph tags
524
+ return `<p>${p.trim()}</p>`;
525
+ }).join('\n');
526
+
527
+ return md;
528
+ }
529
+
530
+
531
+ // --- UI Logic ---
532
+
533
+ let activeTab = 'admin';
534
+
535
+ function switchTab(tabId) {
536
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
537
+ document.querySelectorAll('.tab-button').forEach(el => el.classList.remove('active'));
538
+
539
+ document.getElementById(`content-${tabId}`).classList.remove('hidden');
540
+ document.getElementById(`tab-${tabId}`).classList.add('active');
541
+ activeTab = tabId;
542
+ }
543
+
544
+ // Initial setup
545
+ document.addEventListener('DOMContentLoaded', () => {
546
+ switchTab(activeTab);
547
+ });
548
+ </script>
549
+ </body>
550
+ </html>
551
+