File size: 14,526 Bytes
7ed6c2d
 
a810a27
7ed6c2d
 
f812bd3
a810a27
7ed6c2d
 
f812bd3
7ed6c2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f812bd3
7ed6c2d
 
 
 
 
 
 
 
 
 
 
 
 
f812bd3
7ed6c2d
 
 
f812bd3
7ed6c2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f812bd3
7ed6c2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f812bd3
7ed6c2d
a810a27
f812bd3
a810a27
7ed6c2d
a810a27
 
7ed6c2d
 
a810a27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7ed6c2d
a810a27
 
f812bd3
 
a810a27
7ed6c2d
 
 
a810a27
7ed6c2d
a810a27
 
 
 
 
 
 
 
 
 
7ed6c2d
 
 
 
 
 
a810a27
7ed6c2d
 
 
 
 
f812bd3
 
 
 
7ed6c2d
 
a810a27
 
7ed6c2d
 
 
f812bd3
a810a27
 
 
7ed6c2d
 
 
f812bd3
a810a27
7ed6c2d
f812bd3
 
a810a27
7ed6c2d
 
 
a810a27
7ed6c2d
a810a27
 
 
 
 
 
 
 
 
 
 
 
 
7ed6c2d
 
 
 
 
 
a810a27
f812bd3
7ed6c2d
e8aa402
7ed6c2d
 
 
 
 
 
 
 
 
 
f812bd3
 
 
 
7ed6c2d
 
a810a27
 
f812bd3
7ed6c2d
 
f812bd3
a810a27
f812bd3
a810a27
7ed6c2d
 
 
f812bd3
a810a27
 
7ed6c2d
f812bd3
7ed6c2d
f812bd3
7ed6c2d
 
 
 
 
 
 
 
 
a810a27
7ed6c2d
a810a27
7ed6c2d
 
 
f812bd3
7ed6c2d
 
 
 
 
 
 
 
 
 
 
a810a27
7ed6c2d
a810a27
7ed6c2d
 
 
f812bd3
7ed6c2d
 
 
a810a27
7ed6c2d
 
 
f812bd3
7ed6c2d
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
import json
import time
import gradio as gr  # 匯入 Gradio
from typing import Dict, Any

# --- Markdown 轉換輔助函式 ---
# (此區域功能是將 JSON 轉換為易於閱讀的 Markdown)

def json_to_admin_markdown(data: Dict[str, Any]) -> str:
    """將「會議記錄」JSON 轉換為 Markdown 格式。"""
    try:
        md = f"# {data.get('文件類型 (Document Type)', '會議記錄')}\n\n"
        
        info = data.get("meeting_info", {})
        md += f"## {info.get('topic', '會議主題')}\n"
        md += f"* **日期 (Date):** {info.get('date', 'N/A')}\n"
        md += f"* **地點 (Location):** {info.get('location', 'N/A')}\n\n"

        md += "### 出席人員 (Attendees)\n"
        for attendee in data.get("attendees", ["N/A"]):
            md += f"* {attendee}\n"
        md += "\n"

        md += "### 會議重點 (Key Points)\n"
        for i, point in enumerate(data.get("key_points", ["N/A"]), 1):
            md += f"{i}.  {point}\n"
        md += "\n"

        md += "### 決議事項 (Resolutions)\n"
        resolutions = data.get("resolutions", [])
        if resolutions:
            md += "| 任務 (Item) | 負責人 (Responsible) | 期限 (Deadline) |\n"
            md += "|:---|:---|:---|\n"
            for item in resolutions:
                md += f"| {item.get('item', '')} | {item.get('responsible', '')} | {item.get('deadline', '')} |\n"
        else:
            md += "無\n"
        md += "\n"
        
        md += "---\n"
        md += f"_{data.get('audit_note', '')}_\n"
        
        return md
        
    except Exception as e:
        return f"ERROR 產生 Markdown 預覽時出錯: {e}"


