File size: 27,428 Bytes
094197b
11bfd54
e5ac43c
 
 
 
f2e9915
094197b
e5ac43c
abd1160
e5ac43c
abd1160
d6395fe
abd1160
 
 
 
 
e5ac43c
 
 
 
 
 
8ca0972
e5ac43c
abd1160
e5ac43c
 
 
 
 
 
8ca0972
e5ac43c
 
 
 
f2e9915
 
 
 
69a9699
f2e9915
 
e5ac43c
f2e9915
e5ac43c
 
 
 
 
 
 
69a9699
e5ac43c
69a9699
e5ac43c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6395fe
f2e9915
 
e5ac43c
 
 
abd1160
d6395fe
e5ac43c
 
 
d6395fe
e5ac43c
 
 
 
 
 
d6395fe
e5ac43c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
094197b
 
11bfd54
 
 
 
094197b
 
11bfd54
 
 
 
 
094197b
 
11bfd54
 
 
 
 
094197b
 
11bfd54
 
 
 
 
 
094197b
 
 
11bfd54
 
 
 
 
 
 
 
e4e12de
 
 
 
 
 
e5ac43c
 
 
094197b
11bfd54
094197b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11bfd54
 
0906445
 
11bfd54
 
 
 
 
a09a96e
1fb00fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11bfd54
 
0906445
11bfd54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0906445
11bfd54
0906445
 
 
094197b
11bfd54
0906445
094197b
11bfd54
094197b
 
 
 
 
 
0906445
094197b
0906445
 
094197b
 
e5ac43c
 
 
094197b
 
a09a96e
094197b
 
 
e5ac43c
094197b
11bfd54
094197b
 
 
a09a96e
1fb00fb
 
 
 
a09a96e
1fb00fb
 
 
 
 
 
0906445
1fb00fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2e9915
8ca0972
e5ac43c
 
 
 
 
 
 
abd1160
e5ac43c
 
f2e9915
e5ac43c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
abd1160
d6395fe
abd1160
e5ac43c
 
 
 
 
 
f2e9915
 
e5ac43c
 
 
f2e9915
e5ac43c
f2e9915
 
e5ac43c
 
 
 
 
 
 
 
8ca0972
e5ac43c
 
f2e9915
e5ac43c
 
 
f2e9915
8ca0972
e5ac43c
 
 
 
 
 
 
 
 
 
 
d6395fe
e5ac43c
 
 
 
 
 
 
 
f2e9915
8ca0972
e5ac43c
 
 
 
 
 
d6395fe
e5ac43c
 
 
f2e9915
e5ac43c
 
 
 
 
d6395fe
f2e9915
e5ac43c
 
8ca0972
 
 
1fb00fb
 
 
e5ac43c
1fb00fb
e5ac43c
1fb00fb
a09a96e
 
e5ac43c
 
e4e12de
 
 
 
 
 
 
 
 
e5ac43c
e4e12de
 
 
 
f2e9915
 
 
 
 
 
 
 
 
 
 
 
e5ac43c
d6395fe
f2e9915
e5ac43c
 
 
a09a96e
e4e12de
e5ac43c
e4e12de
 
e5ac43c
 
 
e4e12de
 
 
 
 
 
 
e5ac43c
f2e9915
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4e12de
d6395fe
f2e9915
e4e12de
 
 
0906445
8ca0972
 
1fb00fb
 
 
 
 
 
 
8ca0972
1fb00fb
 
 
 
 
 
 
8ca0972
f2e9915
1fb00fb
 
 
d6395fe
e4e12de
1fb00fb
 
094197b
a09a96e
094197b
 
1fb00fb
e5ac43c
094197b
 
11bfd54
69a9699
11bfd54
0906445
 
 
11bfd54
094197b
69a9699
094197b
 
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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
import gradio as gr
import random
import textwrap
import folium
from obspy.clients.fdsn import Client
from obspy import UTCDateTime
import ast # 用於安全地解析字典

# -------------------------------------------------------------------
# 範例 1 (基礎) 的預設資料
# -------------------------------------------------------------------
TUTORIAL_BASIC_INITIAL_DICT = {
    "板塊構造 (Plate Tectonics)": "地球的岩石圈被分成許多稱為「板塊」的巨大板塊,這些板塊在軟流圈上緩慢移動,彼此碰撞或分離,形成了火山、地震和山脈。",
    "地震波 (Seismic Wave)": "地震時從震源向四面八方傳播的能量波。主要分為 P 波(壓縮波,速度快)和 S 波(剪力波,速度慢),它們是我們研究地球內部的主要工具。",
    "海嘯 (Tsunami)": "由海底地震、火山爆發或山崩引起的巨大海浪。它在深海中傳播速度極快(可達時速 800 公里),但波高很低;當它靠近淺海岸時,速度減慢,波高則急遽增加。"
}

