cwadayi commited on
Commit
e5ac43c
·
verified ·
1 Parent(s): c2414c2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +292 -154
app.py CHANGED
@@ -1,29 +1,157 @@
1
  import gradio as gr
2
  import random
3
- import textwrap # 用於處理程式碼字串的縮排
 
 
 
4
 
5
- # [新增] 匯入範例 App 的核心功能
6
- # 感謝步驟 1 & 2 的修改,我們現在可以安全地匯入它們
7
- try:
8
- from tutorial_basic import get_definition, geo_dict
9
- from tutorial_advanced import fetch_and_plot_events
10
- IMPORTS_SUCCESSFUL = True
11
- except ImportError as e:
12
- print(f"警告:無法匯入範例程式。{e}")
13
- IMPORTS_SUCCESSFUL = False
 
14
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- # 從外部 .py 檔案讀取教學程式碼 (用於教學分頁)
17
- def load_code_from_file(filename):
 
 
 
18
  try:
19
- with open(filename, 'r', encoding='utf-8') as f:
20
- return f.read()
21
- except FileNotFoundError:
22
- return f"# 錯誤:找不到範例檔案 {filename}\n# 請確認您已上傳此檔案到 Space。"
 
 
 
 
 
 
 
 
23
  except Exception as e:
24
- return f"# 讀取時發生錯誤:{e}"
25
 
26
- # 1. 專案資料 (帶有 Emoji)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  projects_data = {
28
  "🌊 地震與波形展示": [
29
  {"title": "📡 地震目錄搜尋 API", "url": "https://cwadayi-python-app.hf.space/"},
@@ -55,30 +183,23 @@ projects_data = {
55
  ]
56
  }
57
 
58
- # 2. 隨機推薦函式
59
  def recommend_project():
60
- """從所有專案中隨機挑選一個並回傳 Markdown 格式的推薦"""
61
  all_projects = [item for sublist in projects_data.values() for item in sublist]
62
  recommendation = random.choice(all_projects)
63
-
64
  return f"""
65
  ### {recommendation['title']}
66
-
67
  [**點此立即前往 🚀**]({recommendation['url']})
68
  """
69
 
70
- # 3. requirements.txt 範例內容
71
  TUTORIAL_REQUIREMENTS = textwrap.dedent("""
72
  gradio
73
  obspy
74
  folium
75
  """)
76
 
77
- # 4. 從外部檔案載入教學程式碼
78
- TUTORIAL_CODE_BASIC = load_code_from_file("tutorial_basic.py")
79
- TUTORIAL_CODE_ADVANCED = load_code_from_file("tutorial_advanced.py")
80
-
81
- # 5. 自訂 CSS 樣式
82
  css = """
83
  /* 標頭樣式 */
84
  #main-header {
@@ -178,14 +299,16 @@ css = """
178
  #footer { text-align: center; color: #777; font-size: 0.9rem; margin-top: 2rem; }
179
  """
180
 
181
- # 6. 使用 gr.Blocks() 建構介面
 
 
182
  with gr.Blocks(css=css, title="地球物理 x Hugging Face 專案展示") as demo:
183
 
184
  # 標頭區
185
  with gr.Row(elem_id="main-header"):
186
  gr.Markdown(
187
  """
188
- <img id="header-image" src="https://images.pexels.com/photos/220201/pexels-photo-2201.jpeg" alt="Abstract seismic waves banner">
189
  <h1>地球物理 x Hugging Face 專案展示</h1>
190
  <p>一個地球物理學家的數位百寶箱:探索一系列由 cwadayi 打造的地震與地科應用!</p>
