DawnC commited on
Commit
efa5992
·
verified ·
1 Parent(s): 910bfed

Upload 2 files

Browse files
Files changed (2) hide show
  1. history_manager.py +273 -65
  2. search_history.py +120 -105
history_manager.py CHANGED
@@ -6,13 +6,13 @@ import traceback
6
 
7
  class UserHistoryManager:
8
  def __init__(self):
9
- """初始化歷史紀錄管理器"""
10
  self.history_file = "user_history.json"
11
  print(f"Initializing UserHistoryManager with file: {os.path.abspath(self.history_file)}")
12
  self._init_file()
13
 
14
  def _init_file(self):
15
- """初始化JSON檔案"""
16
  try:
17
  if not os.path.exists(self.history_file):
18
  print(f"Creating new history file: {self.history_file}")
@@ -20,102 +20,155 @@ class UserHistoryManager:
20
  json.dump([], f)
21
  else:
22
  print(f"History file exists: {self.history_file}")
23
- with open(self.history_file, 'r', encoding='utf-8') as f:
24
- data = json.load(f)
25
- print(f"Current history entries: {len(data)}")
 
 
 
 
26
  except Exception as e:
27
  print(f"Error in _init_file: {str(e)}")
28
  print(traceback.format_exc())
29
 
30
- def save_history(self, user_preferences: dict = None, results: list = None, search_type: str = "criteria", description: str = None) -> bool:
31
  """
32
- 保存搜尋歷史,確保結果資料被完整保存
33
-
34
- Args:
35
- user_preferences: 使用者的搜尋偏好設定
36
- results: 品種推薦結果列表
37
- search_type: 搜尋類型
38
- description: 搜尋描述
39
-
40
- Returns:
41
- bool: 保存是否成功
42
  """
43
  try:
44
- # 初始化時區和當前時間
45
  taipei_tz = pytz.timezone('Asia/Taipei')
46
  current_time = datetime.now(taipei_tz)
47
-
48
- # 創建歷史紀錄項目並包含時間戳記
49
  history_entry = {
50
  "timestamp": current_time.strftime("%Y-%m-%d %H:%M:%S"),
51
  "search_type": search_type
52
  }
53
-
54
- # 確保結果資料的完整性
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  if results and isinstance(results, list):
56
  processed_results = []
57
- for result in results[:15]:
58
- # 確保每個結果都包含必要的欄位
59
- if isinstance(result, dict):
60
- processed_result = {
61
- 'breed': result.get('breed', 'Unknown'),
62
- 'overall_score': result.get('overall_score', 0),
63
- 'rank': result.get('rank', 0),
64
- 'size': result.get('size', 'Unknown')
65
- }
66
- processed_results.append(processed_result)
 
 
67
  history_entry["results"] = processed_results
68
-
69
- # 加入使用者偏好設定(如果有的話)
70
- if user_preferences:
71
- formatted_preferences = {
72
- 'living_space': user_preferences.get('living_space'),
73
- 'exercise_time': user_preferences.get('exercise_time'),
74
- 'grooming_commitment': user_preferences.get('grooming_commitment'),
75
- 'experience_level': user_preferences.get('experience_level'),
76
- 'has_children': user_preferences.get('has_children'),
77
- 'noise_tolerance': user_preferences.get('noise_tolerance'),
78
- 'size_preference': user_preferences.get('size_preference')
79
  }
80
- history_entry["preferences"] = user_preferences
81
-
82
- # 讀取現有歷史
83
- with open(self.history_file, 'r', encoding='utf-8') as f:
84
- history = json.load(f)
85
-
86
- # 加入新紀錄並保持歷史限制
 
 
 
 
 
 
 
87
  history.append(history_entry)
88
- if len(history) > 20: # 保留最近 20
89
- history = history[-20:]
90
-
91
- # 儲存更新後的歷史
92
- with open(self.history_file, 'w', encoding='utf-8') as f:
93
- json.dump(history, f, ensure_ascii=False, indent=2)
94
-
95
- print(f"Successfully saved history entry: {history_entry}")
 
 
 
 
 
96
  return True
97
-
98
  except Exception as e:
99
  print(f"Error saving history: {str(e)}")
100
  print(traceback.format_exc())
101
  return False
102
 
103
-
104
  def get_history(self) -> list:
105
- """獲取搜尋歷史"""
106
  try:
107
  print("Attempting to read history") # Debug