TUTORIAL_CODE_BASIC = textwrap.dedent(f"""
# --- 這是 Level 1 範例的完整程式碼 ---
# 試試看:
# 1. 在 geo_dict 中新增一個你自己的詞條。
# 2. 修改 "海嘯" 的解釋。
# 3. 按下方的 "套用變更" 按鈕。
# 4. 回到 "Live Demo" 分頁看看你的修改!

# 1. 知識庫 (你的地科字典)
geo_dict = {TUTORIAL_BASIC_INITIAL_DICT!r}

# 2. 定義介面函式
def get_definition(term):
    # geo_dict 會從這個程式碼的 "全域" 範圍內被找到
    return geo_dict.get(term, "查無此名詞")
""")

# -------------------------------------------------------------------
# 範例 1 (基礎) 的動態執行 Wrapper
# -------------------------------------------------------------------
def run_basic_code(term, code_string):
    # [***** 修復 *****]
    # 建立一個同時作為 global 和 local 範圍的 scope
    # 這樣在 `exec` 中定義的函式才能找到在同一個 scope 中定義的全域變數
    execution_scope = {"__builtins__": {}} 
    try:
        # [修復] 將 global 和 local 都設置為同一個 execution_scope
        exec(code_string, execution_scope, execution_scope) 
        
        get_definition_func = execution_scope.get("get_definition")
        
        if not callable(get_definition_func):
            return "程式碼錯誤:找不到 `get_definition` 函式。"
            
        # 呼叫學生程式碼中定義的函式
        return get_definition_func(term)
        
    except Exception as e:
        return f"執行時發生錯誤:\n{e}"

# -------------------------------------------------------------------
# 範例 2 (進階) 的預設程式碼
# -------------------------------------------------------------------
TUTORIAL_CODE_ADVANCED = textwrap.dedent("""
# --- 這是 Level 2 範例的完整程式碼 ---
# 試試看:
# 1. 找到 "color = 'green'",把它改成 "color = 'blue'"。
# 2. 找到 "radius=mag * 1.5",把它改成 "radius=mag * 3"。
# 3. 按下方的 "套用變更" 按鈕。
# 4. 回到 "Live Demo" 分頁,搜尋地震看看你的修改!

# 1. 定義核心函式:搜尋並繪圖
def fetch_and_plot_events(min_mag, days_ago):
    try:
        # Folium, Client, UTCDateTime 已經被安全地匯入
        client = Client("IRIS")
        endtime = UTCDateTime.now()
        starttime = endtime - (days_ago * 24 * 3600)
        
        catalog = client.get_events(
            starttime=starttime,
            endtime=endtime,
            minmagnitude=min_mag
        )
        
        m = folium.Map(location=[0, 0], zoom_start=2)
        
        if len(catalog) == 0:
            return "<p>在此條件下查無地震資料,請嘗試放寬搜尋條件。</p>"

        for event in catalog:
            origin = event.preferred_origin()
            mag = event.preferred_magnitude().mag
            lat = origin.latitude
            lon = origin.longitude
            depth_km = origin.depth / 1000.0
            
            if depth_km < 70:
                color = "green" # <-- 試著修改這裡!
            elif depth_km < 300:
                color = "orange"
            else:
                color = "red"
            
            popup_html = f'''
            <b>時間:</b> {origin.time.strftime('%Y-%m-%d %H:%M:%S')}<br>
            <b>規模 (M):</b> {mag:.1f}<br>
            <b>深度 (km):</b> {depth_km:.1f}<br>
            <b>位置:</b> ({lat:.2f}, {lon:.2f})
            '''
            
            folium.CircleMarker(
                location=[lat, lon],
                radius=mag * 1.5, # <-- 試著修改這裡!
                popup=folium.Popup(popup_html, max_width=300),
                color=color,
                fill=True,
                fill_color=color,
                fill_opacity=0.6
            ).add_to(m)
            
        return m._repr_html_()

    except Exception as e:
        return f"<p>發生錯誤:{e}</p>"
""")