191
  """
@@ -219,100 +342,140 @@ with gr.Blocks(css=css, title="地球物理 x Hugging Face 專案展示") as dem
219
  elem_classes="project-button"
220
  )
221
 
222
- # --- [新增] Tab 2: 範例 1 (地科字典) ---
223
  with gr.TabItem("📖 範例 1: 地科字典", id=1):
224
- gr.Markdown("# 🚀 範例 1: 地科字典 (Live Demo)")
225
- gr.Markdown("這是在「AI 創客空間」中教學的 Level 1 基礎範例的**即時互動版本**。")
226
- if IMPORTS_SUCCESSFUL:
227
- # 建立 Level 1 App 的介面
228
- dropdown = gr.Dropdown(
229
- label="請選擇一個地科名詞",
230
- choices=list(geo_dict.keys()) # 從匯入的 geo_dict 取得資料
231
- )
232
- output_textbox = gr.Textbox(
233
- label="名詞解釋",
234
- lines=5,
235
- interactive=False
236
- )
237
- dropdown.change(
238
- fn=get_definition, # 使用匯入的 get_definition 函式
239
- inputs=dropdown,
240
- outputs=output_textbox
241
- )
242
- else:
243
- gr.Warning("範例 1 載入失敗。請檢查 app.py 的匯入 (import) 設定。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
- # --- [新增] Tab 3: 範例 2 (地震地圖) ---
 
 
 
 
 
 
 
246
  with gr.TabItem("🌍 範例 2: 地震地圖", id=2):
247
- gr.Markdown("# 🚀 範例 2: 進階地震地圖 (Live Demo)")
248
- gr.Markdown("這是在「AI 創客空間」中教學的 Level 2 進階範例的**即時互動版本**。")
249
- if IMPORTS_SUCCESSFUL:
250
- # 建立 Level 2 App 的介面
251
- with gr.Row():
252
- mag_slider = gr.Slider(
253
- minimum=4.0,
254
- maximum=8.0,
255
- value=5.0,
256
- step=0.1,
257
- label="最小地震規模 (M)"
258
- )
259
- days_slider = gr.Slider(
260
- minimum=1,
261
- maximum=30,
262
- value=7,
263
- step=1,
264
- label="搜尋天數 (過去幾天)"
 
 
 
 
265
  )
266
- search_button = gr.Button("搜尋並繪製地圖 🌍", variant="primary")
267
- map_output = gr.HTML(label="地震分佈圖")
268
- search_button.click(
269
- fn=fetch_and_plot_events, # 使用匯入的 fetch_and_plot_events 函式
270
- inputs=[mag_slider, days_slider],
271
- outputs=map_output
272
- )
273
- else:
274
- gr.Warning("範例 2 載入失敗。請檢查 app.py 的匯入 (import) 設定,並確認 `obspy` 和 `folium` 已在 `requirements.txt` 中。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
  # --- Tab 4: AI 創客空間 (教學) ---
277
  with gr.TabItem("🎓 AI 創客空間", id=3):
278
- # (此分頁保持不變,繼續顯示教學文字和程式碼)
279
  gr.Markdown(
280
  """
281
  <div style="padding: 1rem 0;">
282
- <h2>🚀 打造你的地科 App!</h2>
283
  <p>這是一個教你如何「跟 AI (例如 Gemini) 協作」來快速打造並部署應用程式的SOP:</p>
 
284
  </div>
285
  """
286
  )
287
- with gr.Accordion("Level 1: 基礎字典 App 📖 (點我展開)", open=True):
288
- gr.Markdown(
289
- """
290
- <div style="padding: 1rem 0;">
291
- <h3>Step 1: 點子發想 🧠</h3>
292
- <p>先想一個簡單的點子。例如:「我想做一個查詢『地科名詞』的字典!」</p>
293
-
294
- <h3>Step 2: 詠唱 AI 咒語 🧙</h3>
295
- <p>打開你最愛的 AI 助手,對它說出清晰的指令 (Prompt),例如:</p>
296
- <blockquote>
297
- "請幫我用 Python 和 Gradio 寫一個簡單的應用。我需要一個下拉式選單,選項有「板塊構造」、「地震波」、「海嘯」。當我選擇一個選項時,下方要顯示對應的中文解釋。請給我完整的 `app.py` 程式碼。"
298
- </blockquote>
299
-
300
- <h3>Step 3: 取得程式碼 📜</h3>
301
- <p>AI 會給你一份完整的程式碼。下面這份就是 AI 可能會產生的答案,你可以直接複製它來進行下一步!</p>
302
- </div>
303
- """
304
- )
305
- gr.Code(
306
- label="app.py (Level 1 範例)",
307
- value=TUTORIAL_CODE_BASIC,
308
- language="python",
309
- interactive=False
310
- )
311
  gr.Markdown(
312
  """