108
- with open(self.history_file, 'r', encoding='utf-8') as f:
109
- data = json.load(f)
110
- print(f"Read {len(data)} history entries") # Debug
111
- return data if isinstance(data, list) else []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  except Exception as e:
113
  print(f"Error reading history: {str(e)}")
114
  print(traceback.format_exc())
115
  return []
116
 
117
  def clear_all_history(self) -> bool:
118
- """清除所有歷史紀錄"""
119
  try:
120
  print("Attempting to clear all history") # Debug
121
  with open(self.history_file, 'w', encoding='utf-8') as f:
@@ -126,3 +179,158 @@ class UserHistoryManager:
126
  print(f"Error clearing history: {str(e)}")
127
  print(traceback.format_exc())
128
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  class UserHistoryManager:
8
  def __init__(self):
9
+ """Initialize history record manager"""
10
  self.history_file = "user_history.json"
11
  print(f"Initializing UserHistoryManager with file: {os.path.abspath(self.history_file)}")
12
  self._init_file()
13
 
14
  def _init_file(self):
15
+ """Initialize JSON file"""
16
  try:
17
  if not os.path.exists(self.history_file):
18
  print(f"Creating new history file: {self.history_file}")
 
20
  json.dump([], f)
21
  else:
22
  print(f"History file exists: {self.history_file}")
23
+ # Added a check for empty file before loading
24
+ if os.path.getsize(self.history_file) > 0:
25
+ with open(self.history_file, 'r', encoding='utf-8') as f:
26
+ data = json.load(f)
27
+ print(f"Current history entries: {len(data)}")
28
+ else:
29
+ print("History file is empty.")
30
  except Exception as e:
31
  print(f"Error in _init_file: {str(e)}")
32
  print(traceback.format_exc())
33
 
34
+ def save_history(self, user_preferences: dict = None, results: list = None, search_type: str = "criteria", description: str = None, user_description: str = None) -> bool:
35
  """
36
+ Save search history with complete result data
 
 
 
 
 
 
 
 
 
37
  """
38
  try:
 
39
  taipei_tz = pytz.timezone('Asia/Taipei')
40
  current_time = datetime.now(taipei_tz)
41
+
 
42
  history_entry = {
43
  "timestamp": current_time.strftime("%Y-%m-%d %H:%M:%S"),
44
  "search_type": search_type
45
  }
46
+
47
+ description_text = user_description or description
48
+ if search_type == "description" and description_text:
49
+ history_entry["user_description"] = description_text[:200] + "..." if len(description_text) > 200 else description_text
50
+
51
+ def _to_float(x, default=0.0):
52
+ try:
53
+ return float(x)
54
+ except Exception:
55
+ return default
56
+
57
+ def _to_int(x, default=0):
58
+ try:
59
+ return int(x)
60
+ except Exception:
61
+ return default
62
+
63
  if results and isinstance(results, list):
64
  processed_results = []
65
+ for i, r in enumerate(results[:15], start=1):
66
+ processed_results.append({
67
+ "breed": str(r.get("breed", "Unknown")),
68
+ "rank": _to_int(r.get("rank", i)),
69
+ # 先拿 overall_score,沒有就退 final_score,都轉成 float
70
+ "overall_score": _to_float(r.get("overall_score", r.get("final_score", 0))),
71
+ # 描述搜尋常見附加分,也一併安全轉型
72
+ "semantic_score": _to_float(r.get("semantic_score", 0)),
73
+ "comparative_bonus": _to_float(r.get("comparative_bonus", 0)),
74
+ "lifestyle_bonus": _to_float(r.get("lifestyle_bonus", 0)),
75
+ "size": str(r.get("size", "Unknown")),
76
+ })
77
  history_entry["results"] = processed_results
78
+
79
+ if user_preferences:
80
+ history_entry["preferences"] = {
81
+ 'living_space': user_preferences.get('living_space'),
82
+ 'exercise_time': user_preferences.get('exercise_time'),
83
+ 'grooming_commitment': user_preferences.get('grooming_commitment'),
84
+ 'experience_level': user_preferences.get('experience_level'),
85
+ 'has_children': user_preferences.get('has_children'),
86
+ 'noise_tolerance': user_preferences.get('noise_tolerance'),
87
+ 'size_preference': user_preferences.get('size_preference')
 
88
  }
