cwadayi commited on
Commit
ce92422
·
verified ·
1 Parent(s): 9e484ae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -23
app.py CHANGED
@@ -13,29 +13,44 @@ from pathlib import Path
13
  from datetime import datetime
14
  import pytz
15
 
16
- # ===== FIX: Corrected the import path for ApiException =====
17
  from linebot.v3.messaging import (
18
  Configuration,
19
  ApiClient,
20
  MessagingApi,
21
  PushMessageRequest,
22
  TextMessage,
23
- ApiException # <-- ApiException is part of the messaging module
24
  )
25
- # ==========================================================
26
 
27
  # --- 網站分析與計數器 ---
28
  COUNTER_FILE = Path("visits.txt")
29
  def get_and_update_visits():
30
- if not COUNTER_FILE.exists(): count = 1
 
 
 
31
  else:
32
- try: count = int(COUNTER_FILE.read_text()) + 1
33
- except (ValueError, IOError): count = 1
34
- COUNTER_FILE.write_text(str(count))
 
 
 
35
  return count
36
 
37
- # --- 發送 LINE 通知 ---
38
- def send_line_notification(visit_count):
 
 
 
 
 
 
 
 
 
 
39
  access_token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')
40
  user_id = os.environ.get('YOUR_LINE_USER_ID')
41
 
@@ -45,10 +60,6 @@ def send_line_notification(visit_count):
45
 
46
  configuration = Configuration(access_token=access_token)
47
 
48
- tz = pytz.timezone('Asia/Taipei')
49
- current_time = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
50
- message_text = f"🌐 網頁新載入!\n\n時間: {current_time}\n總載入次數: {visit_count}\n\n恭喜!您的地球物理課程網頁又多了一次瀏覽!"
51
-
52
  try:
53
  with ApiClient(configuration) as api_client:
54
  line_bot_api = MessagingApi(api_client)
@@ -62,12 +73,12 @@ def send_line_notification(visit_count):
62
  except ApiException as e:
63
  print(f"Error sending LINE notification: {e.body}")
64
 
65
- # ===== 在應用程式啟動時,執行計數與通知 =====
66
  count = get_and_update_visits()
67
  visit_count_html = f"🚀 **總載入次數:** {count}"
68
- send_line_notification(count)
69
 
70
- # --- 1. & 2. 課程資訊與進度表 ---
 
71
  course_introduction_md = """
72
  # 探索地球的脈動:用程式與AI解碼地球奧秘
73
  **課程網頁 (Hugging Face Space)**
@@ -122,6 +133,8 @@ grading_policy_md = """
122
  * **期中考 (40%)**: 範圍涵蓋前八週的地球物理知識與資料處理技能,用以檢核您對核心概念的理解程度。
123
  * **平時成績 (10%)**: 根據您的課堂參與度、提問與互動表現進行評估。積極參與是學習的催化劑!
124
  """
 
 
125
  schedule_data = {
126
  "週次": ["第一週 (9/10)", "第二週 (9/17)", "第三週 (9/24)", "第四週 (10/1)", "第五週 (10/8)", "第六週 (10/15)", "第七週 (10/22)", "第八週 (10/29)", "第九週 (11/5)", "第十週 (11/12)", "第十一週 (11/19)", "第十二週 (11/26)", "第十三週 (12/3)", "第十四週 (12/10)", "第十五週 (12/17)", "第十六週", "十七週", "第十八週"],
127
  "地球物理知識": ["地球物理概要", "折射探勘學", "校園野外實驗 (折射探勘)", "地球物理專題演講", "折射探勘學", "重力探勘學", "重力探勘學", "期中考", "板塊構造與地震", "板塊構造與地震", "板塊構造與地磁", "板塊構造與地磁", "板塊構造與地熱", "板塊構造與地熱", "期末專題發表", "行憲紀念日 (放假)", "元旦 (放假)", "參訪大屯火山觀測中心 (暫定)"],
@@ -182,8 +195,10 @@ ax.set_title(plot_title); ax.set_xlabel("Time (seconds)"); ax.set_ylabel("Amplit
182
  print(f"P-S time difference: {s_wave_arrival - p_wave_arrival} seconds")
183
  # fig = fig
184
  """