313
- <div style="padding: 1rem 0;">
314
- <h3>Step 4: 部署到 Space 🚀</h3>
315
- <p>部署你自己的版本!</p>
316
  <ol>
317
  <li>到 Hugging Face 網站,點選你的頭像,選擇 <strong>"New Space"</strong>。</li>
318
  <li>為你的 Space 命名 (例如: <code>my-geo-dictionary</code>)。</li>
@@ -320,35 +483,25 @@ with gr.Blocks(css=css, title="地球物理 x Hugging Face 專案展示") as dem
320
  <li>點擊 "Create Space"。</li>
321
  <li>在你的 Space 頁面,點選 "Files and versions" -> "Add file" -> "Create new file"。</li>
322
  <li>將檔案命名為 <strong><code>app.py</code></strong> (必須是這個名字!)。</li>
323
- <li><strong>將上面 "Step 3" 的整段程式碼,完整貼到 `app.py` 的編輯器中。</strong></li>
324
  <li>點擊 "Commit new file" (提交檔案)。</li>
325
  </ol>
326
-
327
- <h3>Step 5: 恭喜你!🎉</h3>
328
- <p>等待幾分鐘,Hugging Face 會自動幫你把 App "建置" (Building) 起來。當你看到 "Running" 狀態時,恭喜你!你的第一個地科 App 已經上線了!</p>
329
- </div>
330
  """
331
  )
 
 
 
 
 
 
332
 
333
  with gr.Accordion("Level 2: 進階地震地圖 App 🌍 (點我展開)", open=False):
 
334
  gr.Markdown(
335
  """
336
- <div style="padding: 1rem 0;">
337
- <h3>Step 1 & 2: 進階點子與咒語 🧙</h3>
338
- <p>Level 1 太簡單了?來挑戰更專業的!</p>
339
- <p><strong>新點子:</strong>「我想做一個互動地圖,顯示全球最近的地震。使用者可以自訂最小規模和搜尋天數。」</p>
340
- <blockquote>
341
- <strong>AI 咒語範例:</strong> "請幫我用 Gradio 寫一個 App。我需要兩個滑桿 (Slider) 讓使用者輸入「最小規模」和「搜尋天數」。按下按鈕後,程式應使用 `obspy` 函式庫的 `Client` 搜尋 IRIS FDSN 的地震資料,然後使用 `folium` 函式庫將這些地震繪製在一個互動地圖上。地圖應顯示在 Gradio 介面中。"
342
- </blockquote>
343
-
344
- <h3>[極度重要] Step 3.5: 告訴 Space 你需要新工具 (<code>requirements.txt</code>)</h3>
345
- <p>因為這個 App 需要 `obspy` 和 `folium` 這兩個 Python 函式庫 (Gradio 預設沒有安裝),我們必須建立一個檔案來告訴 Hugging Face Space:</p>
346
- <ol>
347
- <li>在你的 Space 頁面,點選 "Files and versions" -> "Add file" -> "Create new file"。</li>
348
- <li>將檔案命名為 <strong><code>requirements.txt</code></strong> (必須是這個名字!)。</li>
349
- <li>將下面的三行文字貼到檔案中:</li>
350
- </ol>
351
- </div>
352
  """
353
  )
354
  gr.Code(
@@ -356,35 +509,20 @@ with gr.Blocks(css=css, title="地球物理 x Hugging Face 專案展示") as dem
356
  value=TUTORIAL_REQUIREMENTS,
357
  interactive=False
358
  )
359
- gr.Markdown(
360
- """
361
- <div style="padding: 1rem 0;">
362
- <h3>Step 3.6: 取得進階程式碼 📜</h3>
363
- <p>AI 會給你一份更複雜的 `app.py` 程式碼,就像下面這份:</p>
364
- </div>
365
- """
366
- )
367
  gr.Code(
368
  label="app.py (Level 2 範例)",
369
- value=TUTORIAL_CODE_ADVANCED,
 
 
 
 
 
 
 
370
  language="python",
371
  interactive=False
372
  )
373
- gr.Markdown(
374
- """
375
- <div style="padding: 1rem 0;">
376
- <h3>Step 4: 部署 App (上傳 2 個檔案)</h3>
377
- <p>部署你自己的版本!</p>
378
- <ol>
379
- <li><code>requirements.txt</code> (包含 `gradio`, `obspy`, `folium` 的清單)</li>
380
- <li><code>app.py</code> (包含上面 Level 2 的程式碼)</li>
381
- </ol>
382
-
383
- <h3>Step 5: 恭喜!你已晉升!🎉</h3>
384
- <p>Hugging Face 偵測到 `requirements.txt` 後,會自動安裝 `obspy` 和 `folium`。建置完成後 (可能需要幾分鐘),你將擁有一個專業級的地科應用程式!</spp>
385
- </div>
386
- """
387
- )
388
 
389
  # --- Tab 5: 關於 Spaces ---
390
  with gr.TabItem("ℹ️ 關於 Spaces", id=4):
@@ -417,7 +555,7 @@ with gr.Blocks(css=css, title="地球物理 x Hugging Face 專案展示") as dem
417
  gr.Markdown(
418
  """
