DeepLearning101 commited on
Commit
c9e287a
·
verified ·
1 Parent(s): 3df9932

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +323 -0
app.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ import os
4
+ import pandas as pd
5
+ from dotenv import load_dotenv
6
+ from services import GeminiService
7
+
8
+ # Load Env
9
+ load_dotenv()
10
+ SAVE_FILE = os.getenv("SAVE_FILE_NAME", "saved_professors.json")
11
+
12
+ # Init Service
13
+ try:
14
+ gemini_service = GeminiService()
15
+ except Exception as e:
16
+ print(f"Service Error: {e}")
17
+ gemini_service = None
18
+
19
+ # --- Helper Functions ---
20
+
21
+ def get_key(p):
22
+ return f"{p['name']}-{p['university']}"
23
+
24
+ def load_data():
25
+ if os.path.exists(SAVE_FILE):
26
+ try:
27
+ with open(SAVE_FILE, 'r', encoding='utf-8') as f:
28
+ return json.load(f)
29
+ except:
30
+ return []
31
+ return []
32
+
33
+ def save_data(data):
34
+ try:
35
+ with open(SAVE_FILE, 'w', encoding='utf-8') as f:
36
+ json.dump(data, f, ensure_ascii=False, indent=2)
37
+ except Exception as e:
38
+ print(f"Save Error: {e}")
39
+
40
+ # 將教授列表轉換為 Pandas DataFrame 以供顯示
41
+ def format_df(prof_list):
42
+ if not prof_list:
43
+ return pd.DataFrame(columns=["狀態", "姓名", "大學", "系所", "標籤"])
44
+
45
+ data = []
46
+ for p in prof_list:
47
+ status_map = {'match': '✅', 'mismatch': '❌', 'pending': '❓'}
48
+ status_icon = status_map.get(p.get('status'), '')
49
+ has_detail = "📄" if p.get('details') else ""
50
+
51
+ tags = ", ".join(p.get('tags', []))
52
+
53
+ data.append([
54
+ f"{status_icon} {has_detail}",
55
+ p['name'],
56
+ p['university'],
57
+ p['department'],
58
+ tags
59
+ ])
60
+ return pd.DataFrame(data, columns=["狀態", "姓名", "大學", "系所", "標籤"])
61
+
62
+ # --- Event Handlers ---
63
+
64
+ def search_professors(query, current_saved):
65
+ if not query: return gr.update(), current_saved, gr.update()
66
+
67
+ try:
68
+ results = gemini_service.search_professors(query)
69
+ # 搜尋結果不存檔,只暫存在 State 中
70
+ # 但為了顯示,我們需要跟 saved 做比對 (merge logic)
71
+ return format_df(results), results, gr.update(visible=True) # Show Load More
72
+ except Exception as e:
73
+ raise gr.Error(f"搜尋失敗: {e}")
74
+
75
+ def load_more(query, current_search_results):
76
+ if not query: return gr.update(), current_search_results
77
+
78
+ current_names = [p['name'] for p in current_search_results]
79
+ try:
80
+ new_results = gemini_service.search_professors(query, exclude_names=current_names)
81
+
82
+ # 去重
83
+ existing_keys = set(get_key(p) for p in current_search_results)
84
+ for p in new_results:
85
+ if get_key(p) not in existing_keys:
86
+ current_search_results.append(p)
87
+
88
+ return format_df(current_search_results), current_search_results
89
+ except Exception as e:
90
+ raise gr.Error(f"載入失敗: {e}")
91
+
92
+ def select_professor_from_df(evt: gr.SelectData, search_results, saved_data):
93
+ # evt.index[0] 是被點擊的行數
94
+ index = evt.index[0]
95
+
96
+ # 判斷目前是在看「搜尋結果」還是「追蹤清單」
97
+ # 這裡簡化邏輯:Gradio 比較難做 View Switcher,我們假設列表是混合顯示或單一顯示
98
+ # 為了簡單,我們這裡假設使用者點的是目前的顯示列表
99
+
100
+ # 我們需要知道目前顯示的是哪一份資料。
101
+ # 簡單做法:搜尋後,search_results 會更新。
102
+ # 我們先假設使用者是在點擊 search_results (如果沒搜尋,就顯示 saved)
103
+
104
+ target_list = search_results if search_results else saved_data
105
+ if index >= len(target_list): return gr.update(), gr.update(), gr.update(), None, None
106
+
107
+ prof = target_list[index]
108
+
109
+ # 優先找已存檔的詳細資料
110
+ key = get_key(prof)
111
+ saved_prof = next((p for p in saved_data if get_key(p) == key), None)
112
+ current_prof = saved_prof if saved_prof else prof
113
+
114
+ # 準備回傳
115
+ details_md = ""
116
+ chat_history = []
117
+
118
+ # 檢查是否有 Details
119
+ if current_prof.get('details') and len(current_prof.get('details')) > 10:
120
+ details_md = current_prof['details']
121
+ if not saved_prof:
122
+ saved_data.insert(0, current_prof)
123
+ save_data(saved_data)
124
+ else:
125
+ # 需要 Call API
126
+ gr.Info(f"正在調查 {current_prof['name']}...")
127
+ try:
128
+ res = gemini_service.get_professor_details(current_prof)
129
+ current_prof['details'] = res['text']
130
+ current_prof['sources'] = res['sources']
131
+ details_md = res['text']
132
+
133
+ # 更新存檔
134
+ if saved_prof:
135
+ saved_prof.update(current_prof)
136
+ else:
137
+ saved_data.insert(0, current_prof)
138
+ save_data(saved_data)
139
+ except Exception as e:
140
+ raise gr.Error(f"調查失敗: {e}")
141
+
142
+ # 格式化 Sources
143
+ if current_prof.get('sources'):
144
+ details_md += "\n\n### 📚 參考來源\n"
145
+ for s in current_prof['sources']:
146
+ details_md += f"- [{s['title']}]({s['uri']})\n"
147
+
148
+ return (
149
+ gr.update(visible=True), # Detail Column
150
+ details_md,
151
+ [], # Reset Chat
152
+ current_prof, # Update selected state
153
+ saved_data # Update saved state
154
+ )
155
+
156
+ def chat_response(history, message, selected_prof):
157
+ if not selected_prof: return history, ""
158
+
159
+ context = selected_prof.get('details', '')
160
+ if not context: return history, ""
161
+
162
+ # 轉換 history 給 service 用
163
+ # Gradio history is [[user, bot], [user, bot]]
164
+ service_history = []
165
+ for h in history:
166
+ service_history.append({"role": "user", "content": h[0]})
167
+ if h[1]:
168
+ service_history.append({"role": "model", "content": h[1]})
169
+
170
+ try:
171
+ reply = gemini_service.chat_with_ai(service_history, message, context)
172
+ history.append((message, reply))
173
+ except Exception as e:
174
+ history.append((message, f"Error: {e}"))
175
+
176
+ return history, ""
177
+
178
+ def update_status(status, selected_prof, saved_data):
179
+ if not selected_prof: return gr.update(), saved_data
180
+
181
+ selected_prof['status'] = status if selected_prof.get('status') != status else None
182
+
183
+ # Update in saved list
184
+ key = get_key(selected_prof)
185
+ for i, p in enumerate(saved_data):
186
+ if get_key(p) == key:
187
+ saved_data[i] = selected_prof
188
+ break
189
+ save_data(saved_data)
190
+
191
+ return gr.Info(f"狀態已更新: {status}"), saved_data
192
+
193
+ def remove_prof(selected_prof, saved_data):
194
+ if not selected_prof: return gr.update(), gr.update(value=None), saved_data, gr.update(visible=False)
195
+
196
+ key = get_key(selected_prof)
197
+ new_saved = [p for p in saved_data if get_key(p) != key]
198
+ save_data(new_saved)
199
+
200
+ return (
201
+ gr.Info("已移除"),
202
+ format_df(new_saved), # Update DF
203
+ new_saved,
204
+ gr.update(visible=False) # Hide Details
205
+ )
206
+
207
+ def toggle_view(mode, search_res, saved_data):
208
+ if mode == "搜尋結果":
209
+ return format_df(search_res), gr.update(visible=True)
210
+ else:
211
+ return format_df(saved_data), gr.update(visible=False) # Hide load more
212
+
213
+ # --- UI Layout ---
214
+
215
+ with gr.Blocks(title="開箱教授去哪兒?", theme=gr.themes.Soft()) as demo:
216
+
217
+ # State
218
+ saved_state = gr.State(load_data())
219
+ search_res_state = gr.State([])
220
+ selected_prof_state = gr.State(None)
221
+
222
+ gr.Markdown("# Prof.404 🎓 開箱教授去哪兒? (Gradio Edition)")
223
+
224
+ with gr.Row():
225
+ search_input = gr.Textbox(label="搜尋研究領域", placeholder="例如: LLM, 量子計算...", scale=4)
226
+ search_btn = gr.Button("🔍 搜尋", variant="primary", scale=1)
227
+
228
+ with gr.Row():
229
+ view_radio = gr.Radio(["搜尋結果", "追蹤清單"], label="顯示模式", value="搜尋結果")
230
+
231
+ with gr.Row():
232
+ # Left: List
233
+ with gr.Column(scale=1):
234
+ prof_df = gr.Dataframe(
235
+ headers=["狀態", "姓名", "大學", "系所", "標籤"],
236
+ datatype=["str", "str", "str", "str", "str"],
237
+ interactive=False,
238
+ label="教授列表 (點擊查看詳情)"
239
+ )
240
+ load_more_btn = gr.Button("載入更多", visible=False)
241
+
242
+ # Right: Details
243
+ with gr.Column(scale=2, visible=False) as details_col:
244
+ detail_md = gr.Markdown("詳細資料...")
245
+
246
+ with gr.Row():
247
+ btn_match = gr.Button("✅ 符合")
248
+ btn_mismatch = gr.Button("❌ 不符")
249
+ btn_pending = gr.Button("❓ 待觀察")
250
+ btn_remove = gr.Button("🗑️ 移除", variant="stop")
251
+
252
+ gr.Markdown("### 💬 AI 助手")
253
+ chatbot = gr.Chatbot(height=300)
254
+ msg = gr.Textbox(label="提問")
255
+ send_btn = gr.Button("送出")
256
+
257
+ # --- Wiring ---
258
+
259
+ # Search
260
+ search_btn.click(
261
+ search_professors,
262
+ inputs=[search_input, saved_state],
263
+ outputs=[prof_df, search_res_state, load_more_btn]
264
+ )
265
+
266
+ # Load More
267
+ load_more_btn.click(
268
+ load_more,
269
+ inputs=[search_input, search_res_state],
270
+ outputs=[prof_df, search_res_state]
271
+ )
272
+
273
+ # View Toggle
274
+ view_radio.change(
275
+ toggle_view,
276
+ inputs=[view_radio, search_res_state, saved_state],
277
+ outputs=[prof_df, load_more_btn]
278
+ )
279
+
280
+ # Select Row
281
+ prof_df.select(
282
+ select_professor_from_df,
283
+ inputs=[search_res_state, saved_state],
284
+ outputs=[details_col, detail_md, chatbot, selected_prof_state, saved_state]
285
+ )
286
+
287
+ # Chat
288
+ send_btn.click(
289
+ chat_response,
290
+ inputs=[chatbot, msg, selected_prof_state],
291
+ outputs=[chatbot, msg]
292
+ )
293
+ msg.submit(
294
+ chat_response,
295
+ inputs=[chatbot, msg, selected_prof_state],
296
+ outputs=[chatbot, msg]
297
+ )
298
+
299
+ # Status Buttons
300
+ # Note: clicking status needs to refresh the dataframe to show the icon update
301
+ def refresh_current_view(mode, search_r, saved_d):
302
+ return toggle_view(mode, search_r, saved_d)[0] # return only df
303
+
304
+ for btn, status in [(btn_match, 'match'), (btn_mismatch, 'mismatch'), (btn_pending, 'pending')]:
305
+ btn.click(
306
+ update_status,
307
+ inputs=[gr.State(status), selected_prof_state, saved_state],
308
+ outputs=[gr.State(None), saved_state] # Info is side effect
309
+ ).then(
310
+ refresh_current_view,
311
+ inputs=[view_radio, search_res_state, saved_state],
312
+ outputs=[prof_df]
313
+ )
314
+
315
+ # Remove
316
+ btn_remove.click(
317
+ remove_prof,
318
+ inputs=[selected_prof_state, saved_state],
319
+ outputs=[gr.State(None), prof_df, saved_state, details_col]
320
+ )
321
+
322
+ if __name__ == "__main__":
323
+ demo.launch()