89
+
90
+ try:
91
+ history = []
92
+ if os.path.exists(self.history_file) and os.path.getsize(self.history_file) > 0:
93
+ with open(self.history_file, 'r', encoding='utf-8') as f:
94
+ history = json.load(f)
95
+ except json.JSONDecodeError as e:
96
+ print(f"JSON decode error when reading history: {str(e)}")
97
+ backup_file = f"{self.history_file}.backup.{int(datetime.now().timestamp())}"
98
+ if os.path.exists(self.history_file):
99
+ os.rename(self.history_file, backup_file)
100
+ print(f"Backed up corrupted file to {backup_file}")
101
+ history = []
102
+
103
  history.append(history_entry)
104
+ history = history[-20:] # Keep recent 20 entries
105
+
106
+ temp_file = f"{self.history_file}.tmp"
107
+ try:
108
+ with open(temp_file, 'w', encoding='utf-8') as f:
109
+ json.dump(history, f, ensure_ascii=False, indent=2)
110
+ os.rename(temp_file, self.history_file)
111
+ except Exception as e:
112
+ if os.path.exists(temp_file):
113
+ os.remove(temp_file)
114
+ raise
115
+
116
+ print(f"Successfully saved history entry for {search_type} search.")
117
  return True
118
+
119
  except Exception as e:
120
  print(f"Error saving history: {str(e)}")
121
  print(traceback.format_exc())
122
  return False
123
 
124
+ # get_history, clear_all_history, and format_history_for_display methods remain the same as you provided
125
  def get_history(self) -> list:
126
+ """Get search history"""
127
  try:
128
  print("Attempting to read history") # Debug
129
+
130
+ # Check if file exists and is not empty
131
+ if not os.path.exists(self.history_file):
132
+ print("History file does not exist, creating empty file")
133
+ with open(self.history_file, 'w', encoding='utf-8') as f:
134
+ json.dump([], f)
135
+ return []
136
+
137
+ # Check file size
138
+ if os.path.getsize(self.history_file) == 0:
139
+ print("History file is empty, initializing with empty array")
140
+ with open(self.history_file, 'w', encoding='utf-8') as f:
141
+ json.dump([], f)
142
+ return []
143
+
144
+ # Try to read with error recovery
145
+ try:
146
+ with open(self.history_file, 'r', encoding='utf-8') as f:
147
+ content = f.read().strip()
148
+ if not content:
149
+ print("File content is empty, returning empty list")
150
+ return []
151
+ data = json.loads(content)
152
+ print(f"Read {len(data)} history entries") # Debug
153
+ return data if isinstance(data, list) else []
154
+ except json.JSONDecodeError as je:
155
+ print(f"JSON decode error: {str(je)}")
156
+ print(f"Corrupted content near position {je.pos}")
157
+ # Backup corrupted file and create new one
158
+ backup_file = f"{self.history_file}.backup"
159
+ os.rename(self.history_file, backup_file)
160
+ print(f"Backed up corrupted file to {backup_file}")
161
+ with open(self.history_file, 'w', encoding='utf-8') as f:
162
+ json.dump([], f)
163
+ return []
164
+
165
  except Exception as e:
166
  print(f"Error reading history: {str(e)}")
167
  print(traceback.format_exc())
168
  return []
169
 
170
  def clear_all_history(self) -> bool:
171
+ """Clear all history records"""
172
  try:
173
  print("Attempting to clear all history") # Debug
174
  with open(self.history_file, 'w', encoding='utf-8') as f:
 
179
  print(f"Error clearing history: {str(e)}")
180
  print(traceback.format_exc())
181
  return False