419
  <hr>
420
- <p id="footer">此頁面由 Gemini 協助生成,展示 "1022 中央地科演講" 中的 Hugging Face G專案。</p>
421
  """
422
  )
423
 
 
1
  import gradio as gr
2
  import random
3
+ import textwrap
4
+ import folium
5
+ from obspy.clients.fdsn import Client
6
+ from obspy import UTCDateTime
7
 
8
+ # -------------------------------------------------------------------
9
+ # 範例 1 (基礎) 的預設程式碼
10
+ # -------------------------------------------------------------------
11
+ TUTORIAL_CODE_BASIC = textwrap.dedent("""
12
+ # --- 這是 Level 1 範例的完整程式碼 ---
13
+ # 試試看:
14
+ # 1. geo_dict 中新增一個你自己的詞條。
15
+ # 2. 修改 "海嘯" 的解釋。
16
+ # 3. 按下方的 "套用變更" 按鈕。
17
+ # 4. 回到 "Live Demo" 分頁看看你的修改!
18
 
19
+ # 1. 知識庫 (你的地科字典)
20
+ geo_dict = {
21
+ "板塊構造 (Plate Tectonics)": "地球的岩石圈被分成許多稱為「板塊」的巨大板塊,這些板塊在軟流圈上緩慢移動,彼此碰撞或分離,形成了火山、地震和山脈。",
22
+ "地震波 (Seismic Wave)": "地震時從震源向四面八方傳播的能量波。主要分為 P 波(壓縮波,速度快)和 S 波(剪力波,速度慢),它們是我們研究地球內部的主要工具。",
23
+ "海嘯 (Tsunami)": "由海底地震、火山爆發或山崩引起的巨大海浪。它在深海中傳播速度極快(可達時速 800 公里),但波高很低;當它靠近淺海岸時,速度減慢,波高則急遽增加。"
24
+ }
25
+
26
+ # 2. 定義介面函式
27
+ def get_definition(term):
28
+ # geo_dict 會從這個程式碼的 "全域" 範圍內被找到
29
+ return geo_dict.get(term, "查無此名詞")
30
+ """)
31
 
32
+ # -------------------------------------------------------------------
33
+ # 範例 1 (基礎) 的動態執行 Wrapper
34
+ # -------------------------------------------------------------------
35
+ def run_basic_code(term, code_string):
36
+ local_scope = {}
37
  try:
38
+ # 在一個受限的環境中執行程式碼
39
+ # 這會定義 geo_dict 和 get_definition
40
+ exec(code_string, {"__builtins__": {}}, local_scope)
41
+
42
+ get_definition_func = local_scope.get("get_definition")
43
+
44
+ if not callable(get_definition_func):
45
+ return "程式碼錯誤:找不到 `get_definition` 函式。"
46
+
47
+ # 呼叫學生程式碼中定義的函式
48
+ return get_definition_func(term)
49
+
50
  except Exception as e:
51
+ return f"執行時發生錯誤:\n{e}"
52
 
53
+ # -------------------------------------------------------------------
54
+ # 範例 2 (進階) 的預設程式碼
55
+ # -------------------------------------------------------------------
56
+ TUTORIAL_CODE_ADVANCED = textwrap.dedent("""
57
+ # --- 這是 Level 2 範例的完整程式碼 ---
58
+ # 試試看:
59
+ # 1. 找到 "color = 'green'",把它改成 "color = 'blue'"。
60
+ # 2. 找到 "radius=mag * 1.5",把它改成 "radius=mag * 3"。
61
+ # 3. 按下方的 "套用變更" 按鈕。
62
+ # 4. 回到 "Live Demo" 分頁,搜尋地震看看你的修改!
63
+
64
+ # 1. 定義核心函式:搜尋並繪圖
65
+ def fetch_and_plot_events(min_mag, days_ago):
66
+ try:
67
+ # Folium, Client, UTCDateTime 已經被安全地匯入
68
+ client = Client("IRIS")
69
+ endtime = UTCDateTime.now()
70
+ starttime = endtime - (days_ago * 24 * 3600)
71
+
72
+ catalog = client.get_events(
73
+ starttime=starttime,
74
+ endtime=endtime,
75
+ minmagnitude=min_mag
76
+ )
77
+
78
+ m = folium.Map(location=[0, 0], zoom_start=2)
79
+
80
+ if len(catalog) == 0:
81
+ return "<p>在此條件下查無地震資料,請嘗試放寬搜尋條件。</p>"
82
+
83
+ for event in catalog:
84
+ origin = event.preferred_origin()
85
+ mag = event.preferred_magnitude().mag
86
+ lat = origin.latitude
87
+ lon = origin.longitude
88
+ depth_km = origin.depth / 1000.0
89
+
90
+ if depth_km < 70:
91
+ color = "green" # <-- 試著修改這裡!
92
+ elif depth_km < 300:
93
+ color = "orange"
94
+ else:
95
+ color = "red"
96
+
97
+ popup_html = f'''
98
+ <b>時間:</b> {origin.time.strftime('%Y-%m-%d %H:%M:%S')}<br>
99
+ <b>規模 (M):</b> {mag:.1f}<br>
100
+ <b>深度 (km):</b> {depth_km:.1f}<br>
101
+ <b>位置:</b> ({lat:.2f}, {lon:.2f})
102
+ '''
103
+
104
+ folium.CircleMarker(
105
+ location=[lat, lon],
106
+ radius=mag * 1.5, # <-- 試著修改這裡!
107
+ popup=folium.Popup(popup_html, max_width=300),
108
+ color=color,
109
+ fill=True,
110
+ fill_color=color,
111
+ fill_opacity=0.6
112
+ ).add_to(m)
113
+
114
+ return m._repr_html_()
115
+
116
+ except Exception as e:
117
+ return f"<p>發生錯誤:{e}</p>"
118
+ """)
119
+
120
+ # -------------------------------------------------------------------
121
+ # 範例 2 (進階) 的安全沙盒與動態執行 Wrapper
122
+ # -------------------------------------------------------------------
123
+ def run_advanced_code(min_mag, days_ago, code_string):
124
+ # 建立一個安全的 "全域" 環境,只允許必要的模組
125
+ safe_globals = {
126
+ "__builtins__": {
127
+ "print": print, "Exception": Exception, "len": len, "str": str,
128
+ "float": float, "int": int, "list": list, "dict": dict, "range": range
129
+ },
130
+ "folium": folium,
131
+ "Client": Client,
132
+ "UTCDateTime": UTCDateTime
133
+ }
134
+
135
+ local_scope = {}
136
+
137
+ try:
138
+ # 在安全沙盒中執行學生程式碼
139
+ exec(code_string, safe_globals, local_scope)
140
+
141
+ fetch_func = local_scope.get("fetch_and_plot_events")
142
+
143
+ if not callable(fetch_func):
144
+ return "<p>程式碼錯誤:找不到 `fetch_and_plot_events` 函式。</p>"
145
+
146
+ # 呼叫學生程式碼中定義的函式
147
+ return fetch_func(min_mag, days_ago)
148
+
149
+ except Exception as e:
150
+ return f"<p>執行時發生錯誤:\n{e}</p>"
151
+
152
+ # -------------------------------------------------------------------
153
+ # 專案總覽的資料
154
+ # -------------------------------------------------------------------
155
  projects_data = {
156
  "🌊 地震與波形展示": [
157
  {"title": "📡 地震目錄搜尋 API", "url": "https://cwadayi-python-app.hf.space/"},
 
183
  ]
184
  }
185
 
 
186
  def recommend_project():
 
187
  all_projects = [item for sublist in projects_data.values() for item in sublist]
188
  recommendation = random.choice(all_projects)
 
189
  return f"""