185
- def execute_user_code(code_string):
186
  string_io = io.StringIO()
 
 
187
  try:
188
  with contextlib.redirect_stdout(string_io):
189
  local_scope = {}
@@ -192,17 +207,80 @@ def execute_user_code(code_string):
192
  console_output = string_io.getvalue()
193
  fig = local_scope.get('fig')
194
  if fig is None:
195
- return None, f"程式碼執行完畢,但未找到 'fig' 物件。\nPrint 輸出:\n{console_output}"
196
- return fig, f" 程式碼執行成功!\n\n--- Console Output ---\n{console_output}"
 
 
 
 
197
  except Exception:
198
- return None, f"❌ 程式碼執行失敗!\n\n--- Error Traceback ---\n{traceback.format_exc()}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  # --- AI 助教知識庫 ---
201
  KNOWLEDGE_BASE = [
202
- # ... (All 27+ knowledge points remain the same) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  ]
204
 
205
  def ai_chatbot_with_kb(message, history):
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  user_message = message.lower()
207
  for item in KNOWLEDGE_BASE:
208
  for keyword in item['keywords']:
@@ -256,8 +334,17 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange"),
256
  description="由結構化知識庫驅動的問答機器人"
257
  )
258
 
259
- map_run_button.click(fn=execute_user_code, inputs=[map_code], outputs=[map_plot_output, map_console_output])
260
- seismo_run_button.click(fn=execute_user_code, inputs=[seismo_code], outputs=[seismo_plot_output, seismo_console_output])
 
 
 
 
 
 
 
 
 
261
 
262
  # --- 5. 啟動應用程式 ---
263
  demo.launch()
 
13
  from datetime import datetime
14
  import pytz
15
 
16
+ # 導入 LINE Bot SDK
17
  from linebot.v3.messaging import (
18
  Configuration,
19
  ApiClient,
20
  MessagingApi,
21
  PushMessageRequest,
22
  TextMessage,
23
+ ApiException
24
  )
 
25
 
26
  # --- 網站分析與計數器 ---
27
  COUNTER_FILE = Path("visits.txt")
28
  def get_and_update_visits():
29
+ """讀取、更新並返回訪客計數。"""
30
+ if not COUNTER_FILE.exists():
31
+ count = 1
32
+ COUNTER_FILE.write_text(str(count))
33
  else:
34
+ try:
35
+ count = int(COUNTER_FILE.read_text()) + 1
36
+ COUNTER_FILE.write_text(str(count))
37
+ except (ValueError, IOError):
38
+ count = 1
39
+ COUNTER_FILE.write_text(str(count))
40
  return count
41
 
42
+ def get_current_visit_count():
43
+ """僅讀取目前的訪客計數,不增加。"""
44
+ if not COUNTER_FILE.exists():
45
+ return 0
46
+ try:
47
+ return int(COUNTER_FILE.read_text())
48
+ except (ValueError, IOError):
49
+ return 0
50
+
51
+ # --- 優化後的 LINE 通知功能 ---
52
+ def send_line_notification(message_text):
53
+ """發送一條自訂訊息的 LINE Push Message 通知開發者。"""
54
  access_token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')
55
  user_id = os.environ.get('YOUR_LINE_USER_ID')
56
 
 
60
 
61
  configuration = Configuration(access_token=access_token)
62
 
 
 
 
 
63
  try:
64
  with ApiClient(configuration) as api_client:
65
  line_bot_api = MessagingApi(api_client)
 
73
  except ApiException as e:
74
  print(f"Error sending LINE notification: {e.body}")
75
 
76
+ # ===== 在應用程式啟動時,只更新計數,不發通知 =====
77
  count = get_and_update_visits()