182
+
183
+ def format_history_for_display(self) -> str:
184
+ """
185
+ Format history records for HTML display
186
+
187
+ Returns:
188
+ str: Formatted HTML string
189
+ """
190
+ try:
191
+ history = self.get_history()
192
+
193
+ if not history:
194
+ return """
195
+ <div style="text-align: center; padding: 20px; color: #718096;">
196
+ <p>No search history yet</p>
197
+ </div>
198
+ """
199
+
200
+ html_parts = []
201
+ html_parts.append("""
202
+ <div style="max-height: 400px; overflow-y: auto;">
203
+ """)
204
+
205
+ for i, entry in enumerate(reversed(history)): # Latest entries first
206
+ search_type = entry.get('search_type', 'criteria')
207
+ timestamp = entry.get('timestamp', 'Unknown time')
208
+ results = entry.get('results', [])
209
+
210
+ # Set tag color based on search type
211
+ if search_type == 'description':
212
+ tag_color = "#4299e1" # Blue
213
+ tag_bg = "rgba(66, 153, 225, 0.1)"
214
+ tag_text = "Description Search"
215
+ icon = "🤖"
216
+ else:
217
+ tag_color = "#48bb78" # Green
218
+ tag_bg = "rgba(72, 187, 120, 0.1)"
219
+ tag_text = "Criteria Search"
220
+ icon = "🔍"
221
+
222
+ # Search content preview
223
+ preview_content = ""
224
+ if search_type == 'description':
225
+ user_desc = entry.get('user_description', '')
226
+ if user_desc:
227
+ preview_content = f"Description: {user_desc}"
228
+ else:
229
+ prefs = entry.get('preferences', {})
230
+ if prefs:
231
+ living = prefs.get('living_space', '')
232
+ size = prefs.get('size_preference', '')
233
+ exercise = prefs.get('exercise_time', '')
234
+ preview_content = f"Living: {living}, Size: {size}, Exercise: {exercise}min"
235
+
236
+ # Result summary
237
+ result_summary = ""
238
+ if results:
239
+ top_breeds = [r.get('breed', 'Unknown') for r in results[:3]]
240
+ result_summary = f"Recommended: {', '.join(top_breeds)}"
241
+ if len(results) > 3:
242
+ result_summary += f" and {len(results)} breeds total"
243
+
244
+ html_parts.append(f"""
245
+ <div style="
246
+ border: 1px solid #e2e8f0;
247
+ border-radius: 8px;
248
+ padding: 12px;
249
+ margin: 8px 0;
250
+ background: white;
251
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
252
+ ">
253
+ <div style="display: flex; justify-content: between; align-items: center; margin-bottom: 8px;">
254
+ <div style="
255
+ background: {tag_bg};
256
+ color: {tag_color};
257
+ padding: 4px 8px;
258
+ border-radius: 12px;
259
+ font-size: 0.8em;
260
+ font-weight: 600;
261
+ display: inline-flex;
262
+ align-items: center;
263
+ gap: 4px;
264
+ ">
265
+ {icon} {tag_text}
266
+ </div>
267
+ <div style="font-size: 0.8em; color: #718096;">
268
+ {timestamp}
269
+ </div>
270
+ </div>
271
+
272
+ {f'<div style="font-size: 0.9em; color: #4a5568; margin: 4px 0;">{preview_content}</div>' if preview_content else ''}
273
+ {f'<div style="font-size: 0.9em; color: #2d3748; font-weight: 500;">{result_summary}</div>' if result_summary else ''}
274
+ </div>
275
+ """)
276
+
277
+ html_parts.append("</div>")
278
+
279
+ return ''.join(html_parts)
280
+
281
+ except Exception as e:
282
+ print(f"Error formatting history for display: {str(e)}")
283
+ return f"""
284
+ <div style="text-align: center; padding: 20px; color: #e53e3e;">
285
+ <p>Error loading history records: {str(e)}</p>
286
+ </div>
287
+ """
288
+
289
+ def get_search_statistics(self) -> dict:
290
+ """
291
+ Get search statistics information
292
+
293
+ Returns:
294
+ dict: Statistics information
295
+ """
296
+ try:
297
+ history = self.get_history()
298
+
299
+ stats = {
300
+ 'total_searches': len(history),
301
+ 'criteria_searches': 0,
302
+ 'description_searches': 0,
303
+ 'most_searched_breeds': {},
304
+ 'search_frequency_by_day': {}
305
+ }
306
+
307
+ for entry in history:
308
+ search_type = entry.get('search_type', 'criteria')
309
+ if search_type == 'description':
310
+ stats['description_searches'] += 1
311
+ else:
312
+ stats['criteria_searches'] += 1
313
+
314
+ # Count breed search frequency
315
+ results = entry.get('results', [])
316
+ for result in results:
317
+ breed = result.get('breed', 'Unknown')
318
+ stats['most_searched_breeds'][breed] = stats['most_searched_breeds'].get(breed, 0) + 1
319
+
320
+ # Count search frequency by date
321
+ timestamp = entry.get('timestamp', '')
322
+ if timestamp:
323
+ date = timestamp.split(' ')[0]
324
+ stats['search_frequency_by_day'][date] = stats['search_frequency_by_day'].get(date, 0) + 1
325
+
326
+ return stats
327
+
328
+ except Exception as e:
329
+ print(f"Error getting search statistics: {str(e)}")
330
+ return {
331
+ 'total_searches': 0,
332
+ 'criteria_searches': 0,
333
+ 'description_searches': 0,
334
+ 'most_searched_breeds': {},
335
+ 'search_frequency_by_day': {}
336
+ }
search_history.py CHANGED
@@ -1,20 +1,20 @@
1
  import gradio as gr