190
  ### {recommendation['title']}
 
191
  [**點此立即前往 🚀**]({recommendation['url']})
192
  """
193
 
 
194
  TUTORIAL_REQUIREMENTS = textwrap.dedent("""
195
  gradio
196
  obspy
197
  folium
198
  """)
199
 
200
+ # -------------------------------------------------------------------
201
+ # 自訂 CSS
202
+ # -------------------------------------------------------------------
 
 
203
  css = """
204
  /* 標頭樣式 */
205
  #main-header {
 
299
  #footer { text-align: center; color: #777; font-size: 0.9rem; margin-top: 2rem; }
300
  """
301
 
302
+ # -------------------------------------------------------------------
303
+ # Gradio 應用程式主體
304
+ # -------------------------------------------------------------------
305
  with gr.Blocks(css=css, title="地球物理 x Hugging Face 專案展示") as demo:
306
 
307
  # 標頭區
308
  with gr.Row(elem_id="main-header"):
309
  gr.Markdown(
310
  """
311
+ <img id="header-image" src="https://images.pexels.com/photos/220201/pexels-photo-220201.jpeg" alt="Abstract seismic waves banner">
312
  <h1>地球物理 x Hugging Face 專案展示</h1>
313
  <p>一個地球物理學家的數位百寶箱:探索一系列由 cwadayi 打造的地震與地科應用!</p>