# -------------------------------------------------------------------
# 範例 2 (進階) 的安全沙盒與動態執行 Wrapper
# -------------------------------------------------------------------
def run_advanced_code(min_mag, days_ago, code_string):
    # 建立一個安全的 "全域" 環境,只允許必要的模組
    # 這個設計是正確的,因為函式(fetch_and_plot_events)需要存取
    # 這些 "預先匯入" 的全域模組。
    safe_globals = {
        "__builtins__": {
            "print": print, "Exception": Exception, "len": len, "str": str, 
            "float": float, "int": int, "list": list, "dict": dict, "range": range,
            "True": True, "False": False, "None": None
        },
        "folium": folium,
        "Client": Client,
        "UTCDateTime": UTCDateTime
    }
    
    local_scope = {}
    
    try:
        # 在安全沙盒中執行學生程式碼
        exec(code_string, safe_globals, local_scope)
        
        fetch_func = local_scope.get("fetch_and_plot_events")
        
        if not callable(fetch_func):
            return "<p>程式碼錯誤:找不到 `fetch_and_plot_events` 函式。</p>"
        
        # 呼叫學生程式碼中定義的函式
        return fetch_func(min_mag, days_ago)
        
    except Exception as e:
        return f"<p>執行時發生錯誤:\n{e}</p>"

# -------------------------------------------------------------------
# 專案總覽的資料
# -------------------------------------------------------------------
projects_data = {
    "🌊 地震與波形展示": [
        {"title": "📡 地震目錄搜尋 API", "url": "https://cwadayi-python-app.hf.space/"},
        {"title": "⚠️ 地震預警訊息顯示", "url": "https://huggingface.co/spaces/cwadayi/streamlit_alarm_Taiwan"},
        {"title": "🗺️ PyGMT 震央繪圖展示", "url": "https://huggingface.co/spaces/cwadayi/streamlit_gmt_demo_new"},
        {"title": "📈 ObsPy 地震資料處理", "url": "https://huggingface.co/spaces/cwadayi/streamlit_obspy"}
    ],
    "🛰️ 地震監測與資料應用": [
        {"title": "📍 Geiger 法地震定位展示", "url": "https://huggingface.co/spaces/cwadayi/streamlit_earthquake_location"},
        {"title": "🔍 地震監測流程 (擷取-撿拾-定位)", "url": "https://huggingface.co/spaces/cwadayi/earthquake_monitoring"},
        {"title": "📊 Google Sheet 資料顯示 (類Grafana)", "url": "https://huggingface.co/spaces/cwadayi/Grafana_like_2"},
        {"title": "🏠 PWS 地震預警顯示", "url": "https://cwadayi-app-show-pws.hf.space/cwa-earthquakes"},
        {"title": "🌍 全球地震顯示 (Streamlit)", "url": "https://huggingface.co/spaces/cwadayi/streamlit"}
    ],
    "💻 資料擷取與 API 應用": [
        {"title": "💬 LINE ROBOT 伺服器", "url": "https://huggingface.co/spaces/cwadayi/LINE-ROBOT"},
        {"title": "🗺️ 震央分佈圖 (MCP-2)", "url": "https://huggingface.co/spaces/cwadayi/MCP-2"},
        {"title": "📥 PWS 資訊擷取", "url": "https://huggingface.co/spaces/cwadayi/MCP-pws"},
        {"title": "🏛️ USGS 地震資料 API", "url": "https://huggingface.co/spaces/cwadayi/Usgs_api_gemini"},
        {"title": "🇹🇼 CWA 地震資料 API", "url": "https://huggingface.co/spaces/cwadayi/CWA_API_chatgpt5_1"}
    ],
    "🏫 地球物理課程與教材": [
        {"title": "🎓 地球物理課程 (總覽)", "url": "https://huggingface.co/spaces/cwadayi/Geophysics_class"},
        {"title": "📗 地物課程網站 (Day1)", "url": "https://huggingface.co/spaces/cwadayi/Geophysics_day1"},
        {"title": "📘 地物課程網站 (Day2)", "url": "https://huggingface.co/spaces/cwadayi/Geophysics_day2"},
        {"title": "📏 折射震測展示 (SDD)", "url": "https://huggingface.co/spaces/cwadayi/Refraction_sdd"},
        {"title": "📐 折射震測展示 (Refraction 3)", "url": "https://huggingface.co/spaces/cwadayi/Refraction_3"},
        {"title": "⚖️ 重力異常展示 (Bouguer & Free-air)", "url": "https://huggingface.co/spaces/cwadayi/Gravity"}
    ]
}