2
  import traceback
3
- from typing import Optional, Dict, List
4
  from history_manager import UserHistoryManager
5
 
6
- class SearchHistoryComponent :
7
- def __init__ ( self ):
8
  """初始化搜尋歷史組件"""
9
  self.history_manager = UserHistoryManager()
10
 
11
- def format_history_html ( self, history_data: Optional [ List [ Dict ]] = None ) -> str :
12
- try :
13
- if history_data is None :
14
  history_data = self.history_manager.get_history()
15
 
16
- if not history_data:
17
- return """
18
  <div style='text-align: center; padding: 40px 20px;'>
19
  <p>No search history yet. Try making some breed recommendations!</p>
20
  </div>
@@ -22,62 +22,115 @@ class SearchHistoryComponent :
22
 
23
  html = "<div class='history-container'>"
24
 
25
- # 對歷史記錄進行反轉,最新的顯示在前面
26
- for entry in reversed (history_data):
27
- timestamp = entry.get( 'timestamp' , 'Unknown time' )
28
- search_type = entry.get( 'search_type' , 'criteria' )
29
- results = entry.get( 'results' , [])
30
-
31
- # 顯示時間戳記和搜尋類型
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  html += f"""
33
  <div class="history-entry">
34
- <div class="history-header" style="border-left: 4px solid #4299e1; padding-left: 10px;">
35
- <span class="timestamp">🕒 {timestamp} </span>
36
- <span class="search-type" style="color: #4299e1; font-weight: bold; margin-left: 10px;">
37
- Search History
 
 
 
 
 
 
 
 
 
 
 
38
  </span>
39
  </div>
40
  """
41
 
42
- # 顯示搜尋參數
43
- if search_type == "criteria" :
44
- prefs = entry.get( 'preferences' , {})
45
  html += f"""
46
  <div class="params-list" style="background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
47
  <h4 style="margin-bottom: 12px;">Search Parameters:</h4>
48
  <ul style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
49
- <li>Living Space: {prefs.get( 'living_space' , 'N/A' )} </li>
50
- <li>Exercise Time: {prefs.get( 'exercise_time' , 'N/A' )} minutes</li>
51
- <li>Grooming: {prefs.get( 'grooming_commitment' , 'N/A' )} </li>
52
- <li>Size Preference: {prefs.get( 'size_preference' , 'N/A' )} </li>
53
- <li>Experience: {prefs.get( 'experience_level' , 'N/A' )} </li>
54
- <li>Children at Home: { "Yes" if prefs.get( 'has_children' ) else "No" } </li>
55
- <li>Noise Tolerance: {prefs.get( 'noise_tolerance' , 'N/A' )} </li>
56
  </ul>
57
  </div>
58
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- # 關鍵修改:確保結果部分始終顯示
61
- if results: # 只有在有結果時才顯示結果區域
62
  html += """
63
  <div class="results-list" style="margin-top: 16px;">
64
  <h4 style="margin-bottom: 12px;">Top 15 Breed Matches:</h4>
65
  <div class="breed-list">
66
  """
67
 
68
- # 顯示每個推薦結果
69
- for i, result in enumerate (results[: 15 ], 1 ):
70
- breed = result.get( 'breed' , 'Unknown breed' )
71
- score = result.get( 'overall_score' , 0 ) # 改用overall_score
72
- if isinstance (score, ( int , float )): # 確保分數是數字
73
- score = float (score) * 100 # 轉換為百分比
 
 
 
 
 
 
 
 
 
 
74
 