314
  """
 
342
  elem_classes="project-button"
343
  )
344
 
345
+ # --- [升級] Tab 2: 範例 1 (即時編輯) ---
346
  with gr.TabItem("📖 範例 1: 地科字典", id=1):
347
+ gr.Markdown("# 🚀 範例 1: 地科字典 (Live Demo & Editor)")
348
+
349
+ # 儲存學生程式碼的狀態變數
350
+ basic_code_state = gr.State(value=TUTORIAL_CODE_BASIC)
351
+
352
+ with gr.Tabs():
353
+ with gr.TabItem("🚀 Live Demo"):
354
+ gr.Markdown("在這裡測試你的程式碼!修改「Edit Code」分頁中的內容,按下「套用變更」,然後在這裡查看結果。")
355
+ dropdown = gr.Dropdown(
356
+ label="請選擇一個地科名詞",
357
+ # [動態] 選項會根據學生程式碼動態生成
358
+ choices=list(eval(TUTORIAL_CODE_BASIC.splitlines()[10].split('=')[1].strip()))
359
+ )
360
+ output_textbox = gr.Textbox(label="名詞解釋", lines=5, interactive=False)
361
+
362
+ with gr.TabItem("✏️ Edit Code"):
363
+ gr.Markdown("在這裡修改 Python 程式碼,然後按下「套用變更」儲存。")
364
+ basic_code_editor = gr.Code(
365
+ label="app.py (Level 1 範例)",
366
+ value=TUTORIAL_CODE_BASIC,
367
+ language="python",
368
+ interactive=True,
369
+ lines=20
370
+ )
371
+ apply_btn_basic = gr.Button("套用變更", variant="primary")
372
+ apply_msg_basic = gr.Markdown("")
373
+
374
+ # --- 範例 1 的邏輯綁定 ---
375
+
376
+ # 1. "套用變更" 按鈕的邏輯
377
+ def update_basic_code(new_code):
378
+ # 執行程式碼以檢查是否有語法錯誤
379
+ # 並動態更新 Dropdown 的選項
380
+ local_scope = {}
381
+ try:
382
+ exec(new_code, {"__builtins__": {}}, local_scope)
383
+ new_dict = local_scope.get("geo_dict")
384
+ if not isinstance(new_dict, dict):
385
+ raise ValueError("找不到 `geo_dict` 變數或其不是一個字典。")
386
+
387
+ # 成功!更新 Dropdown 選項並儲存程式碼
388
+ new_choices = list(new_dict.keys())
389
+ return {
390
+ basic_code_state: new_code, # 儲存新程式碼到 state
391
+ dropdown: gr.Dropdown(choices=new_choices), # 更新 Dropdown
392
+ apply_msg_basic: gr.Markdown("✅ 變更已套用!請回到 'Live Demo' 分頁查看。")
393
+ }
394
+ except Exception as e:
395
+ # 失敗!
396
+ return {
397
+ basic_code_state: basic_code_state, # 保持舊程式碼
398
+ dropdown: dropdown, # 保持舊 Dropdown
399
+ apply_msg_basic: gr.Markdown(f"❌ 程式碼錯誤,未套用:\n```\n{e}\n```")
400
+ }
401
+
402
+ apply_btn_basic.click(
403
+ fn=update_basic_code,
404
+ inputs=[basic_code_editor],
405
+ outputs=[basic_code_state, dropdown, apply_msg_basic]
406
+ )
407
 
408
+ # 2. "Live Demo" Dropdown 的邏輯
409
+ dropdown.change(
410
+ fn=run_basic_code,
411
+ inputs=[dropdown, basic_code_state], # [動態] 傳入儲存的程式碼
412
+ outputs=output_textbox
413
+ )
414
+
415
+ # --- [升級] Tab 3: 範例 2 (即時編輯) ---
416
  with gr.TabItem("🌍 範例 2: 地震地圖", id=2):
417
+ gr.Markdown("# 🚀 範例 2: 進階地震地圖 (Live Demo & Editor)")
418
+
419
+ # 儲存學生程式碼的狀態變數
420
+ advanced_code_state = gr.State(value=TUTORIAL_CODE_ADVANCED)
421
+
422
+ with gr.Tabs():
423
+ with gr.TabItem("🚀 Live Demo"):
424
+ gr.Markdown("在這裡測試你的程式碼!修改「Edit Code」分頁中的內容,按下「套用變更」,然後在這裡點擊「搜尋」。")
425
+ with gr.Row():
426
+ mag_slider = gr.Slider(minimum=4.0, maximum=8.0, value=5.0, step=0.1, label="最小地震規模 (M)")
427
+ days_slider = gr.Slider(minimum=1, maximum=30, value=7, step=1, label="搜尋天數 (過去幾天)")
428
+ search_button = gr.Button("搜尋並繪製地圖 🌍", variant="primary")
429
+ map_output = gr.HTML(label="地震分佈圖")
430
+
431
+ with gr.TabItem("✏️ Edit Code"):
432
+ gr.Markdown("在這裡修改 Python 程式碼,然後按下「套用變更」儲存。")
433
+ advanced_code_editor = gr.Code(
434
+ label="app.py (Level 2 範例)",
435
+ value=TUTORIAL_CODE_ADVANCED,
436
+ language="python",
437
+ interactive=True,
438
+ lines=40 # 更多行
439
  )
440
+ apply_btn_advanced = gr.Button("套用變更", variant="primary")
441
+ apply_msg_advanced = gr.Markdown("")
442
+
443
+ # --- 範例 2 的邏輯綁定 ---
444
+
445
+ # 1. "套用變更" 按鈕的邏輯
446
+ def update_advanced_code(new_code):
447
+ # 僅儲存,我們在點擊 "Search" 時才做完整測試
448
+ apply_msg_advanced.value = "✅ 變更已儲存!請回到 'Live Demo' 分頁並點擊 '搜尋' 來執行新程式碼。"
449
+ return new_code, "✅ 變更已儲存!請回到 'Live Demo' 分頁並點擊 '搜尋' 來執行新程式碼。"
450
+
451
+ apply_btn_advanced.click(
452
+ fn=lambda x: (x, "✅ 變更已儲存!請回到 'Live Demo' 分頁並點擊 '搜尋' 來執行新程式碼。"), # 簡單的儲存
453
+ inputs=[advanced_code_editor],
454
+ outputs=[advanced_code_state, apply_msg_advanced]
455
+ )
456
+
457
+ # 2. "Live Demo" Search 按鈕的邏輯
458
+ search_button.click(
459
+ fn=run_advanced_code,
460
+ inputs=[mag_slider, days_slider, advanced_code_state], # [動態] 傳入儲存的程式碼
461
+ outputs=map_output
462
+ )
463
 
464
  # --- Tab 4: AI 創客空間 (教學) ---
465
  with gr.TabItem("🎓 AI 創客空間", id=3):
 
466
  gr.Markdown(
467
  """
