Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,249 +1,551 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 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 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
"generationConfig": {
|
| 131 |
-
"responseMimeType": "application/json",
|
| 132 |
-
"responseSchema": response_schema
|
| 133 |
}
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 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 |
-
|
| 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 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
)
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|