Spaces:
Running
Running
Upload 8 files
Browse files- app.py +24 -0
- config/content.py +73 -0
- config/data.py +39 -0
- config/defaults.py +55 -0
- core/callbacks.py +86 -0
- core/notifications.py +39 -0
- core/visits.py +30 -0
- ui/layouts.py +81 -0
app.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from core.visits import get_and_update_visits
|
| 4 |
+
from ui.layouts import create_ui
|
| 5 |
+
|
| 6 |
+
# --- 1. Update site visit count on startup ---
|
| 7 |
+
# This part runs only once when the application starts.
|
| 8 |
+
try:
|
| 9 |
+
count = get_and_update_visits()
|
| 10 |
+
visit_count_html = f"🚀 **總載入次數:** {count}"
|
| 11 |
+
print(f"Application loaded. Total visits: {count}")
|
| 12 |
+
except Exception as e:
|
| 13 |
+
visit_count_html = "🚀 **總載入次數:** N/A"
|
| 14 |
+
print(f"Could not update visit count: {e}")
|
| 15 |
+
|
| 16 |
+
# --- 2. Create the main UI ---
|
| 17 |
+
# We insert the visit count HTML into the UI using a simple trick
|
| 18 |
+
# by adding a Markdown component at the very top.
|
| 19 |
+
demo = create_ui()
|
| 20 |
+
demo.blocks.insert(0, gr.Markdown(visit_count_html)) # Insert the count at the top
|
| 21 |
+
|
| 22 |
+
# --- 3. Launch the application ---
|
| 23 |
+
if __name__ == "__main__":
|
| 24 |
+
demo.launch()
|
config/content.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# config/content.py
|
| 2 |
+
|
| 3 |
+
course_introduction_md = """
|
| 4 |
+
# 🌍 探索地球的脈動:用程式與AI解碼地球奧秘
|
| 5 |
+
|
| 6 |
+
### 引言:讓數據為地球發聲
|
| 7 |
+
|
| 8 |
+
地球的語言,是用數據寫成的詩篇——隱藏在地震波的起伏、重力場的微擾與磁場的變動之中。
|
| 9 |
+
|
| 10 |
+
要讀懂這首詩,我們需要成為同時精通 **地球科學** 與 **數據科學** 的雙語人才。本課程的唯一目標,就是賦予您這樣的 **雙核心能力**。
|
| 11 |
+
|
| 12 |
+
我們的學習路徑,建立在相輔相成的兩大支柱之上:
|
| 13 |
+
|
| 14 |
+
- **🧠 科學思維的建立 (理論)**:我們將系統性地學習地球物理學的核心理論,從 **折射/重力探勘** 的原理,到 **板塊構造** 的宏觀視野,並透過野外實習,讓您了解真實世界的數據從何而來。這將是您提出深刻科學問題的基礎。
|
| 15 |
+
|
| 16 |
+
- **💻 技術工具的打磨 (實務)**:我們將帶您從零開始,親手打造一套現代化的數據分析工具箱。您將學會使用 `Python` 與 `Git`,利用 **`PyGMT`** 與 **`ObsPy`** 將複雜數據轉為專業圖表,並透過 **`Gradio`** 將您的成果打造成互動網頁應用,分享給全世界。
|
| 17 |
+
|
| 18 |
+
在這趟旅程中,您更將學會運用 **`Gemini`** 等 AI 工具,為您的應用注入智慧,讓機器幫助我們更深入地解碼數據中的奧秘。
|
| 19 |
+
|
| 20 |
+
> 這不只是一門課,而是一場成為 **新世代地球科學家** 的完整訓練。歡迎您加入我們,一起探索地球的脈動!
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
course_goals_md = """
|
| 24 |
+
### 課程目標:從學習者到實踐者,打造你的「地球科學 x 數據科學」雙核心能力
|
| 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 |
+
- **`Python` 與 `Git/GitHub`**: 您將能熟練運用於科學計算與版本控制,實現高效的團隊協作。
|
| 50 |
+
- **雲端開發環境**: 您將能在 `Colab` 與 `GitHub Codespaces` 等環境中無縫切換,隨時隨地進行開發。
|
| 51 |
+
|
| 52 |
+
#### 🔹 實現專業的科學數據可視化
|
| 53 |
+
- **`PyGMT`**: 您將能精通繪製 **出版品質** 的專業地理圖件,將空間數據化為引人入勝的故事。
|
| 54 |
+
- **`ObsPy`**: 您將能嫻熟地從原始地震波形中 **濾波、分析**,並提取出關鍵訊息。
|
| 55 |
+
|
| 56 |
+
#### 🔹 打造吸睛的互動式網頁應用
|
| 57 |
+
- **`Gradio` 與 `Streamlit`**: 您將能快速將分析成果封裝成 **Web App**,讓任何人都能透過瀏覽器操作。
|
| 58 |
+
- **`Hugging Face Spaces`**: 您將學會將作品 **免費部署** 於雲端,建立屬於您自己的線上作品集。
|
| 59 |
+
|
| 60 |
+
#### 🔹 賦能 AI,讓應用更智慧
|
| 61 |
+
- **`Gemini API`**: 您將學會串接強大的大型語言模型,讓您的程式具備自然語言理解與生成能力。
|
| 62 |
+
- **`Dify`**: 您將能透過平台快速建構出能回答複雜問題、甚至能操作工具的 AI 智能體。
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
> **🚀 總結:本課程的目標是讓您成為一位不僅懂地球物理的科學家,更是一位能用數據解決問題的工程師。您將帶著走的,是一套能夠自主學習、獨立完成專案的跨領域核心能力。**
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
grading_policy_md = """
|
| 69 |
+
### 成績計算方式
|
| 70 |
+
* **作業 (50%)**: 包含課程中指派的各項實作任務,例如程式碼練習、數據分析報告、Colab筆記本繳交、以及GitHub Repo的更新紀錄等。這是評量您動手實踐能力的主要依據。
|
| 71 |
+
* **期中考 (40%)**: 範圍涵蓋前八週的地球物理知識與資料處理技能,用以檢核您對核心概念的理解程度。
|
| 72 |
+
* **平時成績 (10%)**: 根據您的課堂參與度、提問與互動表現進行評估。積極參與是學習的催化劑!
|
| 73 |
+
"""
|
config/data.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# config/data.py
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
schedule_data = {
|
| 5 |
+
"週次": ["第一週 (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)", "第十六週", "十七週", "第十八週"],
|
| 6 |
+
"地球物理知識": ["地球物理概要", "折射探勘學", "校園野外實驗 (折射探勘)", "地球物理專題演講", "折射探勘學", "重力探勘學", "重力探勘學", "期中考", "板塊構造與地震", "板塊構造與地震", "板塊構造與地磁", "板塊構造與地磁", "板塊構造與地熱", "板塊構造與地熱", "期末專題發表", "行憲紀念日 (放假)", "元旦 (放假)", "參訪大屯火山觀測中心 (暫定)"],
|
| 7 |
+
"資料處理技能": ["安裝Python, 申請各類帳號, 使用Colab/Codespace/Dify", "PyGMT, ObsPy, Streamlit, Gradio 介紹", "-", "-", "Hugging Face Space 製作互動程式", "Colab 使用", "GitHub 與 Codespace 使用", "-", "PyGMT", "PyGMT", "ObsPy", "ObsPy", "Gemini API 申請, Dify", "Dify", "-", "-", "-", "-"]
|
| 8 |
+
}
|
| 9 |
+
schedule_df = pd.DataFrame(schedule_data)
|
| 10 |
+
|
| 11 |
+
KNOWLEDGE_BASE = [
|
| 12 |
+
{'keywords': ['你好', '哈囉', 'hello', 'hi'], 'answer': f"你好!我是課程 AI 助教,很高興為您服務。今天是 {pd.Timestamp.now(tz='Asia/Taipei').strftime('%Y年%m月%d日')},有什麼可以協助您的嗎?"},
|
| 13 |
+
{'keywords': ['你是誰', '功能', '幹嘛', 'who are you'], 'answer': "我是本課程的 AI 助教,我的核心任務是根據內建的知識庫,回答您關於課程的各種問題,包含**地球物理概念**、**程式工具**與**課程安排**。我還可以在「互動體驗區」協助您執行與除錯程式碼!"},
|
| 14 |
+
{'keywords': ['成績', '評分', '分數', 'grading'], 'answer': "課程的評分標準設計如下:\n* **作業 (50%)**: 包含了所有程式碼練習、數據分析報告等。\n* **期中考 (40%)**: 範圍涵蓋前八週的地球物理知識與資料處理技能。\n* **平時成績 (10%)**: 根據您的課堂參與度、提問與互動表現進行評估。\n您可以在「成績計算」分頁看到完整的說明。"},
|
| 15 |
+
{'keywords': ['進度', '課綱', '課程表', 'schedule', 'syllabus', '第幾週'], 'answer': "完整的課程進度表都列在「課程進度」分頁中喔!您可以去那裡查看每一週的地球物理知識主題和資料處理技能目標。"},
|
| 16 |
+
{'keywords': ['期末', '專題', '報告', 'final project'], 'answer': "關於期末專題,我們預計在第十五週進行發表。這是一個很好的機會,讓您可以整合本學期所學的地球物理知識和數據處理技能,選擇一個您感興趣的主題進行深入研究與展示。"},
|
| 17 |
+
{'keywords': ['考試', '期中考', 'midterm'], 'answer': "期中考安排在第八週。考試範圍將涵蓋前七週所有教過的地球物理概念(如折射探勘、重力探勘)和資料處理技能(如 PyGMT, ObsPy, GitHub 等)。請務必複習課程內容和實作作業!"},
|
| 18 |
+
{'keywords': ['地球物理', 'geophysics', '學什麼'], 'answer': "地球物理學是一門利用物理原理(如波動、重力、磁力、熱力)來研究地球的科學,範圍非常廣泛!主要可以分為:\n1. **固體地球物理學**: 研究地球內部,如地震學、重力學。\n2. **應用地球物理學**: 尋找石油、礦產等資源或應用於工程與環境探勘。\n這門課會帶您認識核心概念,並用程式實作來分析相關數據!"},
|
| 19 |
+
{'keywords': ['折射', '震測'], 'answer': "折射震測是透過人為產生震波,並分析震波在地下不同速度地層間的折射路徑與時間,來反推地下構造的方法。我們在第三週會有校園野外實驗,讓大家親手操作!"},
|
| 20 |
+
{'keywords': ['重力', 'gravimetry'], 'answer': "重力探勘是透過精密儀器測量地表重力值的微小差異,來推斷地下物質的密度分佈。例如,高密度的礦床會產生正重力異常。"},
|
| 21 |
+
{'keywords': ['板塊', 'tectonics'], 'answer': "板塊構造學說描述了地球的岩石圈是由數個巨大板塊組成,板塊的移動、碰撞或分離造成了地震、火山和造山運動。這是現代地球科學的基石。"},
|
| 22 |
+
{'keywords': ['pygmt', 'gmt', 'cartopy', '地圖', 'map'], 'answer': "`PyGMT` 和 `Cartopy` 都是 Python 中非常強大的地理資訊繪圖函式庫。\n* **PyGMT**: GMT 的 Python 介面,指令簡潔,功能專業,適合發表高品質的學術圖件。\n* **Cartopy**: 與 `matplotlib` 整合度極高,可以輕鬆加入地圖投影、海岸線等特徵。「地圖繪製實驗室」使用的就是 `Cartopy`!"},
|
| 23 |
+
{'keywords': ['obspy'], 'answer': "ObsPy 是一個專為地震學設計的開源 Python 函式庫,能方便地讀寫各種地震波形格式、進行訊號處理(如濾波)、儀器響應校正等���是地震學家的瑞士刀。"},
|
| 24 |
+
{'keywords': ['gradio', 'streamlit'], 'answer': "Gradio 和 Streamlit 都是能讓我們用純 Python 快速建立互動式網頁應用的工具!\n* **Gradio**: 特別適合為函式或機器學習模型打造 demo 介面,就像這個課程網頁。\n* **Streamlit**: 則常用於建立數據分析的儀表板 (Dashboard)。"},
|
| 25 |
+
{'keywords': ['hugging face', 'hf', 'huggingface'], 'answer': "Hugging Face 是一個以 AI 為核心的社群與平台。它最棒的功能之一 **Spaces** 提供免費資源,讓我們可以輕鬆部署 Gradio 或 Streamlit 應用,分享給全世界!這個課程網頁就是部署在 Hugging Face Spaces 上。"},
|
| 26 |
+
{'keywords': ['gemini', 'dify'], 'answer': "Gemini 是 Google 開發的強大大型語言模型 (LLM)。Dify 則是一個 Low-code AI 應用開發平台,可以讓我們快速串接像 Gemini 這樣的語言模型,打造出有記憶能力的聊天機器人或 AI 應用。"},
|
| 27 |
+
{'keywords': ['謝謝', '感謝', 'thank'], 'answer': "不客氣!能幫助到您我也很開心。若還有其他問題,隨時都可以再問我喔!"},
|
| 28 |
+
{'keywords': ['笑話', '好玩', '有趣'], 'answer': "好的,給你說個地質學家的冷笑話:\n\n為什麼地質學家從來不賭博?\n\n...因為他們知道什麼叫做「斷層」(Fault)! 😄"},
|
| 29 |
+
{'keywords': ['安裝', '環境', 'anaconda', 'miniconda', 'setup', 'environment'], 'answer': "對於初學者,強烈推薦使用 **Anaconda** 或 **Miniconda** 來建置 Python 環境。\n* **Anaconda**: 包含了 Python、數百個常用科學計算套件以及 `conda` 管理器。\n* **Miniconda**: 僅包含 Python 和 `conda`,更輕量。\n使用 `conda` 可以輕鬆創建獨立的虛擬環境,避免套件版本衝突。"},
|
| 30 |
+
{'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| **適用情境** | 執行分析腳本、跑模型 | 開發完整專案、網頁應用 |"},
|
| 31 |
+
{'keywords': ['git', 'github', '版本控制', '版控', 'version control', 'commit'], 'answer': "`Git` 是一個**版本控制系統**,`GitHub` 是一個**托管 Git 專案的平台**。\n使用版本控制有三大好處:\n1. **紀錄歷程**: 可隨時回溯到任何過去的版本。\n2. **團隊協作**: 多人可同時修改專案並合併工作。\n3. **分支開發**: 可安全地實驗新功能而不影響主線。"},
|
| 32 |
+
{'keywords': ['api', '應用程式介面'], 'answer': "您可以將 **API (Application Programming Interface)** 想像成餐廳的**菜單**。\n您只需要看懂菜單(API 文件),向服務生(API 端點)下訂單(發送請求),服務生就會將做好的菜(數據或結果)送回給您,而您完全不需要知道廚房內部的運作細節。"},
|
| 33 |
+
{'keywords': ['視覺化', '可視化', 'visualization', '好圖', '圖表'], 'answer': "一張好的科學圖表應具備**清晰性**、**準確性**、**簡潔性**和**故事性**。\n記住,圖表的目的是有效傳達資訊,而不是展示花俏的繪圖技巧。"},
|
| 34 |
+
{'keywords': ['地磁', '古地磁', '磁場', 'geomagnetism', 'paleomagnetism'], 'answer': "地球本身就像一個巨大的磁鐵,擁有**地磁**。當火山熔岩冷卻時,其中的磁性礦物會將當時地球磁場的方向「鎖住」,這就是**古地磁**紀錄。海底擴張帶兩側對稱的地磁異常條帶,是板塊構造學說最經典的證據之一。"},
|
| 35 |
+
{'keywords': ['地熱', '能源', 'geothermal'], 'answer': "地熱是來自地球內部的可再生能源。我們主要透過鑽井到地下的熱儲層,利用高溫蒸汽或熱水來發電。地熱發電的優點是**穩定**且**碳排放極低**,且通常集中在板塊邊界,台灣正好就位在這個絕佳的地理位置上!"},
|
| 36 |
+
{'keywords': ['地震儀', '地震學', 'seismograph', 'seismology', '差別'], 'answer': "**地震學 (Seismology)** 是一門**科學**,研究地震與地球內部構造。\n**地震儀 (Seismograph)** 是一種**儀器**,是地震學家使用的工具,用來記錄地面振動。\n簡單來說:**地震學家使用地震儀來進行地震學研究。**"},
|
| 37 |
+
{'keywords': ['野外', 'field work', '實驗', '準備', '穿什麼'], 'answer': "對於第三週的校園折射震測實驗,建議準備如下:\n* **服裝**: 穿著輕便、適合活動的長褲與不怕髒的鞋子。\n* **防曬/防雨**: 根據天氣預報,準備好帽子、防曬乳或雨具。\n* **其他**: 務必攜帶足夠的水和筆記本。"},
|
| 38 |
+
{'keywords': ['專題', '題目', '想法', 'topic', 'idea'], 'answer': "這裡提供幾個期末專題的靈感方���:\n1. **特定區域地震活動分析**: 使用 `ObsPy` 下載台灣特定區域的地震資料,分析其時空分佈並用 `PyGMT` 或 `Cartopy` 繪圖。\n2. **重力或磁力異常圖的解譯**: 尋找公開的重磁資料,繪製異常圖並嘗試解讀其對應的地質構造。\n3. **AI 應用於地球物理**: 訓練一個簡單的機器學習模型,用來自動區分 P 波和 S 波的到達時間。\n4. **互動式地球物理教學工具**: 使用 `Gradio` 或 `Streamlit` 製作一個互動小工具,用來展示特定概念。"}
|
| 39 |
+
]
|
config/defaults.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# config/defaults.py
|
| 2 |
+
|
| 3 |
+
DEFAULT_MAP_CODE = """
|
| 4 |
+
# === 歡迎來到地圖繪製實驗室 ===
|
| 5 |
+
# 試著修改下方的參數,然後點擊「執行程式碼」看看會發生什麼事!
|
| 6 |
+
# --- 地圖參數設定 ---
|
| 7 |
+
center_lon, center_lat = 135, 35
|
| 8 |
+
extent_lon, extent_lat = 30, 25
|
| 9 |
+
map_title = "Map with Custom Symbols"
|
| 10 |
+
coastline_color = 'darkgreen'
|
| 11 |
+
# --- 符號參數設定 (在這裡新增或修改你的標記點!) ---
|
| 12 |
+
symbols = [
|
| 13 |
+
{'lon': 121.52, 'lat': 25.04, 'style': 'r*', 'label': 'Taipei 101'},
|
| 14 |
+
{'lon': 139.69, 'lat': 35.68, 'style': 'bs', 'label': 'Tokyo'},
|
| 15 |
+
{'lon': 126.97, 'lat': 37.56, 'style': 'go', 'label': 'Seoul'},
|
| 16 |
+
]
|
| 17 |
+
# --- 核心繪圖區 ---
|
| 18 |
+
import matplotlib.pyplot as plt
|
| 19 |
+
import cartopy.crs as ccrs
|
| 20 |
+
import cartopy.feature as cfeature
|
| 21 |
+
fig = plt.figure(figsize=(8, 8))
|
| 22 |
+
projection = ccrs.Mercator(central_longitude=center_lon)
|
| 23 |
+
ax = fig.add_subplot(1, 1, 1, projection=projection)
|
| 24 |
+
ax.set_extent([center_lon - extent_lon / 2, center_lon + extent_lon / 2, center_lat - extent_lat / 2, center_lat + extent_lat / 2], crs=ccrs.PlateCarree())
|
| 25 |
+
ax.add_feature(cfeature.LAND, edgecolor='black', facecolor='#c5a582')
|
| 26 |
+
ax.add_feature(cfeature.OCEAN, facecolor='#a2d1f5')
|
| 27 |
+
ax.add_feature(cfeature.COASTLINE.with_scale('50m'), edgecolor=coastline_color)
|
| 28 |
+
ax.add_feature(cfeature.BORDERS, linestyle=':')
|
| 29 |
+
for symbol in symbols:
|
| 30 |
+
ax.plot(symbol['lon'], symbol['lat'], symbol['style'], markersize=12, markeredgecolor='white', transform=ccrs.PlateCarree(), label=symbol['label'])
|
| 31 |
+
gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=0.7, color='gray', alpha=0.5, linestyle='--')
|
| 32 |
+
gl.top_labels = False
|
| 33 |
+
gl.right_labels = False
|
| 34 |
+
ax.legend()
|
| 35 |
+
ax.set_title(map_title)
|
| 36 |
+
print(f"成功繪製了 {len(symbols)} 個符號!")
|
| 37 |
+
# fig = fig
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
DEFAULT_SEISMO_CODE = """
|
| 41 |
+
# === 歡迎來到震波圖繪製實驗室 ===
|
| 42 |
+
# --- 參數設定區 ---
|
| 43 |
+
p_wave_arrival = 10; s_wave_arrival = 25; main_freq = 2.0; decay_rate = 0.04; plot_title = "Simulated P and S Wave Arrival"
|
| 44 |
+
# --- 核心繪圖區 ---
|
| 45 |
+
import matplotlib.pyplot as plt; import numpy as np
|
| 46 |
+
t = np.linspace(0, 100, 1000); amp = np.zeros_like(t)
|
| 47 |
+
p_wave_mask = t > p_wave_arrival; p_wave = 1.0 * np.exp(-decay_rate * (t - p_wave_arrival)) * np.sin(2 * np.pi * main_freq * (t - p_wave_arrival)); amp += p_wave * p_wave_mask
|
| 48 |
+
s_wave_mask = t > s_wave_arrival; s_wave = 2.5 * np.exp(-decay_rate * (t - s_wave_arrival)) * np.sin(2 * np.pi * (main_freq * 0.6) * (t - s_wave_arrival)); amp += s_wave * s_wave_mask
|
| 49 |
+
amp += 0.1 * np.random.randn(len(t)); fig, ax = plt.subplots(figsize=(8, 4)); ax.plot(t, amp, 'k-')
|
| 50 |
+
ax.axvline(p_wave_arrival, color='r', linestyle='--', label=f'P-wave at {p_wave_arrival}s')
|
| 51 |
+
ax.axvline(s_wave_arrival, color='b', linestyle='--', label=f'S-wave at {s_wave_arrival}s')
|
| 52 |
+
ax.set_title(plot_title); ax.set_xlabel("Time (seconds)"); ax.set_ylabel("Amplitude"); ax.grid(True); ax.legend(); plt.tight_layout()
|
| 53 |
+
print(f"P-S time difference: {s_wave_arrival - p_wave_arrival} seconds")
|
| 54 |
+
# fig = fig
|
| 55 |
+
"""
|
core/callbacks.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/callbacks.py
|
| 2 |
+
import io
|
| 3 |
+
import contextlib
|
| 4 |
+
import traceback
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import pytz
|
| 7 |
+
|
| 8 |
+
from core.visits import get_current_visit_count
|
| 9 |
+
from core.notifications import send_line_notification_in_background
|
| 10 |
+
from config.data import KNOWLEDGE_BASE
|
| 11 |
+
|
| 12 |
+
def execute_user_code(code_string, source_lab):
|
| 13 |
+
"""Executes user-provided code in a restricted environment and sends a notification."""
|
| 14 |
+
string_io = io.StringIO()
|
| 15 |
+
status = "✅ 成功"
|
| 16 |
+
error_info = ""
|
| 17 |
+
fig = None
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
with contextlib.redirect_stdout(string_io):
|
| 21 |
+
local_scope = {}
|
| 22 |
+
# Pre-import necessary libraries for the user's code
|
| 23 |
+
exec("import matplotlib.pyplot as plt; import numpy as np; import cartopy.crs as ccrs; import cartopy.feature as cfeature; from matplotlib.ticker import FixedFormatter", local_scope)
|
| 24 |
+
exec(code_string, local_scope)
|
| 25 |
+
|
| 26 |
+
console_output = string_io.getvalue()
|
| 27 |
+
fig = local_scope.get('fig')
|
| 28 |
+
|
| 29 |
+
if fig is None:
|
| 30 |
+
status = "⚠️ 警告"
|
| 31 |
+
error_info = "程式碼執行完畢,但未找到 'fig' 物件。"
|
| 32 |
+
return None, f"{error_info}\nPrint 輸出:\n{console_output}"
|
| 33 |
+
|
| 34 |
+
success_message = f"✅ 程式碼執行成功!\n\n--- Console Output ---\n{console_output}"
|
| 35 |
+
return fig, success_message
|
| 36 |
+
|
| 37 |
+
except Exception:
|
| 38 |
+
status = "❌ 失敗"
|
| 39 |
+
error_info = traceback.format_exc()
|
| 40 |
+
final_message = f"❌ 程式碼執行失敗!\n\n--- Error Traceback ---\n{error_info}"
|
| 41 |
+
return None, final_message
|
| 42 |
+
|
| 43 |
+
finally:
|
| 44 |
+
# This block will always run, regardless of success or failure
|
| 45 |
+
tz = pytz.timezone('Asia/Taipei')
|
| 46 |
+
current_time = datetime.now(tz).strftime('%H:%M:%S')
|
| 47 |
+
visit_count = get_current_visit_count()
|
| 48 |
+
|
| 49 |
+
notification_text = (
|
| 50 |
+
f"🔬 程式碼實驗室互動!\n\n"
|
| 51 |
+
f"時間: {current_time}\n"
|
| 52 |
+
f"實驗室: {source_lab}\n"
|
| 53 |
+
f"執行狀態: {status}\n"
|
| 54 |
+
f"總載入數: {visit_count}"
|
| 55 |
+
)
|
| 56 |
+
if status == "❌ 失敗":
|
| 57 |
+
# Add specific error type to notification for quick debugging
|
| 58 |
+
error_type = error_info.strip().split('\n')[-1]
|
| 59 |
+
notification_text += f"\n錯誤類型: {error_type}"
|
| 60 |
+
|
| 61 |
+
send_line_notification_in_background(notification_text)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def ai_chatbot_with_kb(message, history):
|
| 65 |
+
"""Handles chatbot interaction, queries the knowledge base, and sends a notification."""
|
| 66 |
+
# Send notification in the background
|
| 67 |
+
tz = pytz.timezone('Asia/Taipei')
|
| 68 |
+
current_time = datetime.now(tz).strftime('%H:%M:%S')
|
| 69 |
+
visit_count = get_current_visit_count()
|
| 70 |
+
notification_text = (
|
| 71 |
+
f"🤖 AI 助教被提問!\n\n"
|
| 72 |
+
f"時間: {current_time}\n"
|
| 73 |
+
f"使用者問題:\n「{message}」\n\n"
|
| 74 |
+
f"總載入數: {visit_count}"
|
| 75 |
+
)
|
| 76 |
+
send_line_notification_in_background(notification_text)
|
| 77 |
+
|
| 78 |
+
# Perform knowledge base lookup
|
| 79 |
+
user_message = message.lower().strip()
|
| 80 |
+
for item in KNOWLEDGE_BASE:
|
| 81 |
+
for keyword in item['keywords']:
|
| 82 |
+
if keyword in user_message:
|
| 83 |
+
return item['answer']
|
| 84 |
+
|
| 85 |
+
# Default response if no keyword is matched
|
| 86 |
+
return "這個問題很有趣,不過我的知識庫目前還沒有收錄相關的答案。您可以試著問我關於**課程評分、Anaconda安裝、Colab與Codespaces的差別、什麼是API,或者期末專題的靈感**等問題!"
|
core/notifications.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/notifications.py
|
| 2 |
+
import os
|
| 3 |
+
import threading
|
| 4 |
+
from linebot.v3.messaging import (
|
| 5 |
+
Configuration,
|
| 6 |
+
ApiClient,
|
| 7 |
+
MessagingApi,
|
| 8 |
+
PushMessageRequest,
|
| 9 |
+
TextMessage,
|
| 10 |
+
ApiException
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
def send_line_notification(message_text):
|
| 14 |
+
"""Sends a LINE notification to the specified user."""
|
| 15 |
+
access_token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')
|
| 16 |
+
user_id = os.environ.get('YOUR_LINE_USER_ID')
|
| 17 |
+
|
| 18 |
+
if not access_token or not user_id:
|
| 19 |
+
print("LINE Secrets not found. Skipping notification.")
|
| 20 |
+
return
|
| 21 |
+
|
| 22 |
+
configuration = Configuration(access_token=access_token)
|
| 23 |
+
try:
|
| 24 |
+
with ApiClient(configuration) as api_client:
|
| 25 |
+
line_bot_api = MessagingApi(api_client)
|
| 26 |
+
line_bot_api.push_message(
|
| 27 |
+
PushMessageRequest(to=user_id, messages=[TextMessage(text=message_text)])
|
| 28 |
+
)
|
| 29 |
+
print("LINE notification sent successfully in background!")
|
| 30 |
+
except ApiException as e:
|
| 31 |
+
print(f"Error sending LINE notification in background: {e.body}")
|
| 32 |
+
|
| 33 |
+
def send_line_notification_in_background(message_text):
|
| 34 |
+
"""Creates and starts a new thread to send a LINE notification, avoiding blocking the main app."""
|
| 35 |
+
notification_thread = threading.Thread(
|
| 36 |
+
target=send_line_notification,
|
| 37 |
+
args=(message_text,)
|
| 38 |
+
)
|
| 39 |
+
notification_thread.start()
|
core/visits.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/visits.py
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
COUNTER_FILE = Path("visits.txt")
|
| 5 |
+
|
| 6 |
+
def get_and_update_visits():
|
| 7 |
+
"""Reads the current visit count, increments it, writes it back, and returns the new count."""
|
| 8 |
+
if not COUNTER_FILE.exists():
|
| 9 |
+
count = 1
|
| 10 |
+
else:
|
| 11 |
+
try:
|
| 12 |
+
count = int(COUNTER_FILE.read_text()) + 1
|
| 13 |
+
except (ValueError, IOError):
|
| 14 |
+
count = 1 # Reset counter if file is corrupted
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
COUNTER_FILE.write_text(str(count))
|
| 18 |
+
except IOError as e:
|
| 19 |
+
print(f"Error writing to counter file: {e}")
|
| 20 |
+
|
| 21 |
+
return count
|
| 22 |
+
|
| 23 |
+
def get_current_visit_count():
|
| 24 |
+
"""Reads and returns the current visit count without incrementing it."""
|
| 25 |
+
if not COUNTER_FILE.exists():
|
| 26 |
+
return 0
|
| 27 |
+
try:
|
| 28 |
+
return int(COUNTER_FILE.read_text())
|
| 29 |
+
except (ValueError, IOError):
|
| 30 |
+
return 0 # Return 0 if file is corrupted
|
ui/layouts.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ui/layouts.py
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from config import content, data, defaults
|
| 4 |
+
from core import callbacks
|
| 5 |
+
|
| 6 |
+
def create_ui():
|
| 7 |
+
"""Creates and returns the Gradio UI Blocks."""
|
| 8 |
+
|
| 9 |
+
# --- Main UI Layout ---
|
| 10 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange"), title="地球物理學與AI應用") as demo:
|
| 11 |
+
# --- Introduction Section ---
|
| 12 |
+
gr.Markdown(content.course_introduction_md)
|
| 13 |
+
|
| 14 |
+
# --- Main Tabs ---
|
| 15 |
+
with gr.Tabs():
|
| 16 |
+
|
| 17 |
+
# --- Tab 1: Course Goals ---
|
| 18 |
+
with gr.TabItem("課程目標"):
|
| 19 |
+
gr.Markdown(content.course_goals_md)
|
| 20 |
+
|
| 21 |
+
# --- Tab 2: Course Schedule ---
|
| 22 |
+
with gr.TabItem("課程進度"):
|
| 23 |
+
gr.Markdown("### 每週課程安排")
|
| 24 |
+
gr.DataFrame(data.schedule_df, wrap=True)
|
| 25 |
+
|
| 26 |
+
# --- Tab 3: Grading Policy ---
|
| 27 |
+
with gr.TabItem("成績計算"):
|
| 28 |
+
gr.Markdown(content.grading_policy_md)
|
| 29 |
+
|
| 30 |
+
# --- Tab 4: Interactive Labs ---
|
| 31 |
+
with gr.TabItem("互動體驗區 (程式碼實驗室)"):
|
| 32 |
+
gr.Markdown("## 🚀 互動程式碼實驗室")
|
| 33 |
+
gr.Markdown("歡迎來到這裡!直接修改下方的 Python 程式碼,點擊「執行」,即可在右側看到成果。這是學習程式與地球物理最直接的方式!")
|
| 34 |
+
gr.Info("注意:執行環境已受限,僅支援資料視覺化相關操作。請勿嘗試檔案讀寫或網路請求。")
|
| 35 |
+
|
| 36 |
+
# --- Lab 1: Mapping ---
|
| 37 |
+
with gr.Accordion("🌍 地圖繪製實驗室 (PyGMT/Cartopy 概念)", open=True):
|
| 38 |
+
with gr.Row():
|
| 39 |
+
with gr.Column(scale=2):
|
| 40 |
+
gr.Markdown("### 說明\n這段程式碼使用 `cartopy` 和 `matplotlib` 函式庫來繪製地理地圖。\n\n**您可以試著:**\n1. 修改 `center_lon`, `center_lat` 來改變地圖中心。\n2. 調整 `extent_lon`, `extent_lat` 來縮放地圖。\n3. 將 `coastline_color` 改成 'red' 或其他顏色。\n4. **在 `symbols` 列表中新增或修改字典,來繪製自訂的符號(例如:標示您所在的城市)。**")
|
| 41 |
+
map_code = gr.Code(label="可編輯的 Python 程式碼", value=defaults.DEFAULT_MAP_CODE, language="python", lines=25)
|
| 42 |
+
map_run_button = gr.Button("執行程式碼", variant="primary")
|
| 43 |
+
with gr.Column(scale=3):
|
| 44 |
+
map_plot_output = gr.Plot(label="地圖輸出")
|
| 45 |
+
map_console_output = gr.Textbox(label="執行結果 / 錯誤訊息", lines=8, interactive=False)
|
| 46 |
+
|
| 47 |
+
# --- Lab 2: Seismology ---
|
| 48 |
+
with gr.Accordion("📈 震波圖繪製實驗室 (ObsPy 概念)", open=False):
|
| 49 |
+
with gr.Row():
|
| 50 |
+
with gr.Column(scale=2):
|
| 51 |
+
gr.Markdown("### 說明\n這段程式碼使用 `numpy` 產生模擬的地震波數據,並用 `matplotlib` 將其視覺化。\n\n**您可以試著:**\n1. 修改 `p_wave_arrival` 和 `s_wave_arrival` 來改變 P/S 波的抵達時間。\n2. 調整 `main_freq` 來改變地震波的頻率(數值越大,波形越密集)。\n3. 將 `decay_rate` 調小,觀察振幅衰減變慢的效果。")
|
| 52 |
+
seismo_code = gr.Code(label="可編輯的 Python 程式碼", value=defaults.DEFAULT_SEISMO_CODE, language="python", lines=25)
|
| 53 |
+
seismo_run_button = gr.Button("執行程式碼", variant="primary")
|
| 54 |
+
with gr.Column(scale=3):
|
| 55 |
+
seismo_plot_output = gr.Plot(label="震波圖輸出")
|
| 56 |
+
seismo_console_output = gr.Textbox(label="執行結果 / 錯誤訊息", lines=8, interactive=False)
|
| 57 |
+
|
| 58 |
+
# --- Tab 5: AI Chatbot ---
|
| 59 |
+
with gr.TabItem("🤖 AI 課程助教"):
|
| 60 |
+
gr.Markdown("### 🤖 AI 課程助教 (知識庫強化版)")
|
| 61 |
+
gr.Markdown("我內建了豐富的課程知識庫,試著問我 **「如何安裝Python環境?」**、**「什麼是版本控制?」** 或 **「給我一些期末專題的靈感」**")
|
| 62 |
+
gr.ChatInterface(
|
| 63 |
+
callbacks.ai_chatbot_with_kb,
|
| 64 |
+
chatbot=gr.Chatbot(height=450, type="messages", avatar_images=(None, "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png")),
|
| 65 |
+
title="課程AI助教",
|
| 66 |
+
description="由結構化知識庫驅動的問答機器人"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# --- Connect Buttons to Backend Functions ---
|
| 70 |
+
map_run_button.click(
|
| 71 |
+
fn=lambda code: callbacks.execute_user_code(code, "地圖繪製"),
|
| 72 |
+
inputs=[map_code],
|
| 73 |
+
outputs=[map_plot_output, map_console_output]
|
| 74 |
+
)
|
| 75 |
+
seismo_run_button.click(
|
| 76 |
+
fn=lambda code: callbacks.execute_user_code(code, "震波圖"),
|
| 77 |
+
inputs=[seismo_code],
|
| 78 |
+
outputs=[seismo_plot_output, seismo_console_output]
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
return demo
|