468
  <div style="padding: 1rem 0;">
469
+ <h2>🚀 打造你自己的地科 App!</h2>
470
  <p>這是一個教你如何「跟 AI (例如 Gemini) 協作」來快速打造並部署應用程式的SOP:</p>
471
+ <p><strong>最好的學習方式就是動手修改!</strong> 請前往「範例 1」和「範例 2」分頁中的 <strong>"✏️ Edit Code"</strong> 標籤,直接修改程式碼並查看即時結果!</p>
472
  </div>
473
  """
474
  )
475
+ with gr.Accordion("Level 1: 基礎字典 App 📖 (點我展開)", open=False):
476
+ gr.Markdown("### 部署你自己的版本:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  gr.Markdown(
478
  """
 
 
 
479
  <ol>
480
  <li>到 Hugging Face 網站,點選你的頭像,選擇 <strong>"New Space"</strong>。</li>
481
  <li>為你的 Space 命名 (例如: <code>my-geo-dictionary</code>)。</li>
 
483
  <li>點擊 "Create Space"。</li>
484
  <li>在你的 Space 頁面,點選 "Files and versions" -> "Add file" -> "Create new file"。</li>
485
  <li>將檔案命名為 <strong><code>app.py</code></strong> (必須是這個名字!)。</li>
486
+ <li><strong>將下面的整段程式碼,完整貼到 `app.py` 的編輯器中。</strong></li>
487
  <li>點擊 "Commit new file" (提交檔案)。</li>