75
  html += f"""
76
  <div class="breed-item" style="margin-bottom: 8px;">
77
  <div class="breed-info" style="display: flex; align-items: center; justify-content: space-between; padding: 8px; background: #f8fafc; border-radius: 6px;">
78
  <span class="breed-rank" style="background: linear-gradient(135deg, #4299e1, #48bb78); color: white; padding: 4px 10px; border-radius: 6px; font-weight: 600; min-width: 40px; text-align: center;">#{i}</span>
79
  <span class="breed-name" style="font-weight: 500; color: #2D3748; margin: 0 12px;">{breed.replace('_', ' ')}</span>
80
- <span class="breed-score" style="background: #F0FFF4; color: #48BB78; padding: 4px 8px; border-radius: 4px; font-weight: 600;">{score:.1f}%</span>
81
  </div>
82
  </div>
83
  """
@@ -87,86 +140,50 @@ class SearchHistoryComponent :
87
  </div>
88
  """
89
 
90
- html += "</div>" # 關閉history-entry div
91
 
92
- html += "</div>" # 關閉history-container div
93
  return html
94
 
95
  except Exception as e:
96
- print ( f"Error formatting history: { str (e)} " )
97
- print (traceback.format_exc())
98
- return f"""
99
  <div style='text-align: center; padding: 20px; color: #dc2626;'>
100
  Error formatting history. Please try refreshing the page.
101
- <br>Error details: { str (e)}
102
  </div>
103
  """
104
 
105
- def clear_history ( self ) -> str :
106
- """清除所有搜尋紀錄"""
107
- try :
108
  success = self.history_manager.clear_all_history()
109
- print ( f"Clear history result: {success} " )
110
  return self.format_history_html()
111
  except Exception as e:
112
- print ( f"Error in clear_history: { str (e)} " )
113
- print (traceback.format_exc())
114
- return "Error clearing history"
115
 
116
- def refresh_history ( self ) -> str :
117
- """刷新歷史記錄顯示"""
118
- try :
119
  return self.format_history_html()
120
  except Exception as e:
121
- print ( f"Error in refresh_history: { str (e)} " )
122
- return "Error refreshing history"
123
-
124
- def save_search ( self, user_preferences: Optional [ dict ] = None ,
125
- results: list = None ,
126
- search_type: str = "criteria" ,
127
- description: str = None ) -> bool :
128
- """
129
- 儲存搜尋結果到歷史記錄
130
- 這個方法負責處理搜尋結果的保存,並確保只保存前15個最相關的推薦結果。
131
- 在儲存之前,會處理結果資料確保格式正確且包含所需的所有資訊。
132
- Args:
133
- user_preferences: 使用者偏好設定(僅用於criteria搜尋)
134
- 包含所有搜尋條件如居住空間、運動時間等
135
- results: 推薦結果列表
136
- 包含所有推薦的狗品種及其評分
137
- search_type: 搜尋類型("criteria" 或"description")
138
- 用於標識搜尋方式
139
- description: 用戶輸入的描述(僅用於description搜尋)
140
- 用於自然語言搜尋時的描述文本
141
-
142
- Returns:
143
- bool: 表示保存是否成功
144
- """
145
- # 首先確保結果不為空且為列表
146
- if results and isinstance (results, list ):
147
- # 只取前15個結果
148
- processed_results = []
149
- for result in results[: 15 ]: # 限制為前15個結果
150
- # 確保每個結果都包含必要的信息
151
- if isinstance (result, dict ):
152
- processed_result = {
153
- 'breed' : result.get( 'breed' , 'Unknown' ),
154
- 'overall_score' : result.get( 'overall_score' , result.get( 'final_score' , 0 )),
155
- 'rank' : result.get( 'rank' , 0 ),
156
- 'base_score' : result.get( 'base_score' , 0 ),
157
- 'bonus_score' : result.get( 'bonus_score' , 0 ),
158
- 'scores' : result.get( 'scores' , {})
159
- }
160
- processed_results.append(processed_result)
161
- else :
162
- # 如果沒有結果,創建空列表
163
- processed_results = []
164
-
165
- # 調用history_manager 的save_history 方法保存處理過的結果
166
  return self.history_manager.save_history(
167
  user_preferences=user_preferences,
168
- results=processed_results, # 使用處理過的結果
169
- search_type= 'criteria'
 
 
170
  )
171
 
172
  def create_history_component ():
@@ -272,7 +289,7 @@ def create_history_tab ( history_component: SearchHistoryComponent ):
272
 
273
  with gr.Row():
274
  with gr.Column(scale= 4 ):
275
- history_display = gr.HTML()
276
  with gr.Row(equal_height= True ):
277
  with gr.Column(scale= 1 ):
278
  clear_history_btn = gr.Button(
@@ -287,8 +304,6 @@ def create_history_tab ( history_component: SearchHistoryComponent ):
287
  elem_classes= "custom-btn refresh-btn"
288
  )
289
 
290
- history_display.value = history_component.format_history_html()
291
-
292
  clear_history_btn.click(
293
  fn=history_component.clear_history,
294
  outputs=[history_display],
@@ -299,4 +314,4 @@ def create_history_tab ( history_component: SearchHistoryComponent ):
299
  fn=history_component.refresh_history,
300
  outputs=[history_display],
301
  api_name= "refresh_history"
302
- )
 
1
  import gradio as gr
2
  import traceback
3
+ from typing import Optional , Dict , List
4
  from history_manager import UserHistoryManager
5
 
6
+ class SearchHistoryComponent:
7
+ def __init__(self):
8
  """初始化搜尋歷史組件"""
9
  self.history_manager = UserHistoryManager()
10
 
11
+ def format_history_html(self, history_data: Optional[List[Dict]] = None) -> str:
12
+ try:
13
+ if history_data is None:
14
  history_data = self.history_manager.get_history()
15
 
16
+ if not history_data:
17
+ return """
18
  <div style='text-align: center; padding: 40px 20px;'>