def json_to_lesson_plan_markdown(data: Dict[str, Any]) -> str:
    """將「教案」JSON 轉換為 Markdown 格式。"""
    try:
        md = f"# {data.get('lesson_plan_title', '教案標題')}\n\n"
        md += f"* **適用年級 (Grade Level):** {data.get('grade_level', 'N/A')}\n\n"

        md += "### 課綱對齊 (Curriculum Alignment)\n"
        for item in data.get("curriculum_alignment", ["N/A"]):
            md += f"* {item}\n"
        md += "\n"

        md += "### 學習目標 (Learning Objectives)\n"
        for i, item in enumerate(data.get("learning_objectives", ["N/A"]), 1):
            md += f"{i}.  {item}\n"
        md += "\n"

        md += "### 活動流程 (Activities)\n"
        activities = data.get("activities", [])
        if activities:
            md += "| 時間 (min) | 階段 (Stage) | 方法 (Method) | 描述 (Description) |\n"
            md += "|:---|:---|:---|:---|\n"
            for item in activities:
                md += f"| {item.get('time_min', '')} | {item.get('stage', '')} | {item.get('method', '')} | {item.get('description', '')} |\n"
        md += "\n"

        md += "### 評量規準 (Rubric)\n"
        rubric = data.get("rubric", {})
        md += f"**{rubric.get('title', '評量規準')}**\n\n"
        
        criteria = rubric.get("criteria", [])
        if criteria:
            md += "| 指標 (Criteria) | A (精熟) | D (待加強) |\n"
            md += "|:---|:---|:---|\n"
            for item in criteria:
                md += f"| {item.get('name', '')} | {item.get('A', '')} | {item.get('D', '')} |\n"
        md += "\n"

        md += "### 差異化建議 (Differentiation Advice)\n"
        md += f"{data.get('differentiation_advice', 'N/A')}\n"
        
        return md

    except Exception as e:
        return f"ERROR 產生 Markdown 預覽時出錯: {e}"

# --- 模擬 API 呼叫函式 ---

def simulate_gemini_api_call(payload: Dict[str, Any], fields: Dict[str, Any]) -> str:
    """
    模擬 Gemini API 的結構化回應。
    返回一個 JSON 字串。
    """
    
    # 模擬延遲
    time.sleep(1.5) 
    
    system_instruction = payload.get('systemInstruction', {}).get('parts', [{}])[0].get('text', '')
    
    # 根據系統提示 (System Prompt) 決定要模擬哪種輸出
    
    if "台灣中學學務處行政書記" in system_instruction:
        # 模擬 Admin Copilot (會議記錄) 輸出
        mock_data = {
            "文件類型 (Document Type)": "學務處會議記錄 (模擬)",
            "meeting_info": {
                "date": fields.get('date', '2025-01-10'),
                "location": fields.get('location', '會議室'),
                "topic": fields.get('topic', '模擬會議主題')
            },
            "attendees": ["校長", "學務主任", "教務主任", "輔導室主任"],
            "key_points": [
                f"基於使用者輸入「{fields.get('key_input', '...')}」進行討論。",
                "新生訓練籌備工作已完成 80%。",
                "確認場地佈置與人力編組。"
            ],
            "resolutions": [
                {"item": "發布期末獎懲正式公告", "responsible": "生輔組", "deadline": "2025-01-15"},
                {"item": "新生訓練場地於前一日完成佈置", "responsible": "總務處", "deadline": "2025-08-20"}
            ],
            "audit_note": "文件由 AI 模擬生成,符合校內寫作規範。"
        }
        return json.dumps(mock_data, ensure_ascii=False)

    elif "台灣國高中資深教師與課程設計師" in system_instruction:
        # 模擬 Teaching Designer (教案) 輸出
        mock_data = {
            "文件類型 (Document Type)": "單元教案與評量規準 (模擬)",
            "lesson_plan_title": f"【{fields.get('subject', 'N/A')}】探索:{fields.get('topic', 'N/A')} ({int(fields.get('hours', 0))} 節課)",
            "grade_level": fields.get('grade', 'N/A'),
            "curriculum_alignment": ["A2 系統思考與解決問題 (課綱素養)", "B3 規劃執行與創新應變"],
            "learning_objectives": ["學生能說明 X 的核心概念。", "學生能應用 Y 技能進行分析。", "學生能透過 Z 進行小組協作。"],
            "activities": [
                {"time_min": 15, "stage": "引起動機", "method": "提問法", "description": "使用真實案例影片,引導學生思考主題。"},
                {"time_min": 30, "stage": "主要活動一", "method": fields.get('method', '合作學習'), "description": "分組進行資料搜集與主題探究 (使用 {equipment})。".format(equipment=fields.get('equipment', 'N/A'))},
                {"time_min": 25, "stage": "成果發表", "method": "發表與問答", "description": "各組上台報告初步發現。"},
                {"time_min": 20, "stage": "總結與評量", "method": "形成性評量", "description": "教師總結,並進行快速測驗。"}
            ],
            "rubric": {
                "title": "單元評量規準 (4 級分)",
                "criteria": [
                    {"name": "概念理解", "A": "能清晰且準確地解釋所有核心概念。", "D": "僅能回答基礎問題。"},
                    {"name": "團隊協作", "A": "積極主動領導團隊,有效分工。", "D": "未參與團隊討論。"},
                    {"name": "資料分析", "A": "能運用多種史料進行深入分析。", "D": "僅列出資料,未加分析。"}
                ]
            },
            "differentiation_advice": f"針對特性「{fields.get('class_needs', 'N/A')}」,建議提供補充詞彙卡或鷹架提問單。"
        }
        return json.dumps(mock_data, ensure_ascii=False)
        
    else:
        return json.dumps({"error": "未知的模擬任務", "message": "系統提示不符"}, ensure_ascii=False)