488
  </ol>
 
 
 
 
489
  """
490
  )
491
+ gr.Code(
492
+ label="app.py (Level 1 範例)",
493
+ value=TUTORIAL_CODE_BASIC.replace("get_definition(term):", "def get_definition(term):\n global geo_dict"), # 修正範例程式碼中的 scope
494
+ language="python",
495
+ interactive=False
496
+ )
497
 
498
  with gr.Accordion("Level 2: 進階地震地圖 App 🌍 (點我展開)", open=False):
499
+ gr.Markdown("### 部署你自己的版本:")
500
  gr.Markdown(
501
  """
502
+ <p>這次你需要上傳 <strong>2 個檔案</strong>到你自己的 Hugging Face Space:</p>
503
+ <h4>1. <code>requirements.txt</code></h4>
504
+ <p>建立這個檔案並貼上下面的內容,告訴 Hugging Face 你需要 `obspy` 和 `folium`。</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  """
506
  )
507
  gr.Code(
 
509
  value=TUTORIAL_REQUIREMENTS,
510
  interactive=False
511
  )
512
+ gr.Markdown("<h4>2. <code>app.py</code></h4><p>建立 `app.py` 檔案並貼上下面的內容。</p>")
 
 
 
 
 
 
 
513
  gr.Code(
514
  label="app.py (Level 2 範例)",
515
+ value=TUTORIAL_CODE_ADVANCED.replace("# 1. 定義核心函式",
516
+ """# --- 匯入函式庫 ---
517
+ import gradio as gr
518
+ import folium
519
+ from obspy.clients.fdsn import Client
520
+ from obspy import UTCDateTime
521
+
522
+ # 1. 定義核心函式"""), # 補上 import
523
  language="python",
524
  interactive=False
525
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
  # --- Tab 5: 關於 Spaces ---
528
  with gr.TabItem("ℹ️ 關於 Spaces", id=4):
 
555
  gr.Markdown(
556
  """
557
  <hr>
558
+ <p id="footer">此頁面由 Gemini 協助生成,展示 "1022 中央地科演講" 中的 Hugging Face 專案。</p>
559
  """
560
  )
561