def recommend_project():
    all_projects = [item for sublist in projects_data.values() for item in sublist]
    recommendation = random.choice(all_projects)
    return f"""
    ### {recommendation['title']}
    [**點此立即前往 🚀**]({recommendation['url']})
    """

TUTORIAL_REQUIREMENTS = textwrap.dedent("""
gradio
obspy
folium
""")

# -------------------------------------------------------------------
# 自訂 CSS
# -------------------------------------------------------------------
css = """
/* 標頭樣式 */
#main-header {
    background: linear-gradient(135deg, #2c3e50, #34495e);
    padding: 2rem 1.5rem;
    border-bottom: 5px solid #1abc9c;
}
#main-header h1 {
    font-size: 2.8rem;
    font-weight: 700;
    color: #ffffff;
    text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
    margin-bottom: 0.5rem;
    text-align: center;
}
#main-header p {
    font-size: 1.2rem;
    color: #ecf0f1;
    max-width: 600px;
    margin: 0 auto;
    text-align: center;
}
#header-image {
    width: 100%;
    max-height: 250px;
    object-fit: cover;
    border-radius: 12px;
    margin-bottom: 1rem;
    box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}

/* Tab 按鈕美化 */
#main-tabs > .tab-buttons > button {
    background-color: #f0f2f5;
    color: #555;
    font-size: 1.05rem !important;
    font-weight: 600 !important;
    border: 2px solid #e0e0e0 !important;
    border-radius: 10px !important;
    margin: 0 5px !important;
    padding: 10px 20px !important;
    transition: all 0.3s ease !important;
}
#main-tabs > .tab-buttons > button:hover:not(.selected) {
    background-color: #e8e8e8;
    border-color: #ccc;
    transform: translateY(-2px);
}
#main-tabs > .tab-buttons > button.selected {
    background-color: #1abc9c !important;
    color: white !important;
    border-color: #1abc9c !important;
    box-shadow: 0 4px 10px rgba(22, 160, 133, 0.3);
}

/* 推薦按鈕 */
#recommend-button {
    background: linear-gradient(135deg, #1abc9c, #16a085);
    color: white !important;
    font-size: 1.1rem !important;
    font-weight: bold !important;
    border: none !important;
    border-radius: 10px !important;
    transition: all 0.3s ease !important;
}
#recommend-button:hover {
    background: linear-gradient(135deg, #16a085, #1abc9c);
    transform: scale(1.02);
    box-shadow: 0 5px 15px rgba(22, 160, 133, 0.4);
}

/* 推薦輸出框 */
#recommend-output {
    border: 2px dashed #1abc9c;
    border-radius: 10px;
    padding: 1.5rem;
    text-align: center;
    background-color: #f8fdfc;
}
#recommend-output h3 { margin-top: 0.5rem; margin-bottom: 1.2rem; color: #2c3e50; font-size: 1.4rem; }
#recommend-output a { font-size: 1.1rem; font-weight: bold; color: #2980b9; text-decoration: none; transition: color 0.3s ease; }
#recommend-output a:hover { color: #3498db; text-decoration: underline; }

/* 專案標題 */
.project-title h3 { margin: 0; padding: 0.5rem 0; font-size: 1.2rem; color: #2c3e50; }

/* 專案按鈕 */
.project-button a {
    background-color: #3498db !important;
    color: #ffffff !important;
    border-radius: 8px !important;
    transition: background-color 0.3s ease, transform 0.2s ease !important;
}
.project-button a:hover { background-color: #2980b9 !important; transform: scale(1.03); }

/* 頁腳 */
#footer { text-align: center; color: #777; font-size: 0.9rem; margin-top: 2rem; }
"""