# --- 模組 A: 行政 Copilot 生成器 (Gradio 封裝) ---
# (已修改為使用 simulate_gemini_api_call)

def admin_copilot_generator(template_id: str, topic: str, date: str, location: str, key_input: str) -> str:
    """
    處理 Admin Copilot 的 UI 輸入,呼叫「模擬」 API,並轉換為 Markdown。
    """
    
    # 這些是模擬 API 需要的欄位
    fields = {
        "topic": topic,
        "date": date,
        "location": location,
        "key_input": key_input
    }
    
    # 這些是建構 payload 所需的 (模擬時僅用於判斷任務類型)
    system_prompt = (
        "角色:台灣中學學務處行政書記\n"
        "輸出:JSON(會議資訊、出席、重點、決議、待辦、負責人、期限)\n"
        "格式規範:用詞正式、避免口語、保留專有名詞\n"
        "限制:所有決議必須有負責人和明確期限。"
    )
    response_schema = { "type": "OBJECT" } # 模擬時不需要完整 Schema
    user_query = f"請生成一份會議記錄。主題: {topic}; 輸入重點(或逐字稿):{key_input}"
    
    payload = {
        "contents": [{ "parts": [{ "text": user_query }] }],
        "systemInstruction": { "parts": [{ "text": system_prompt }] },
        "generationConfig": {
            "responseMimeType": "application/json",
            "responseSchema": response_schema
        }
    }

    # 呼叫模擬 API
    json_string = simulate_gemini_api_call(payload, fields)
    
    try:
        data = json.loads(json_string)
        if "error" in data:
            return f"### 模擬錯誤\n\n**訊息:**\n```\n{data.get('message')}\n```"
        
        # 將模擬 JSON 轉換為 Markdown
        markdown_output = json_to_admin_markdown(data)
        return markdown_output
            
    except json.JSONDecodeError as e:
        return f"### 處理回應時出錯\n\n無法解碼模擬 JSON。\n\n**收到的回應:**\n```\n{json_string}\n```"


# --- 模組 B: 教學 AI 設計器 (Gradio 封裝) ---
# (已修改為使用 simulate_gemini_api_call)