19
  <p>No search history yet. Try making some breed recommendations!</p>
20
  </div>
 
22
 
23
  html = "<div class='history-container'>"
24
 
25
+ # 最新的顯示在前面
26
+ for entry in reversed(history_data):
27
+ timestamp = entry.get('timestamp', 'Unknown time')
28
+ search_type = entry.get('search_type', 'criteria')
29
+ results = entry.get('results', [])
30
+
31
+ # 標籤樣式
32
+ if search_type == "description":
33
+ border_color = "#4299e1"
34
+ tag_color = "#4299e1"
35
+ tag_bg = "rgba(66, 153, 225, 0.1)"
36
+ tag_text = "Description Search"
37
+ icon = "🤖"
38
+ else:
39
+ border_color = "#48bb78"
40
+ tag_color = "#48bb78"
41
+ tag_bg = "rgba(72, 187, 120, 0.1)"
42
+ tag_text = "Criteria Search"
43
+ icon = "🔍"
44
+
45
+ # header
46
  html += f"""
47
  <div class="history-entry">
48
+ <div class="history-header" style="border-left: 4px solid {border_color}; padding-left: 10px;">
49
+ <span class="timestamp">🕒 {timestamp}</span>
50
+ <span class="search-type" style="
51
+ background: {tag_bg};
52
+ color: {tag_color};
53
+ padding: 4px 8px;
54
+ border-radius: 12px;
55
+ font-size: 0.8em;
56
+ font-weight: 600;
57
+ margin-left: 10px;
58
+ display: inline-flex;
59
+ align-items: center;
60
+ gap: 4px;
61
+ ">
62
+ {icon} {tag_text}
63
  </span>
64
  </div>
65
  """
66
 
67
+ # 參數/描述
68
+ if search_type == "criteria":
69
+ prefs = entry.get('preferences', {})
70
  html += f"""
71
  <div class="params-list" style="background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
72
  <h4 style="margin-bottom: 12px;">Search Parameters:</h4>
73
  <ul style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
74
+ <li>Living Space: {prefs.get('living_space', 'N/A')}</li>
75
+ <li>Exercise Time: {prefs.get('exercise_time', 'N/A')} minutes</li>
76
+ <li>Grooming: {prefs.get('grooming_commitment', 'N/A')}</li>
77
+ <li>Size Preference: {prefs.get('size_preference', 'N/A')}</li>
78
+ <li>Experience: {prefs.get('experience_level', 'N/A')}</li>
79
+ <li>Children at Home: {"Yes" if prefs.get('has_children') else "No"}</li>
80
+ <li>Noise Tolerance: {prefs.get('noise_tolerance', 'N/A')}</li>
81
  </ul>
82
  </div>
83
  """
84
+ elif search_type == "description":
85
+ description = entry.get('user_description', '')
86
+ html += f"""
87
+ <div class="params-list" style="background: #f0f8ff; padding: 16px; border-radius: 8px; margin-bottom: 16px; border: 1px solid rgba(66, 153, 225, 0.2);">
88
+ <h4 style="margin-bottom: 12px; color: #4299e1;">User Description:</h4>
89
+ <div style="
90
+ background: white;
91
+ padding: 12px;
92
+ border-radius: 6px;
93
+ border-left: 3px solid #4299e1;
94
+ font-style: italic;
95
+ color: #2d3748;
96
+ line-height: 1.5;
97
+ ">
98
+ "{description}"
99
+ </div>
100
+ </div>
101
+ """
102
 