# -------------------------------------------------------------------
# Gradio 應用程式主體
# -------------------------------------------------------------------
with gr.Blocks(css=css, title="地球物理 x Hugging Face 專案展示") as demo:
    
    # 標頭區
    with gr.Row(elem_id="main-header"):
        gr.Markdown(
            """
            <img id="header-image" src="https://images.pexels.com/photos/220201/pexels-photo-220201.jpeg" alt="Abstract seismic waves banner">
            <h1>地球物理 x Hugging Face 專案展示</h1>
            <p>一個地球物理學家的數位百寶箱:探索一系列由 cwadayi 打造的地震與地科應用!</p>
            """
        )
    
    # 主內容區:Tab 介面
    with gr.Tabs(elem_id="main-tabs") as tabs:
        
        # --- Tab 1: 專案總覽 ---
        with gr.TabItem("📚 專案總覽", id=0):
            # 幸運推薦區
            gr.Markdown("---")
            with gr.Blocks():
                gr.Markdown("## ✨ 幸運推薦 ✨")
                recommend_btn = gr.Button("不知道從何逛起? 點我隨機推薦一個專案!", elem_id="recommend-button")
                recommend_out = gr.Markdown("點擊上方按鈕,讓我為您挑選一個有趣的專案!", elem_id="recommend-output")
            gr.Markdown("---")

            # 專案列表區
            gr.Markdown("## 🗂️ 專案列表")
            for category, items in projects_data.items():
                with gr.Accordion(category, open=True):
                    for item in items:
                        with gr.Row(variant="panel", equal_height=True):
                            with gr.Column(scale=4):
                                gr.Markdown(f"### {item['title']}", elem_classes="project-title")
                            with gr.Column(scale=1, min_width=150):
                                gr.Button(
                                    "前往應用 ↗", 
                                    link=item['url'], 
                                    elem_classes="project-button"
                                )
        
        # --- Tab 2: 範例 1 (即時編輯) ---
        with gr.TabItem("📖 範例 1: 地科字典", id=1):
            gr.Markdown("# 🚀 範例 1: 地科字典 (Live Demo & Editor)")
            
            basic_code_state = gr.State(value=TUTORIAL_CODE_BASIC)
            
            with gr.Tabs():
                with gr.TabItem("🚀 Live Demo"):
                    gr.Markdown("在這裡測試你的程式碼!修改「Edit Code」分頁中的內容,按下「套用變更」,然後在這裡查看結果。")
                    
                    dropdown = gr.Dropdown(
                        label="請選擇一個地科名詞",
                        choices=list(TUTORIAL_BASIC_INITIAL_DICT.keys()) # 從預設字典載入
                    )
                    output_textbox = gr.Textbox(label="名詞解釋", lines=5, interactive=False)
                
                with gr.TabItem("✏️ Edit Code"):
                    gr.Markdown("在這裡修改 Python 程式碼,然後按下「套用變更」儲存。")
                    basic_code_editor = gr.Code(
                        label="app.py (Level 1 範例)",
                        value=TUTORIAL_CODE_BASIC,
                        language="python",
                        interactive=True,
                        lines=20
                    )
                    apply_btn_basic = gr.Button("套用變更", variant="primary")
                    apply_msg_basic = gr.Markdown("")
            
            # --- 範例 1 的邏輯綁定 ---
            
            def update_basic_code(new_code):
                local_scope = {}
                try:
                    parsed_code = ast.parse(new_code)
                    exec(compile(parsed_code, "<string>", "exec"), {"__builtins__": {}}, local_scope)
                    
                    new_dict = local_scope.get("geo_dict")
                    if not isinstance(new_dict, dict):
                         raise ValueError("找不到 `geo_dict` 變數或其不是一個字典。")
                    
                    new_choices = list(new_dict.keys())
                    return {
                        basic_code_state: new_code,
                        dropdown: gr.Dropdown(choices=new_choices, label="請選擇一個地科名詞"), 
                        apply_msg_basic: gr.Markdown("✅ 變更已套用!請回到 'Live Demo' 分頁查看。")
                    }
                except Exception as e:
                    # 保持原狀,並顯示錯誤
                    return {
                        basic_code_state: basic_code_state, 
                        dropdown: dropdown, 
                        apply_msg_basic: gr.Markdown(f"❌ 程式碼錯誤,未套用:\n```\n{e}\n```")
                    }
            
            apply_btn_basic.click(
                fn=update_basic_code,
                inputs=[basic_code_editor],
                outputs=[basic_code_state, dropdown, apply_msg_basic]
            )

            dropdown.change(
                fn=run_basic_code,
                inputs=[dropdown, basic_code_state], 
                outputs=output_textbox
            )

        # --- Tab 3: 範例 2 (即時編輯) ---
        with gr.TabItem("🌍 範例 2: 地震地圖", id=2):
            gr.Markdown("# 🚀 範例 2: 進階地震地圖 (Live Demo & Editor)")
            
            advanced_code_state = gr.State(value=TUTORIAL_CODE_ADVANCED)
            
            with gr.Tabs():
                with gr.TabItem("🚀 Live Demo"):
                    gr.Markdown("在這裡測試你的程式碼!修改「Edit Code」分頁中的內容,按下「套用變更」,然後在這裡點擊「搜尋」。")
                    with gr.Row():
                        mag_slider = gr.Slider(minimum=4.0, maximum=8.0, value=5.0, step=0.1, label="最小地震規模 (M)")
                        days_slider = gr.Slider(minimum=1, maximum=30, value=7, step=1, label="搜尋天數 (過去幾天)")
                    search_button = gr.Button("搜尋並繪製地圖 🌍", variant="primary")
                    map_output = gr.HTML(label="地震分佈圖")

                with gr.TabItem("✏️ Edit Code"):
                    gr.Markdown("在這裡修改 Python 程式碼,然後按下「套用變更」儲存。")
                    advanced_code_editor = gr.Code(
                        label="app.py (Level 2 範例)",
                        value=TUTORIAL_CODE_ADVANCED,
                        language="python",
                        interactive=True,
                        lines=40
                    )
                    apply_btn_advanced = gr.Button("套用變更", variant="primary")
                    apply_msg_advanced = gr.Markdown("")
            
            # --- 範例 2 的邏輯綁定 ---

            def update_advanced_code(new_code):
                # 僅儲存,我們在點擊 "Search" 時才做完整測試
                return new_code, "✅ 變更已儲存!請回到 'Live Demo' 分頁並點擊 '搜尋' 來執行新程式碼。"

            apply_btn_advanced.click(
                fn=update_advanced_code,
                inputs=[advanced_code_editor],
                outputs=[advanced_code_state, apply_msg_advanced]
            )

            search_button.click(
                fn=run_advanced_code,
                inputs=[mag_slider, days_slider, advanced_code_state], 
                outputs=map_output
            )

        # --- Tab 4: AI 創客空間 (教學) ---
        with gr.TabItem("🎓 AI 創客空間", id=3):
            gr.Markdown(
                """
                <div style="padding: 1rem 0;">
                <h2>🚀 打造你自己的地科 App!</h2>
                <p>這是一個教你如何「跟 AI (例如 Gemini) 協作」來快速打造並部署應用程式的SOP:</p>
                <p><strong>最好的學習方式就是動手修改!</strong> 請前往「範例 1」和「範例 2」分頁中的 <strong>"✏️ Edit Code"</strong> 標籤,直接修改程式碼並查看即時結果!</p>
                </div>
                """
            )
            with gr.Accordion("Level 1: 基礎字典 App 📖 (點我展開)", open=False):
                gr.Markdown("### 部署你自己的版本:")
                gr.Markdown(
                    """
                    <ol>
                        <li>到 Hugging Face 網站,點選你的頭像,選擇 <strong>"New Space"</strong>。</li>
                        <li>為你的 Space 命名 (例如: <code>my-geo-dictionary</code>)。</li>
                        <li>在 "Space SDK" 中選擇 <strong>"Gradio"</strong> (很重要!)。</li>
                        <li>點擊 "Create Space"。</li>
                        <li>在你的 Space 頁面,點選 "Files and versions" -> "Add file" -> "Create new file"。</li>
                        <li>將檔案命名為 <strong><code>app.py</code></strong> (必須是這個名字!)。</li>
                        <li><strong>將下面的整段程式碼,完整貼到 `app.py` 的編輯器中。</strong></li>
                        <li>點擊 "Commit new file" (提交檔案)。</li>
                    </ol>
                    """
                )
                
                # [修復] 修正教學程式碼,使其可以獨立運作
                standalone_basic_code = (
                    "import gradio as gr\n\n" +
                    TUTORIAL_CODE_BASIC +
                    "\n\nwith gr.Blocks() as demo:\n" +
                    "    gr.Markdown('# 🚀 我的第一個地科 App (Level 1)')\n" +
                    "    dropdown = gr.Dropdown(label='請選擇一個地科名詞', choices=list(geo_dict.keys()))\n" +
                    "    output_textbox = gr.Textbox(label='名詞解釋', lines=5, interactive=False)\n" +
                    "    dropdown.change(fn=get_definition, inputs=dropdown, outputs=output_textbox)\n\n" +
                    "demo.launch()\n"
                )
                gr.Code(
                    label="app.py (Level 1 範例)",
                    value=standalone_basic_code,
                    language="python",
                    interactive=False
                )
            
            with gr.Accordion("Level 2: 進階地震地圖 App 🌍 (點我展開)", open=False):
                gr.Markdown("### 部署你自己的版本:")
                gr.Markdown(
                    """
                    <p>這次你需要上傳 <strong>2 個檔案</strong>到你自己的 Hugging Face Space:</p>
                    <h4>1. <code>requirements.txt</code></h4>
                    <p>建立這個檔案並貼上下面的內容,告訴 Hugging Face 你需要 `obspy` 和 `folium`。</p>
                    """
                )
                gr.Code(
                    label="requirements.txt (必備)",
                    value=TUTORIAL_REQUIREMENTS,
                    interactive=False
                )
                gr.Markdown("<h4>2. <code>app.py</code></h4><p>建立 `app.py` 檔案並貼上下面的內容。</p>")
                
                # [修復] 修正教學程式碼,使其可以獨立運作
                standalone_advanced_code = (
                    "import gradio as gr\nimport folium\nfrom obspy.clients.fdsn import Client\nfrom obspy import UTCDateTime\n\n" +
                    TUTORIAL_CODE_ADVANCED +
                    "\n\nwith gr.Blocks() as demo:\n" +
                    "    gr.Markdown('# 🚀 我的進階地科 App (Level 2)')\n" +
                    "    gr.Markdown('使用 ObsPy 從 IRIS FDSN 搜尋即時地震資料,並用 Folium 繪製互動式地圖。')\n" +
                    "    with gr.Row():\n" +
                    "        mag_slider = gr.Slider(minimum=4.0, maximum=8.0, value=5.0, step=0.1, label='最小地震規模 (M)')\n" +
                    "        days_slider = gr.Slider(minimum=1, maximum=30, value=7, step=1, label='搜尋天數 (過去幾天)')\n" +
                    "    search_button = gr.Button('搜尋並繪製地圖 🌍', variant='primary')\n" +
                    "    map_output = gr.HTML(label='地震分佈圖')\n" +
                    "    search_button.click(fn=fetch_and_plot_events, inputs=[mag_slider, days_slider], outputs=map_output)\n\n" +
                    "demo.launch()\n"
                )
                gr.Code(
                    label="app.py (Level 2 範例)",
                    value=standalone_advanced_code,
                    language="python",
                    interactive=False
                )

        # --- Tab 5: 關於 Spaces ---
        with gr.TabItem("ℹ️ 關於 Spaces", id=4):
            with gr.Row(equal_height=True, variant="panel"):
                with gr.Column():
                    gr.Markdown(
                        """
                        ### 什麼是 Hugging Face Spaces?
                        Hugging Face Spaces 是一個由 Hugging Face 提供的免費平台,讓開發者和創作者可以輕鬆地建立、分享和展示他們的機器學習 Demo 或網頁應用程式。
                        
                        它就像是 AI 應用的「YouTube」!您可以使用 Gradio、Streamlit 等 Python 套件快速打造互動介面,或者部署靜態網頁 (HTML/CSS/JS),甚至運行 Docker 容器。
                        """
                    )
                with gr.Column():
                    gr.Markdown(
                        """
                        ### 🚀 如何部署 Gradio 應用程式?
                        **這本身就是一個 Gradio 應用程式!** 部署它非常簡單:
                        1. 在 Hugging Face 建立一個 new Space。
                        2. C選擇 SDK:選擇 **"Gradio"** (這也是預設選項)。
                        3. 建立 Space 後,點選 "Files and versions" 分頁。
                        4. 建立一個名為 `app.py` 的檔案。
                        5. **將這份 Python 程式碼**貼到 `app.py` 中並提交。
                        6. (進階) 如果你的 App 需要 `obspy` 這樣的額外函式庫,請再建立一個 `requirements.txt` 檔案並列出它們。
                        7. **完成!** 您的Gradio應用程式將自動建置並上線。
                        """
                    )

    # 頁腳
    gr.Markdown(
        """
        <hr>
        <p id="footer">此頁面由 Gemini 協助生成,展示 "1022 中央地科演講" 中的 Hugging Face 專案。</p>
        """
    )
    
    # 7. 綁定按鈕點擊事件
    recommend_btn.click(
        fn=recommend_project,
        inputs=None,
        outputs=recommend_out
    )

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