def lesson_plan_designer(grade: str, subject: str, topic: str, hours: float, method: str, equipment: str, class_needs: str) -> str:
    """
    處理教學設計器的 UI 輸入,呼叫「模擬」 API,並轉換為 Markdown。
    """
    
    # 這些是模擬 API 需要的欄位
    fields = {
        "grade": grade,
        "subject": subject,
        "topic": topic,
        "hours": hours,
        "method": method,
        "equipment": equipment,
        "class_needs": class_needs
    }
    
    # 這些是建構 payload 所需的 (模擬時僅用於判斷任務類型)
    system_prompt = (
        "角色:台灣國高中資深教師與課程設計師\n"
        "輸出:JSON(教案標題、目標、課綱對齊、活動步驟、評量規準、差異化建議)\n"
        "限制:活動分鏡以 15 分鐘粒度;至少 2 項形成性評量。\n"
        "對齊:請將輸出中的 'curriculum_alignment' 欄位,對齊台灣課綱的關鍵能力/素養。"
    )
    response_schema = { "type": "OBJECT" } # 模擬時不需要完整 Schema

    user_query = (
        f"請根據以下資訊設計一個單元教案、評量規數與差異化建議:\n"
        f"年級/學科/單元主題: {grade}/{subject}/{topic}\n"
        f"課時數: {int(hours)} 節\n"
        f"教學法偏好: {method}\n"
        f"可用設備: {equipment}\n"
        f"班級特性: {class_needs}"
    )

    payload = {
        "contents": [{ "parts": [{ "text": user_query }] }],
        "systemInstruction": { "parts": [{ "text": system_prompt }] },
        "generationConfig": {
            "responseMimeType": "application/json",
            "responseSchema": response_schema
        }
    }

    # 呼叫模擬 API
    json_string = simulate_gemini_api_call(payload, fields)

    try:
        data = json.loads(json_string)
        if "error" in data:
            return f"### 模擬錯誤\n\n**訊息:**\n```\n{data.get('message')}\n```"

        # 將模擬 JSON 轉換為 Markdown
        markdown_output = json_to_lesson_plan_markdown(data)
        return markdown_output

    except json.JSONDecodeError as e:
        return f"### 處理回應時出錯\n\n無法解碼模擬 JSON。\n\n**收到的回應:**\n```\n{json_string}\n```"


# --- Gradio 介面定義 ---

# 模組 A 介面 (Admin Copilot)
admin_copilot_interface = gr.Interface(
    fn=admin_copilot_generator,
    inputs=[
        gr.Textbox(label="模板 ID (Template ID - Fixed for MVP)", value="meeting_minutes_standard", interactive=False),
        gr.Textbox(label="會議主題 (Meeting Topic)", value="學務處期末獎懲與新生訓練籌備會議"),
        gr.Textbox(label="日期 (Date)", value="2025-01-10"),
        gr.Textbox(label="地點 (Location)", value="學務處會議室"),
        gr.Textbox(label="輸入重點/逐字稿 (Key Input/Transcript)", value="討論期末獎懲核定程序。新生訓練場地佈置、人員編組確認。", lines=5),
    ],
    outputs=gr.Markdown(label="AI 模擬文件預覽 (Markdown Preview)"),
    title="行政 Copilot:會議記錄生成 (Admin Copilot: Meeting Minutes Generation)",
    description="🎯 (模擬演示版) 產生行政文件預覽。此版本不需 API 金鑰,使用固定的模擬資料。",
    flagging_mode="never",
)

# 模組 B 介面 (Teaching Designer)
lesson_plan_designer_interface = gr.Interface(
    fn=lesson_plan_designer,
    inputs=[
        gr.Dropdown(label="年級 (Grade)", choices=["國中", "高中", "國小"], value="高中"),
        gr.Textbox(label="學科 (Subject)", value="歷史"),
        gr.Textbox(label="單元主題 (Unit Topic)", value="從茶葉看全球化:17-19世紀的貿易網絡"),
        gr.Slider(label="課時數 (Number of Sessions)", minimum=1, maximum=10, step=1, value=4),
        gr.Dropdown(label="教學法偏好 (Pedagogy Preference)", choices=["探究式、PBL", "翻轉教學", "合作學習", "講述法"], value="探究式、PBL"),
        gr.Textbox(label="可用設備 (Available Equipment)", value="平板電腦、投影設備、網路"),
        gr.Textbox(label="班級特性 (Class Characteristics)", value="班級組成多元,需考慮多樣化的史料呈現方式。"),
    ],
    outputs=gr.Markdown(label="AI 模擬教案預覽 (Markdown Preview)"),
    title="教學 AI 設計器:教案與 Rubric 生成 (Teaching AI Designer: Lesson Plan & Rubric)",
    description="📘 (模擬演示版) 產生符合課綱精神的單元教案結構和評量規準預覽。",
    flagging_mode="never",
)

# 將兩個模組整合到分頁介面中
demo = gr.TabbedInterface(
    [admin_copilot_interface, lesson_plan_designer_interface],
    ["模組 A: 行政 Copilot", "模組 B: 教學設計器"],
    title="CampusAI Suite (台灣校園 AI 文書/教學 MVP 演示) - 模擬版",
    theme=gr.themes.Soft()
)

# --- 啟動應用程式 ---
if __name__ == "__main__":
    demo.launch()