78
  visit_count_html = f"🚀 **總載入次數:** {count}"
 
79
 
80
+
81
+ # --- 1. 各區塊的 Markdown 內容 ---
82
  course_introduction_md = """
83
  # 探索地球的脈動:用程式與AI解碼地球奧秘
84
  **課程網頁 (Hugging Face Space)**
 
133
  * **期中考 (40%)**: 範圍涵蓋前八週的地球物理知識與資料處理技能,用以檢核您對核心概念的理解程度。
134
  * **平時成績 (10%)**: 根據您的課堂參與度、提問與互動表現進行評估。積極參與是學習的催化劑!
135
  """
136
+
137
+ # --- 2. 課程進度表 DataFrame ---
138
  schedule_data = {
139
  "週次": ["第一週 (9/10)", "第二週 (9/17)", "第三週 (9/24)", "第四週 (10/1)", "第五週 (10/8)", "第六週 (10/15)", "第七週 (10/22)", "第八週 (10/29)", "第九週 (11/5)", "第十週 (11/12)", "第十一週 (11/19)", "第十二週 (11/26)", "第十三週 (12/3)", "第十四週 (12/10)", "第十五週 (12/17)", "第十六週", "十七週", "第十八週"],
140
  "地球物理知識": ["地球物理概要", "折射探勘學", "校園野外實驗 (折射探勘)", "地球物理專題演講", "折射探勘學", "重力探勘學", "重力探勘學", "期中考", "板塊構造與地震", "板塊構造與地震", "板塊構造與地磁", "板塊構造與地磁", "板塊構造與地熱", "板塊構造與地熱", "期末專題發表", "行憲紀念日 (放假)", "元旦 (放假)", "參訪大屯火山觀測中心 (暫定)"],
 
195
  print(f"P-S time difference: {s_wave_arrival - p_wave_arrival} seconds")
196
  # fig = fig
197
  """
198
+ def execute_user_code(code_string, source_lab):
199
  string_io = io.StringIO()
200
+ status = "✅ 成功"
201
+ error_info = ""
202
  try:
203
  with contextlib.redirect_stdout(string_io):
204
  local_scope = {}
 
207
  console_output = string_io.getvalue()
208
  fig = local_scope.get('fig')
209
  if fig is None:
210
+ status = "⚠️ 警告"
211
+ error_info = "程式碼執行完畢,但未找到 'fig' 物件。"
212
+ return None, f"{error_info}\nPrint 輸出:\n{console_output}"
213
+
214
+ success_message = f"✅ 程式碼執行成功!\n\n--- Console Output ---\n{console_output}"
215
+ return fig, success_message
216
  except Exception:
217
+ status = "❌ 失敗"
218
+ error_info = traceback.format_exc()
219
+ final_message = f"❌ 程式碼執行失敗!\n\n--- Error Traceback ---\n{error_info}"
220
+ return None, final_message
221
+ finally:
222
+ tz = pytz.timezone('Asia/Taipei')
223
+ current_time = datetime.now(tz).strftime('%H:%M:%S')
224
+ visit_count = get_current_visit_count()
225
+
226
+ notification_text = (
227
+ f"🔬 程式碼實驗室互動!\n\n"
228
+ f"時間: {current_time}\n"
229
+ f"實驗室: {source_lab}\n"
230
+ f"執行狀態: {status}\n"
231
+ f"總載入數: {visit_count}"
232
+ )
233
+ if status == "❌ 失敗":
234
+ error_type = error_info.strip().split('\n')[-1]
235
+ notification_text += f"\n錯誤類型: {error_type}"
236
+
237
+ send_line_notification(notification_text)
238
 
239
  # --- AI 助教知識庫 ---
240
  KNOWLEDGE_BASE = [
241
+ {'keywords': ['你好', '哈囉', 'hello', 'hi'], 'answer': f"你好!我是課程 AI 助教,很高興為您服務。今天是 {pd.Timestamp.now(tz='Asia/Taipei').strftime('%Y年%m月%d日')},有什麼可以協助您的嗎?"},
242
+ {'keywords': ['你是誰', '功能', '幹嘛', 'who are you'], 'answer': "我是本課程的 AI 助教,我的核心任務是根據內建的知識庫,回答您關於課程的各種問題,包含**地球物理概念**、**程式工具**與**課程安排**。我還可以在「互動體驗區」協助您執行與除錯程式碼!"},
243
+ {'keywords': ['成績', '評分', '分數', 'grading'], 'answer': "課程的評分標準設計如下:\n* **作業 (50%)**: 包含了所有程式碼練習、數據分析報告等。\n* **期中考 (40%)**: 範圍涵蓋前八週的地球物理知識與資料處理技能。\n* **平時成績 (10%)**: 根據您的課堂參與度、提問與互動表現進行評估。\n您可以在「成績計算」分頁看到完整的說明。"},
244
+ {'keywords': ['進度', '課綱', '課程表', 'schedule', 'syllabus', '第幾週'], 'answer': "完整的課程進度表都列在「課程進度」分頁中喔!您可以去那裡查看每一週的地球物理知識主題和資料處理技能目標。"},
245
+ {'keywords': ['期末', '專題', '報告', 'final project'], 'answer': "關於期末專題,我們預計在第十五週進行發表。這是一個很好的機會,讓您可以整合本學期所學的地球物理知識和數據處理技能,選擇一個您感興趣的主題進行深入研究與展示。"},
246
+ {'keywords': ['考試', '期中考', 'midterm'], 'answer': "期中考安排在第八週。考試範圍將涵蓋前七週所有教過的地球物理概念(如折射探勘、重力探勘)和資料處理技能(如 PyGMT, ObsPy, GitHub 等)。請務必複習課程內容和實作作業!"},
247
+ {'keywords': ['地球物理', 'geophysics', '學什麼'], 'answer': "地球物理學是一門利用物理原理(如波動、重力、磁力、熱力)來研究地球的科學,範圍非常廣泛!主要可以分為:\n1. **固體地球物理學**: 研究地球內部,如地震學、重力學。\n2. **應用地球物理學**: 尋找石油、礦產等資源或應用於工程與環境探勘。\n這門課會帶您認識核心概念,並用程式實作來分析相關數據!"},
248
+ {'keywords': ['折射', '震測'], 'answer': "折射震測是透過人為產生震波,並分析震波在地下不同速度地層間的折射路徑與時間,來反推地下構造的方法。我們在第三週會有校園野外實驗,讓大家親手操作!"},
249
+ {'keywords': ['重力', 'gravimetry'], 'answer': "重力探勘是透過精密儀器測量地表重力值的微小差異,來推斷地下物質的密度分佈。例如,高密度的礦床會產生正重力異常。"},
250
+ {'keywords': ['板塊', 'tectonics'], 'answer': "板塊構造學說描述了地球的岩石圈是由數個巨大板塊組成,板塊的移動、碰撞或分離造成了地震、火山和造山運動。這是現代地球科學的基石。"},
251
+ {'keywords': ['pygmt', 'gmt', 'cartopy', '地圖', 'map'], 'answer': "`PyGMT` 和 `Cartopy` 都是 Python 中非常強大的地理資訊繪圖函式庫。\n* **PyGMT**: GMT 的 Python 介面,指令簡潔,功能專業,適合發表高品質的學術圖件。\n* **Cartopy**: 與 `matplotlib` 整合度極高,可以輕鬆加入地圖投影、海岸線等特徵。「地圖繪製實驗室」使用的就是 `Cartopy`!"},
252
+ {'keywords': ['obspy'], 'answer': "ObsPy 是一個專為地震學設計的開源 Python 函式庫,能方便地讀寫各種地震波形格式、進行訊號處理(如濾波)、儀器響應校正等,是地��學家的瑞士刀。"},
253
+ {'keywords': ['gradio', 'streamlit'], 'answer': "Gradio 和 Streamlit 都是能讓我們用純 Python 快速建立互動式網頁應用的工具!\n* **Gradio**: 特別適合為函式或機器學習模型打造 demo 介面,就像這個課程網頁。\n* **Streamlit**: 則常用於建立數據分析的儀表板 (Dashboard)。"},
254
+ {'keywords': ['hugging face', 'hf', 'huggingface'], 'answer': "Hugging Face 是一個以 AI 為核心的社群與平台。它最棒的功能之一 **Spaces** 提供免費資源,讓我們可以輕鬆部署 Gradio 或 Streamlit 應用,分享給全世界!這個課程網頁就是部署在 Hugging Face Spaces 上。"},
255
+ {'keywords': ['gemini', 'dify'], 'answer': "Gemini 是 Google 開發的強大大型語言模型 (LLM)。Dify 則是一個 Low-code AI 應用開發平台,可以讓我們快速串接像 Gemini 這樣的語言模型,打造出有記憶能力的聊天機器人或 AI 應用。"},
256
+ {'keywords': ['謝謝', '感謝', 'thank'], 'answer': "不客氣!能幫助到您我也很開心。若還有其他問題,隨時都可以再問我喔!"},
257
+ {'keywords': ['笑話', '好玩', '有趣'], 'answer': "好的,給你說個地質學家的冷笑話:\n\n為什麼地質學家從來不賭博?\n\n...因為他們知道什麼叫做「斷層」(Fault)! 😄"},
258
+ {'keywords': ['安裝', '環境', 'anaconda', 'miniconda', 'setup', 'environment'], 'answer': "對於初學者,強烈推薦使用 **Anaconda** 或 **Miniconda** 來建置 Python 環境。\n* **Anaconda**: 包含了 Python、數百個常用科學計算套件以及 `conda` 管理器。\n* **Miniconda**: 僅包含 Python 和 `conda`,更輕量。\n使用 `conda` 可以輕鬆創建獨立的虛擬環境,避免套件版本衝突。"},
259
+ {'keywords': ['colab', 'codespaces', '比較', '差別', 'difference'], 'answer': "`Colab` 和 `GitHub Codespaces` 都是優秀的雲端開發環境:\n\n| 特性 | Google Colab | GitHub Codespaces |\n| :--- | :--- | :--- |\n| **強項** | 數據分析、機器學習 | 完整的軟體開發 |\n| **免費額度** | 提供免費 GPU/TPU | 每月提供免費核心時數 |\n| **環境** | Jupyter Notebook | 完整的 VS Code 編輯器 |\n| **整合性** | Google Drive | GitHub Repo |\n| **適用情境** | 執行分析腳本、跑模型 | 開發完整專案、網頁應用 |"},
260
+ {'keywords': ['git', 'github', '版本控制', '版控', 'version control', 'commit'], 'answer': "`Git` 是一個**版本控制系統**,`GitHub` 是一個**托管 Git 專案的平台**。\n使用版本控制有三大好處:\n1. **紀錄歷程**: 可隨時回溯到任何過去的版本。\n2. **團隊協作**: 多人可同時修改專案並合併工作。\n3. **分支開發**: 可安全地實驗新功能而不影響主線。"},
261
+ {'keywords': ['api', '應用程式介面'], 'answer': "您可以將 **API (Application Programming Interface)** 想像成餐廳的**菜單**。\n您只需要看懂菜單(API 文件),向服務生(API 端點)下訂單(發送請求),服務生就會將做好的菜(數據或結果)送回給您,而您完全不需要知道廚房內部的運作細節。"},
262
+ {'keywords': ['視覺化', '可視化', 'visualization', '好圖', '圖表'], 'answer': "一張好的科學圖表應具備**清晰性**、**準確性**、**簡潔性**和**故事性**。\n記住,圖表的目的是有效傳達資訊,而不是展示花俏的繪圖技巧。"},
263
+ {'keywords': ['地磁', '古地磁', '磁場', 'geomagnetism', 'paleomagnetism'], 'answer': "地球本身就像一個巨大的磁鐵,擁有**地磁**。當火山熔岩冷卻時,其中的磁性礦物會將當時地球磁場的方向「鎖住」,這就是**古地磁**紀錄。海底擴張帶兩側對稱的地磁異常條帶,是板塊構造學說最經典的證據之一。"},
264
+ {'keywords': ['地熱', '能源', 'geothermal'], 'answer': "地熱是來自地球內部的可再生能源。我們主要透過鑽井到地下的熱儲層,利用高溫蒸汽或熱水來發電。地熱發電的優點是**穩定**且**碳排放極低**,且通常集中在板塊邊界,台灣正好就位在這個絕佳的地理位置上!"},
265
+ {'keywords': ['地震儀', '地震學', 'seismograph', 'seismology', '差別'], 'answer': "**地震學 (Seismology)** 是一門**科學**,研究地震與地球內部構造。\n**地震儀 (Seismograph)** 是一種**儀器**,是地震學家使用的工具,用來記錄地面振動。\n簡單來說:**地震學家使用地震儀來進行地震學研究。**"},
266
+ {'keywords': ['野外', 'field work', '實驗', '準備', '穿什麼'], 'answer': "對於第三週的校園折射震測實驗,建議準備如下:\n* **服裝**: 穿著輕便、適合活動的長褲與不怕髒的鞋子。\n* **防曬/防雨**: 根據天氣預報,準備好帽子、防曬乳或雨具。\n* **其他**: 務必攜帶足夠的水和筆記本。"},
267
+ {'keywords': ['專題', '題目', '想法', 'topic', 'idea'], 'answer': "這裡提供幾個期末專題的靈感方向:\n1. **特定區域地震活動分析**: 使用 `ObsPy` 下載台灣特定區域的地震資料,分析其時空分佈並用 `PyGMT` 或 `Cartopy` 繪圖。\n2. **重力或磁力異常圖的解譯**: 尋找公開的重磁資料,繪製異常圖並嘗試解讀其對應的地質構造。\n3. **AI 應用於地球物理**: 訓練一個簡單的機器學習模型,用來自動區分 P 波和 S 波的到達時間。\n4. **互動式地球物理教學工具**: 使用 `Gradio` 或 `Streamlit` 製作一個互動小工具,用來展示特定概念。"}
268
  ]
269
 
270
  def ai_chatbot_with_kb(message, history):
271
+ # 發送 LINE 通知
272
+ tz = pytz.timezone('Asia/Taipei')
273
+ current_time = datetime.now(tz).strftime('%H:%M:%S')
274
+ visit_count = get_current_visit_count()
275
+ notification_text = (
276
+ f"🤖 AI 助教被提問!\n\n"
277
+ f"時間: {current_time}\n"
278
+ f"使用者問題:\n「{message}」\n\n"
279
+ f"總載入數: {visit_count}"
280
+ )
281
+ send_line_notification(notification_text)
282
+
283
+ # 正常執行知識庫查詢
284
  user_message = message.lower()
285
  for item in KNOWLEDGE_BASE:
286
  for keyword in item['keywords']:
 
334
  description="由結構化知識庫驅動的問答機器人"
335
  )
336
 
337
+ # --- 連接按鈕與後端執行函式 ---
338
+ map_run_button.click(
339
+ fn=lambda code: execute_user_code(code, "地圖繪製"),
340
+ inputs=[map_code],
341
+ outputs=[map_plot_output, map_console_output]
342
+ )
343
+ seismo_run_button.click(
344
+ fn=lambda code: execute_user_code(code, "震波圖"),
345
+ inputs=[seismo_code],
346
+ outputs=[seismo_plot_output, seismo_console_output]
347
+ )
348
 
349
  # --- 5. 啟動應用程式 ---
350
  demo.launch()