103
+ # 結果區
104
+ if results:
105
  html += """
106
  <div class="results-list" style="margin-top: 16px;">
107
  <h4 style="margin-bottom: 12px;">Top 15 Breed Matches:</h4>
108
  <div class="breed-list">
109
  """
110
 
111
+ for i, result in enumerate(results[:15], 1):
112
+ breed = result.get('breed', 'Unknown breed')
113
+
114
+ # 分數回退順序:final_score overall_score semantic_score
115
+ score_val = (
116
+ result.get('final_score', None)
117
+ if result.get('final_score', None) not in [None, ""]
118
+ else result.get('overall_score', None)
119
+ )
120
+ if score_val in [None, ""]:
121
+ score_val = result.get('semantic_score', 0)
122
+
123
+ try:
124
+ score_pct = float(score_val) * 100.0
125
+ except Exception:
126
+ score_pct = 0.0
127
 
128
  html += f"""
129
  <div class="breed-item" style="margin-bottom: 8px;">
130
  <div class="breed-info" style="display: flex; align-items: center; justify-content: space-between; padding: 8px; background: #f8fafc; border-radius: 6px;">
131
  <span class="breed-rank" style="background: linear-gradient(135deg, #4299e1, #48bb78); color: white; padding: 4px 10px; border-radius: 6px; font-weight: 600; min-width: 40px; text-align: center;">#{i}</span>
132
  <span class="breed-name" style="font-weight: 500; color: #2D3748; margin: 0 12px;">{breed.replace('_', ' ')}</span>
133
+ <span class="breed-score" style="background: #F0FFF4; color: #48BB78; padding: 4px 8px; border-radius: 4px; font-weight: 600;">{score_pct:.1f}%</span>
134
  </div>
135
  </div>
136
  """
 
140
  </div>
141
  """
142
 
143
+ html += "</div>" # 關閉 .history-entry
144
 
145
+ html += "</div>" # 關閉 .history-container
146
  return html
147
 
148
  except Exception as e:
149
+ print(f"Error formatting history: {str(e)}")
150
+ print(traceback.format_exc())
151
+ return f"""
152
  <div style='text-align: center; padding: 20px; color: #dc2626;'>
153
  Error formatting history. Please try refreshing the page.
154
+ <br>Error details: {str(e)}
155
  </div>
156
  """
157
 
158
+ def clear_history(self) -> str:
159
+ try:
 
160
  success = self.history_manager.clear_all_history()
161
+ print(f"Clear history result: {success}")
162
  return self.format_history_html()
163
  except Exception as e:
164
+ print(f"Error in clear_history: {str(e)}")
165
+ print(traceback.format_exc())
166
+ return "Error clearing history"
167
 
168
+ def refresh_history(self) -> str:
169
+ try:
 
170
  return self.format_history_html()
171
  except Exception as e:
172
+ print(f"Error in refresh_history: {str(e)}")
173
+ return "Error refreshing history"
174
+
175
+ def save_search(self,
176
+ user_preferences: Optional[dict] = None,
177
+ results: list = None,
178
+ search_type: str = "criteria",
179
+ description: str = None) -> bool:
180
+ """參數原樣透傳給 history_manager"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  return self.history_manager.save_history(
182
  user_preferences=user_preferences,
183
+ results=results,
184
+ search_type=search_type,
185
+ description=description,
186
+ user_description=description
187
  )
188
 
189
  def create_history_component ():
 
289
 
290
  with gr.Row():
291
  with gr.Column(scale= 4 ):
292
+ history_display = gr.HTML(value=history_component.format_history_html())
293
  with gr.Row(equal_height= True ):
294
  with gr.Column(scale= 1 ):
295
  clear_history_btn = gr.Button(
 
304
  elem_classes= "custom-btn refresh-btn"
305
  )
306
 
 
 
307
  clear_history_btn.click(
308
  fn=history_component.clear_history,
309
  outputs=[history_display],
 
314
  fn=history_component.refresh_history,
315
  outputs=[history_display],
316
  api_name= "refresh_history"
317
+ )