DawnC commited on
Commit
ee184bc
1 Parent(s): 729d864

Upload 6 files

Browse files
breed_comparison.py ADDED
@@ -0,0 +1,559 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ import sqlite3
4
+ from dog_database import get_dog_description
5
+ from breed_health_info import breed_health_info
6
+ from breed_noise_info import breed_noise_info
7
+
8
+ def create_comparison_tab(dog_breeds, get_dog_description, breed_noise_info, breed_health_info):
9
+ with gr.TabItem("Breed Comparison"):
10
+ gr.HTML("""
11
+ <div style='
12
+ text-align: center;
13
+ padding: 20px 0;
14
+ margin: 15px 0;
15
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
16
+ border-radius: 10px;
17
+ '>
18
+ <p style='
19
+ font-size: 1.2em;
20
+ margin: 0;
21
+ padding: 0 20px;
22
+ line-height: 1.5;
23
+ background: linear-gradient(90deg, #4299e1, #48bb78);
24
+ -webkit-background-clip: text;
25
+ -webkit-text-fill-color: transparent;
26
+ font-weight: 600;
27
+ '>
28
+ Select two dog breeds to compare their characteristics and care requirements.
29
+ </p>
30
+ </div>
31
+ """)
32
+
33
+ with gr.Row():
34
+ breed1_dropdown = gr.Dropdown(
35
+ choices=dog_breeds,
36
+ label="Select First Breed",
37
+ value="Golden_Retriever"
38
+ )
39
+ breed2_dropdown = gr.Dropdown(
40
+ choices=dog_breeds,
41
+ label="Select Second Breed",
42
+ value="Border_Collie"
43
+ )
44
+
45
+ compare_btn = gr.Button("Compare Breeds")
46
+ comparison_output = gr.HTML(label="Comparison Results")
47
+
48
+ def format_noise_data(notes):
49
+ characteristics = []
50
+ triggers = []
51
+ noise_level = "Moderate" # 預設值
52
+
53
+ if isinstance(notes, str):
54
+ lines = notes.strip().split('\n')
55
+ section = ""
56
+ for line in lines:
57
+ line = line.strip()
58
+ if "Typical noise characteristics:" in line:
59
+ section = "characteristics"
60
+ elif "Barking triggers:" in line:
61
+ section = "triggers"
62
+ elif "Noise level:" in line:
63
+ noise_level = line.split(':')[1].strip()
64
+ elif line.startswith('•'):
65
+ if section == "characteristics":
66
+ characteristics.append(line[1:].strip())
67
+ elif section == "triggers":
68
+ triggers.append(line[1:].strip())
69
+
70
+ return {
71
+ 'characteristics': characteristics,
72
+ 'triggers': triggers,
73
+ 'noise_level': noise_level
74
+ }
75
+
76
+ def format_health_data(notes):
77
+ considerations = []
78
+ screenings = []
79
+
80
+ if isinstance(notes, str):
81
+ lines = notes.strip().split('\n')
82
+ current_section = None
83
+
84
+ for line in lines:
85
+ line = line.strip()
86
+ # 修正字串比對
87
+ if "Common breed-specific health considerations" in line:
88
+ current_section = "considerations"
89
+ elif "Recommended health screenings:" in line:
90
+ current_section = "screenings"
91
+ elif line.startswith('•'):
92
+ item = line[1:].strip()
93
+ if current_section == "considerations":
94
+ considerations.append(item)
95
+ elif current_section == "screenings":
96
+ screenings.append(item)
97
+
98
+ # 只有當真的沒有資料時才返回 "Information not available"
99
+ if not considerations and not screenings:
100
+ return {
101
+ 'considerations': ["Information not available"],
102
+ 'screenings': ["Information not available"]
103
+ }
104
+
105
+ return {
106
+ 'considerations': considerations,
107
+ 'screenings': screenings
108
+ }
109
+
110
+ def get_comparison_styles():
111
+ return """
112
+ .comparison-container {
113
+ display: grid;
114
+ grid-template-columns: 1fr 1fr;
115
+ gap: 24px;
116
+ padding: 20px;
117
+ }
118
+
119
+ .breed-column {
120
+ background: white;
121
+ border-radius: 10px;
122
+ padding: 24px;
123
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
124
+ }
125
+
126
+ .section-title {
127
+ font-size: 24px;
128
+ color: #2D3748;
129
+ margin-bottom: 20px;
130
+ padding-bottom: 10px;
131
+ border-bottom: 2px solid #E2E8F0;
132
+ }
133
+
134
+ .info-section {
135
+ display: grid;
136
+ grid-template-columns: repeat(2, 1fr);
137
+ gap: 16px;
138
+ margin-bottom: 24px;
139
+ }
140
+
141
+ .info-item {
142
+ position: relative;
143
+ background: #F8FAFC;
144
+ padding: 16px;
145
+ border-radius: 8px;
146
+ border: 1px solid #E2E8F0;
147
+ }
148
+
149
+ .info-label {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 8px;
153
+ color: #4A5568;
154
+ font-size: 0.9em;
155
+ margin-bottom: 4px;
156
+ }
157
+
158
+ .info-icon {
159
+ cursor: help;
160
+ background: #E2E8F0;
161
+ width: 18px;
162
+ height: 18px;
163
+ border-radius: 50%;
164
+ display: inline-flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ font-size: 12px;
168
+ color: #4A5568;
169
+ margin-left: 4px;
170
+ }
171
+
172
+ .info-icon:hover + .tooltip-content {
173
+ display: block;
174
+ }
175
+
176
+ .tooltip-content {
177
+ display: none;
178
+ position: absolute;
179
+ background: #2D3748;
180
+ color: #FFFFFF;
181
+ padding: 8px 12px;
182
+ border-radius: 6px;
183
+ font-size: 14px;
184
+ line-height: 1.3;
185
+ width: max-content;
186
+ max-width: 280px;
187
+ z-index: 1000;
188
+ top: 0; /* 修改位置 */
189
+ left: 100%;
190
+ margin-left: 10px;
191
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
192
+ white-space: normal; /* 允許文字換行 */
193
+ }
194
+
195
+
196
+ .tooltip-content,
197
+ .tooltip-content *,
198
+ .tooltip-content strong,
199
+ .tooltip-content li,
200
+ .tooltip-content ul,
201
+ .tooltip-content p,
202
+ .tooltip-content span,
203
+ .tooltip-content div {
204
+ color: #FFFFFF !important;
205
+ }
206
+
207
+
208
+ .tooltip-content::before {
209
+ content: '';
210
+ position: absolute;
211
+ left: -6px;
212
+ top: 14px; /* 配合上方位置調整 */
213
+ border-width: 6px;
214
+ border-style: solid;
215
+ border-color: transparent #2D3748 transparent transparent;
216
+ }
217
+
218
+ .tooltip-content strong {
219
+ color: #FFFFFF;
220
+ display: block;
221
+ margin-bottom: 4px;
222
+ font-weight: 600;
223
+ }
224
+
225
+ .tooltip-content ul {
226
+ margin: 0;
227
+ padding-left: 16px;
228
+ color: #FFFFFF;
229
+ }
230
+
231
+ .tooltip-content * {
232
+ color: #FFFFFF;
233
+ }
234
+
235
+ .tooltip-content li {
236
+ margin-bottom: 2px;
237
+ color: #FFFFFF;
238
+ }
239
+
240
+ .tooltip-content li::before {
241
+ color: #FFFFFF !important;
242
+ }
243
+
244
+ .tooltip-content br {
245
+ display: block;
246
+ margin: 2px 0;
247
+ }
248
+
249
+ .info-value {
250
+ color: #2D3748;
251
+ font-weight: 500;
252
+ }
253
+
254
+ .characteristic-section {
255
+ background: #F8FAFC;
256
+ padding: 20px;
257
+ border-radius: 8px;
258
+ margin-bottom: 20px;
259
+ }
260
+
261
+ .subsection-title {
262
+ font-size: 18px;
263
+ color: #2D3748;
264
+ margin-bottom: 16px;
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 8px;
268
+ }
269
+
270
+ .noise-level {
271
+ background: #EDF2F7;
272
+ padding: 16px;
273
+ border-radius: 6px;
274
+ margin: 16px 0;
275
+ border: 1px solid #CBD5E0;
276
+ }
277
+
278
+ .level-label {
279
+ color: #4A5568;
280
+ font-size: 1.1em;
281
+ font-weight: 500;
282
+ margin-bottom: 8px;
283
+ }
284
+
285
+ .level-value {
286
+ color: #2D3748;
287
+ font-size: 1.2em;
288
+ font-weight: 600;
289
+ }
290
+
291
+ .characteristics-grid {
292
+ display: grid;
293
+ grid-template-columns: repeat(2, 1fr);
294
+ gap: 12px;
295
+ margin-top: 12px;
296
+ }
297
+
298
+ .characteristic-item {
299
+ background: white;
300
+ padding: 12px;
301
+ border-radius: 6px;
302
+ border: 1px solid #E2E8F0;
303
+ color: #4A5568;
304
+ }
305
+
306
+ .health-insights {
307
+ margin-top: 24px;
308
+ }
309
+
310
+ .health-grid {
311
+ display: grid;
312
+ grid-template-columns: 1fr;
313
+ gap: 8px;
314
+ }
315
+
316
+ .health-item {
317
+ background: white;
318
+ padding: 12px;
319
+ border-radius: 6px;
320
+ border: 1px solid #E2E8F0;
321
+ color: #E53E3E;
322
+ }
323
+
324
+ .screening-item {
325
+ background: white;
326
+ padding: 12px;
327
+ border-radius: 6px;
328
+ border: 1px solid #E2E8F0;
329
+ color: #38A169;
330
+ }
331
+
332
+ .learn-more-btn {
333
+ display: inline-block;
334
+ margin-top: 20px;
335
+ padding: 12px 24px;
336
+ background: linear-gradient(135deg, #2B6CB0, #2C5282);
337
+ color: white;
338
+ text-decoration: none;
339
+ border-radius: 6px;
340
+ transition: all 0.3s ease;
341
+ text-align: center;
342
+ width: 100%;
343
+ font-weight: 500;
344
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
345
+ }
346
+
347
+ .learn-more-btn:hover {
348
+ background: linear-gradient(135deg, #2C5282, #2B6CB0);
349
+ transform: translateY(-2px);
350
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
351
+ }
352
+
353
+ .info-disclaimer {
354
+ margin-top: 24px;
355
+ padding: 16px;
356
+ background: #F7FAFC;
357
+ border-radius: 8px;
358
+ font-size: 0.9em;
359
+ color: #4A5568;
360
+ line-height: 1.5;
361
+ border-left: 4px solid #4299E1;
362
+ }
363
+
364
+ @media (max-width: 768px) {
365
+ .comparison-container {
366
+ grid-template-columns: 1fr;
367
+ }
368
+ .info-section {
369
+ grid-template-columns: 1fr;
370
+ }
371
+ .characteristics-grid {
372
+ grid-template-columns: 1fr;
373
+ }
374
+ }
375
+ """
376
+
377
+ def show_comparison(breed1, breed2):
378
+ if not breed1 or not breed2:
379
+ return "Please select two breeds to compare"
380
+
381
+ breed1_info = get_dog_description(breed1)
382
+ breed2_info = get_dog_description(breed2)
383
+ breed1_noise = breed_noise_info.get(breed1, {})
384
+ breed2_noise = breed_noise_info.get(breed2, {})
385
+ breed1_health = breed_health_info.get(breed1, {})
386
+ breed2_health = breed_health_info.get(breed2, {})
387
+
388
+ def create_info_item(label, value, tooltip_text=""):
389
+ tooltip = f"""
390
+ <div class="info-label">
391
+ <span>{label}</span>
392
+ <div class="tooltip">
393
+ <span class="info-icon">i</span>
394
+ <div class="tooltip-content">
395
+ {tooltip_text}
396
+ </div>
397
+ </div>
398
+ </div>
399
+ """ if tooltip_text else f'<div class="info-label">{label}</div>'
400
+
401
+ return f"""
402
+ <div class="info-item" style="position: relative;">
403
+ {tooltip}
404
+ <div class="info-value">{value}</div>
405
+ </div>
406
+ """
407
+
408
+ def create_breed_section(breed, info, noise_info, health_info):
409
+ # 建立提示文字
410
+ section_tooltips = {
411
+ 'Size': """
412
+ <strong>Size Categories:</strong><br>
413
+ • Small: Under 20 pounds<br>
414
+ • Medium: 20-60 pounds<br>
415
+ • Large: Over 60 pounds
416
+ """,
417
+ 'Exercise': """
418
+ <strong>Exercise Needs:</strong><br>
419
+ • Low: Short walks suffice<br>
420
+ • Moderate: 1-2 hours daily<br>
421
+ • High: 2+ hours daily activity<br>
422
+ • Very High: Intensive daily exercise
423
+ """,
424
+ 'Grooming': """
425
+ <strong>Grooming Requirements:</strong><br>
426
+ • Low: Occasional brushing<br>
427
+ • Moderate: Weekly grooming<br>
428
+ • High: Daily maintenance needed
429
+ """,
430
+ 'Children': """
431
+ <strong>Compatibility with Children:</strong><br>
432
+ • Yes: Excellent with kids<br>
433
+ • Moderate: Good with supervision<br>
434
+ • No: Better with older children
435
+ """,
436
+ 'Lifespan': """
437
+ <strong>Average Lifespan Range:</strong><br>
438
+ Typical lifespan for this breed with proper care and genetics
439
+ """,
440
+ 'noise': """
441
+ <strong>Noise Behavior Information:</strong><br>
442
+ • Noise Level indicates typical vocalization intensity<br>
443
+ • Characteristics describe common vocal behaviors<br>
444
+ • Triggers list common causes of barking or vocalization
445
+ """,
446
+ 'health': """
447
+ <strong>Health Information:</strong><br>
448
+ • Health considerations are breed-specific concerns<br>
449
+ • Screenings are recommended preventive tests<br>
450
+ • Always consult with veterinary professionals
451
+ """
452
+ }
453
+
454
+ noise_data = format_noise_data(noise_info.get('noise_notes', ''))
455
+ health_data = format_health_data(health_info.get('health_notes', ''))
456
+
457
+ def create_section_header(title, icon, tooltip_text):
458
+ return f"""
459
+ <div class="section-header">
460
+ <span>{icon}</span>
461
+ <span>{title}</span>
462
+ <span class="tooltip">
463
+ <span class="tooltip-icon">ⓘ</span>
464
+ <span class="tooltip-text">{tooltip_text}</span>
465
+ </span>
466
+ </div>
467
+ """
468
+
469
+ return f"""
470
+ <div class="breed-column">
471
+ <h2 class="section-title">🐕 {breed.replace('_', ' ')}</h2>
472
+
473
+ <div class="info-section">
474
+ {create_info_item('Size', info['Size'], section_tooltips['Size'])}
475
+ {create_info_item('Exercise', info['Exercise Needs'], section_tooltips['Exercise'])}
476
+ {create_info_item('Grooming', info['Grooming Needs'], section_tooltips['Grooming'])}
477
+ {create_info_item('With Children', info['Good with Children'], section_tooltips['Children'])}
478
+ {create_info_item('Lifespan', info['Lifespan'], section_tooltips['Lifespan'])}
479
+ </div>
480
+
481
+ <div class="characteristic-section">
482
+ {create_section_header('Noise Behavior', '🔊', section_tooltips['noise'])}
483
+ <div class="noise-level">
484
+ <div class="level-label">Noise Level</div>
485
+ <div class="level-value">{noise_data['noise_level'].upper()}</div>
486
+ </div>
487
+
488
+ <div class="subsection">
489
+ <h4>Typical Characteristics</h4>
490
+ <div class="characteristics-grid">
491
+ {' '.join([f'<div class="characteristic-item">{char}</div>'
492
+ for char in noise_data['characteristics']])}
493
+ </div>
494
+ </div>
495
+
496
+ <div class="subsection">
497
+ <h4>Barking Triggers</h4>
498
+ <div class="characteristics-grid">
499
+ {' '.join([f'<div class="characteristic-item">{trigger}</div>'
500
+ for trigger in noise_data['triggers']])}
501
+ </div>
502
+ </div>
503
+ </div>
504
+
505
+ <div class="characteristic-section health-insights">
506
+ {create_section_header('Health Insights', '🏥', section_tooltips['health'])}
507
+ <div class="subsection">
508
+ <h4>Health Considerations</h4>
509
+ <div class="health-grid">
510
+ {' '.join([f'<div class="health-item">{item}</div>'
511
+ for item in health_data['considerations']])}
512
+ </div>
513
+ </div>
514
+
515
+ <div class="subsection">
516
+ <h4>Recommended Screenings</h4>
517
+ <div class="health-grid">
518
+ {' '.join([f'<div class="screening-item">{item}</div>'
519
+ for item in health_data['screenings']])}
520
+ </div>
521
+ </div>
522
+ </div>
523
+
524
+ <a href="https://www.akc.org/dog-breeds/{breed.lower().replace('_', '-')}/"
525
+ class="learn-more-btn"
526
+ target="_blank">
527
+ 🌐 Learn more about {breed.replace('_', ' ')} on AKC
528
+ </a>
529
+ </div>
530
+ """
531
+
532
+ html_output = f"""
533
+ <div class="comparison-container">
534
+ {create_breed_section(breed1, breed1_info, breed1_noise, breed1_health)}
535
+ {create_breed_section(breed2, breed2_info, breed2_noise, breed2_health)}
536
+ </div>
537
+ <div class="info-disclaimer">
538
+ <strong>Note:</strong> The health and behavioral information provided is for general reference only.
539
+ Individual dogs may vary, and characteristics can be influenced by training, socialization, and genetics.
540
+ Always consult with veterinary professionals for specific health advice and professional trainers for
541
+ behavioral guidance.
542
+ </div>
543
+ <style>{get_comparison_styles()}</style>
544
+ """
545
+
546
+ return html_output
547
+
548
+ compare_btn.click(
549
+ show_comparison,
550
+ inputs=[breed1_dropdown, breed2_dropdown],
551
+ outputs=comparison_output
552
+ )
553
+
554
+ return {
555
+ 'breed1_dropdown': breed1_dropdown,
556
+ 'breed2_dropdown': breed2_dropdown,
557
+ 'compare_btn': compare_btn,
558
+ 'comparison_output': comparison_output
559
+ }
breed_detection.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import gradio as gr
3
+ from PIL import Image
4
+
5
+ def create_detection_tab(predict_fn, example_images):
6
+ with gr.TabItem("Breed Detection"):
7
+ gr.HTML("""
8
+ <div style='
9
+ text-align: center;
10
+ padding: 20px 0;
11
+ margin: 15px 0;
12
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
13
+ border-radius: 10px;
14
+ '>
15
+ <p style='
16
+ font-size: 1.2em;
17
+ margin: 0;
18
+ padding: 0 20px;
19
+ line-height: 1.5;
20
+ background: linear-gradient(90deg, #4299e1, #48bb78);
21
+ -webkit-background-clip: text;
22
+ -webkit-text-fill-color: transparent;
23
+ font-weight: 600;
24
+ '>
25
+ Upload a picture of a dog, and the model will predict its breed and provide detailed information!
26
+ </p>
27
+ <p style='
28
+ font-size: 0.9em;
29
+ color: #666;
30
+ margin-top: 8px;
31
+ padding: 0 20px;
32
+ '>
33
+ Note: The model's predictions may not always be 100% accurate, and it is recommended to use the results as a reference.
34
+ </p>
35
+ </div>
36
+ """)
37
+
38
+ with gr.Row():
39
+ input_image = gr.Image(label="Upload a dog image", type="pil")
40
+ output_image = gr.Image(label="Annotated Image")
41
+
42
+ output = gr.HTML(label="Prediction Results")
43
+ initial_state = gr.State()
44
+
45
+ input_image.change(
46
+ predict_fn,
47
+ inputs=input_image,
48
+ outputs=[output, output_image, initial_state]
49
+ )
50
+
51
+ gr.Examples(
52
+ examples=example_images,
53
+ inputs=input_image
54
+ )
55
+
56
+ return {
57
+ 'input_image': input_image,
58
+ 'output_image': output_image,
59
+ 'output': output,
60
+ 'initial_state': initial_state
61
+ }
breed_recommendation.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sqlite3
3
+ import gradio as gr
4
+ from dog_database import get_dog_description, dog_data
5
+ from breed_health_info import breed_health_info
6
+ from breed_noise_info import breed_noise_info
7
+ from scoring_calculation_system import UserPreferences, calculate_compatibility_score
8
+ from recommendation_html_format import format_recommendation_html, get_breed_recommendations
9
+ from smart_breed_matcher import SmartBreedMatcher
10
+ from description_search_ui import create_description_search_tab
11
+
12
+ def create_recommendation_tab(UserPreferences, get_breed_recommendations, format_recommendation_html, history_component):
13
+
14
+ with gr.TabItem("Breed Recommendation"):
15
+ with gr.Tabs():
16
+ with gr.Tab("Find by Criteria"):
17
+ gr.HTML("""
18
+ <div style='
19
+ text-align: center;
20
+ padding: 20px 0;
21
+ margin: 15px 0;
22
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
23
+ border-radius: 10px;
24
+ '>
25
+ <p style='
26
+ font-size: 1.2em;
27
+ margin: 0;
28
+ padding: 0 20px;
29
+ line-height: 1.5;
30
+ background: linear-gradient(90deg, #4299e1, #48bb78);
31
+ -webkit-background-clip: text;
32
+ -webkit-text-fill-color: transparent;
33
+ font-weight: 600;
34
+ '>
35
+ Tell us about your lifestyle, and we'll recommend the perfect dog breeds for you!
36
+ </p>
37
+ </div>
38
+ """)
39
+
40
+ with gr.Row():
41
+ with gr.Column():
42
+ living_space = gr.Radio(
43
+ choices=["apartment", "house_small", "house_large"],
44
+ label="What type of living space do you have?",
45
+ info="Choose your current living situation",
46
+ value="apartment"
47
+ )
48
+
49
+ exercise_time = gr.Slider(
50
+ minimum=0,
51
+ maximum=180,
52
+ value=60,
53
+ label="Daily exercise time (minutes)",
54
+ info="Consider walks, play time, and training"
55
+ )
56
+
57
+ grooming_commitment = gr.Radio(
58
+ choices=["low", "medium", "high"],
59
+ label="Grooming commitment level",
60
+ info="Low: monthly, Medium: weekly, High: daily",
61
+ value="medium"
62
+ )
63
+
64
+ with gr.Column():
65
+ experience_level = gr.Radio(
66
+ choices=["beginner", "intermediate", "advanced"],
67
+ label="Dog ownership experience",
68
+ info="Be honest - this helps find the right match",
69
+ value="beginner"
70
+ )
71
+
72
+ has_children = gr.Checkbox(
73
+ label="Have children at home",
74
+ info="Helps recommend child-friendly breeds"
75
+ )
76
+
77
+ noise_tolerance = gr.Radio(
78
+ choices=["low", "medium", "high"],
79
+ label="Noise tolerance level",
80
+ info="Some breeds are more vocal than others",
81
+ value="medium"
82
+ )
83
+
84
+ get_recommendations_btn = gr.Button("Find My Perfect Match! 🔍", variant="primary")
85
+ recommendation_output = gr.HTML(label="Breed Recommendations")
86
+
87
+ with gr.Tab("Find by Description"):
88
+ description_input, description_search_btn, description_output, loading_msg = create_description_search_tab()
89
+
90
+
91
+ def on_find_match_click(*args):
92
+ try:
93
+ user_prefs = UserPreferences(
94
+ living_space=args[0],
95
+ exercise_time=args[1],
96
+ grooming_commitment=args[2],
97
+ experience_level=args[3],
98
+ has_children=args[4],
99
+ noise_tolerance=args[5],
100
+ space_for_play=True if args[0] != "apartment" else False,
101
+ other_pets=False,
102
+ climate="moderate",
103
+ health_sensitivity="medium", # 新增: 默認中等敏感度
104
+ barking_acceptance=args[5] # 使用 noise_tolerance 作為 barking_acceptance
105
+ )
106
+
107
+ recommendations = get_breed_recommendations(user_prefs, top_n=10)
108
+
109
+ history_results = [{
110
+ 'breed': rec['breed'],
111
+ 'rank': rec['rank'],
112
+ 'overall_score': rec['final_score'],
113
+ 'base_score': rec['base_score'],
114
+ 'bonus_score': rec['bonus_score'],
115
+ 'scores': rec['scores']
116
+ } for rec in recommendations]
117
+
118
+ # 保存到歷史記錄,也需要更新保存的偏好設定
119
+ history_component.save_search(
120
+ user_preferences={
121
+ 'living_space': args[0],
122
+ 'exercise_time': args[1],
123
+ 'grooming_commitment': args[2],
124
+ 'experience_level': args[3],
125
+ 'has_children': args[4],
126
+ 'noise_tolerance': args[5],
127
+ 'health_sensitivity': "medium",
128
+ 'barking_acceptance': args[5]
129
+ },
130
+ results=history_results
131
+ )
132
+
133
+ return format_recommendation_html(recommendations)
134
+
135
+ except Exception as e:
136
+ print(f"Error in find match: {str(e)}")
137
+ import traceback
138
+ print(traceback.format_exc())
139
+ return "Error getting recommendations"
140
+
141
+ def on_description_search(description: str):
142
+ try:
143
+ matcher = SmartBreedMatcher(dog_data)
144
+ breed_recommendations = matcher.match_user_preference(description, top_n=10)
145
+
146
+ print("Creating user preferences...")
147
+ user_prefs = UserPreferences(
148
+ living_space="apartment" if "apartment" in description.lower() else "house_small",
149
+ exercise_time=60,
150
+ grooming_commitment="medium",
151
+ experience_level="intermediate",
152
+ has_children="children" in description.lower() or "kids" in description.lower(),
153
+ noise_tolerance="medium",
154
+ space_for_play=True if "yard" in description.lower() or "garden" in description.lower() else False,
155
+ other_pets=False,
156
+ climate="moderate",
157
+ health_sensitivity="medium",
158
+ barking_acceptance=None
159
+ )
160
+
161
+ final_recommendations = []
162
+
163
+ for smart_rec in breed_recommendations:
164
+ breed_name = smart_rec['breed']
165
+ breed_info = get_dog_description(breed_name)
166
+ if not isinstance(breed_info, dict):
167
+ continue
168
+
169
+ # 計算基礎相容性分數
170
+ compatibility_scores = calculate_compatibility_score(breed_info, user_prefs)
171
+
172
+ bonus_reasons = []
173
+ bonus_score = 0
174
+ is_preferred = smart_rec.get('is_preferred', False)
175
+ similarity = smart_rec.get('similarity', 0)
176
+
177
+ # 用戶直接提到的品種
178
+ if is_preferred:
179
+ bonus_score = 0.15 # 15% bonus
180
+ bonus_reasons.append("Directly mentioned breed (+15%)")
181
+ # 高相似度品種
182
+ elif similarity > 0.8:
183
+ bonus_score = 0.10 # 10% bonus
184
+ bonus_reasons.append("Very similar to preferred breed (+10%)")
185
+ # 中等相似度品種
186
+ elif similarity > 0.6:
187
+ bonus_score = 0.05 # 5% bonus
188
+ bonus_reasons.append("Similar to preferred breed (+5%)")
189
+
190
+ # 基於品種特性的額外加分
191
+ temperament = breed_info.get('Temperament', '').lower()
192
+ if any(trait in temperament for trait in ['friendly', 'gentle', 'affectionate']):
193
+ bonus_score += 0.02 # 2% bonus
194
+ bonus_reasons.append("Positive temperament traits (+2%)")
195
+
196
+ if breed_info.get('Good with Children') == 'Yes' and user_prefs.has_children:
197
+ bonus_score += 0.03 # 3% bonus
198
+ bonus_reasons.append("Excellent with children (+3%)")
199
+
200
+ # 基礎分數和最終分數計算
201
+ base_score = compatibility_scores.get('overall', 0.7)
202
+ final_score = min(0.95, base_score + bonus_score) # 確保不超過95%
203
+
204
+ final_recommendations.append({
205
+ 'rank': 0,
206
+ 'breed': breed_name,
207
+ 'base_score': round(base_score, 4),
208
+ 'bonus_score': round(bonus_score, 4),
209
+ 'final_score': round(final_score, 4),
210
+ 'scores': compatibility_scores,
211
+ 'match_reason': ' • '.join(bonus_reasons) if bonus_reasons else "Standard match",
212
+ 'info': breed_info,
213
+ 'noise_info': breed_noise_info.get(breed_name, {}),
214
+ 'health_info': breed_health_info.get(breed_name, {})
215
+ })
216
+
217
+ # 根據最終分數排序
218
+ final_recommendations.sort(key=lambda x: (-x['final_score'], x['breed']))
219
+
220
+ # 更新排名
221
+ for i, rec in enumerate(final_recommendations, 1):
222
+ rec['rank'] = i
223
+
224
+ # 新增:保存到歷史記錄
225
+ history_results = [{
226
+ 'breed': rec['breed'],
227
+ 'rank': rec['rank'],
228
+ 'final_score': rec['final_score']
229
+ } for rec in final_recommendations[:10]] # 只保存前10名
230
+
231
+ history_component.save_search(
232
+ user_preferences=None, # description搜尋不需要preferences
233
+ results=history_results,
234
+ search_type="description",
235
+ description=description # 用戶輸入的描述文字
236
+ )
237
+
238
+ # 驗證排序
239
+ print("\nFinal Rankings:")
240
+ for rec in final_recommendations:
241
+ print(f"#{rec['rank']} {rec['breed']}")
242
+ print(f"Base Score: {rec['base_score']:.4f}")
243
+ print(f"Bonus Score: {rec['bonus_score']:.4f}")
244
+ print(f"Final Score: {rec['final_score']:.4f}")
245
+ print(f"Reason: {rec['match_reason']}\n")
246
+
247
+ result = format_recommendation_html(final_recommendations)
248
+ return [gr.update(value=result), gr.update(visible=False)]
249
+
250
+ except Exception as e:
251
+ error_msg = f"Error processing your description. Details: {str(e)}"
252
+ return [gr.update(value=error_msg), gr.update(visible=False)]
253
+
254
+ def show_loading():
255
+ return [gr.update(value=""), gr.update(visible=True)]
256
+
257
+
258
+ get_recommendations_btn.click(
259
+ fn=on_find_match_click,
260
+ inputs=[
261
+ living_space,
262
+ exercise_time,
263
+ grooming_commitment,
264
+ experience_level,
265
+ has_children,
266
+ noise_tolerance
267
+ ],
268
+ outputs=recommendation_output
269
+ )
270
+
271
+ description_search_btn.click(
272
+ fn=show_loading, # 先顯示加載消息
273
+ outputs=[description_output, loading_msg]
274
+ ).then( # 然後執行搜索
275
+ fn=on_description_search,
276
+ inputs=[description_input],
277
+ outputs=[description_output, loading_msg]
278
+ )
279
+
280
+ return {
281
+ 'living_space': living_space,
282
+ 'exercise_time': exercise_time,
283
+ 'grooming_commitment': grooming_commitment,
284
+ 'experience_level': experience_level,
285
+ 'has_children': has_children,
286
+ 'noise_tolerance': noise_tolerance,
287
+ 'get_recommendations_btn': get_recommendations_btn,
288
+ 'recommendation_output': recommendation_output,
289
+ 'description_input': description_input,
290
+ 'description_search_btn': description_search_btn,
291
+ 'description_output': description_output
292
+ }
description_search_ui.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+
4
+ def create_description_search_tab():
5
+ """創建描述搜尋頁面的UI程式碼"""
6
+ guide_html = """
7
+ <div class="breed-search-container">
8
+ <div class="description-guide">
9
+ <h2 class="guide-title" style="
10
+ background: linear-gradient(90deg, #4299e1, #48bb78);
11
+ -webkit-background-clip: text;
12
+ -webkit-text-fill-color: transparent;
13
+ font-weight: 600;
14
+ font-size: 1.8em;
15
+ ">🐾 Describe Your Ideal Dog</h2>
16
+
17
+ <div class="guide-content">
18
+ <p class="intro-text" style="
19
+ background: linear-gradient(90deg, #4299e1, #48bb78);
20
+ -webkit-background-clip: text;
21
+ -webkit-text-fill-color: transparent;
22
+ font-weight: 600;
23
+ font-size: 1.2em;
24
+ margin-bottom: 20px;
25
+ ">Help us find your perfect companion! Please consider including the following details:</p>
26
+
27
+ <div class="criteria-grid" style="
28
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
29
+ border-radius: 10px;
30
+ padding: 20px;
31
+ ">
32
+ <div class="criteria-item">
33
+ <span class="icon">🏃</span>
34
+ <div class="criteria-content">
35
+ <h3>Activity Level</h3>
36
+ <p>Low • Moderate • High • Very Active</p>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="criteria-item">
41
+ <span class="icon">🏠</span>
42
+ <div class="criteria-content">
43
+ <h3>Living Environment</h3>
44
+ <p>Apartment • House • Yard Space</p>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="criteria-item">
49
+ <span class="icon">👨‍👩‍👧‍👦</span>
50
+ <div class="criteria-content">
51
+ <h3>Family Situation</h3>
52
+ <p>Children • Other Pets • Single Adult</p>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="criteria-item">
57
+ <span class="icon">✂️</span>
58
+ <div class="criteria-content">
59
+ <h3>Grooming Commitment</h3>
60
+ <p>Low • Medium • High Maintenance</p>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="criteria-item">
65
+ <span class="icon">🎭</span>
66
+ <div class="criteria-content">
67
+ <h3>Desired Personality</h3>
68
+ <p>Friendly • Independent • Intelligent • Calm</p>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ """
76
+
77
+ # 增加 CSS 的樣式
78
+ css = """
79
+ <style>
80
+ .breed-search-container {
81
+ background: white;
82
+ border-radius: 12px;
83
+ padding: 24px;
84
+ margin-bottom: 20px;
85
+ }
86
+ .guide-title {
87
+ font-size: 1.8rem;
88
+ color: #2c3e50;
89
+ margin-bottom: 20px;
90
+ text-align: center;
91
+ }
92
+ .intro-text {
93
+ color: #666;
94
+ text-align: center;
95
+ margin-bottom: 24px;
96
+ font-size: 1.1rem;
97
+ }
98
+ .criteria-grid {
99
+ display: grid;
100
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
101
+ gap: 20px;
102
+ margin-bottom: 24px;
103
+ }
104
+ .criteria-item {
105
+ display: flex;
106
+ align-items: flex-start;
107
+ padding: 16px;
108
+ background: #f8fafc;
109
+ border-radius: 8px;
110
+ transition: all 0.3s ease;
111
+ }
112
+ .criteria-item:hover {
113
+ transform: translateY(-2px);
114
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
115
+ }
116
+ .criteria-item .icon {
117
+ font-size: 24px;
118
+ margin-right: 12px;
119
+ margin-top: 3px;
120
+ }
121
+ .criteria-content h3 {
122
+ font-size: 1.1rem;
123
+ color: #2c3e50;
124
+ margin: 0 0 4px 0;
125
+ }
126
+ .criteria-content p {
127
+ color: #666;
128
+ margin: 0;
129
+ font-size: 0.95rem;
130
+ }
131
+ </style>
132
+ """
133
+
134
+ with gr.Column():
135
+ # 顯示指南和樣式
136
+ gr.HTML(css + guide_html)
137
+
138
+ # 描述輸入區
139
+ description_input = gr.Textbox(
140
+ label="",
141
+ placeholder="Example: I'm looking for a medium-sized, friendly dog that's good with kids...",
142
+ lines=5
143
+ )
144
+
145
+ # 搜索按鈕
146
+ search_button = gr.Button(
147
+ "Find My Perfect Match! 🔍",
148
+ variant="primary",
149
+ size="lg"
150
+ )
151
+
152
+ # 加載消息
153
+ loading_msg = gr.HTML("""
154
+ <div style='text-align: center; color: #666;'>
155
+ <p><b>Finding your perfect match...</b></p>
156
+ <p>Please wait 15-20 seconds while we analyze your preferences.</p>
157
+ </div>
158
+ """, visible=False)
159
+
160
+ # 結果顯示區域
161
+ result_output = gr.HTML(label="Breed Recommendations")
162
+
163
+ return description_input, search_button, result_output, loading_msg
history_manager.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from datetime import datetime
3
+ import json
4
+ import os
5
+ import pytz
6
+ import traceback
7
+
8
+ class UserHistoryManager:
9
+ def __init__(self):
10
+ """初始化歷史紀錄管理器"""
11
+ self.history_file = "user_history.json"
12
+ print(f"Initializing UserHistoryManager with file: {os.path.abspath(self.history_file)}")
13
+ self._init_file()
14
+
15
+ def _init_file(self):
16
+ """初始化JSON檔案"""
17
+ try:
18
+ if not os.path.exists(self.history_file):
19
+ print(f"Creating new history file: {self.history_file}")
20
+ with open(self.history_file, 'w', encoding='utf-8') as f:
21
+ json.dump([], f)
22
+ else:
23
+ print(f"History file exists: {self.history_file}")
24
+ # 驗證檔案內容
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
+ except Exception as e:
29
+ print(f"Error in _init_file: {str(e)}")
30
+ print(traceback.format_exc())
31
+
32
+ def save_history(self, user_preferences: dict = None, results: list = None, search_type: str = "criteria", description: str = None) -> bool:
33
+ """
34
+ 儲存搜尋歷史,支援兩種類型的搜尋
35
+ Args:
36
+ user_preferences: dict, 使用者偏好設定 (用於 criteria 搜尋)
37
+ results: list, 推薦結果
38
+ search_type: str, 搜尋類型 ("criteria" 或 "description")
39
+ description: str, 使用者輸入的描述 (用於 description 搜尋)
40
+ """
41
+ try:
42
+ taipei_tz = pytz.timezone('Asia/Taipei')
43
+ taipei_time = datetime.now(taipei_tz)
44
+
45
+ history_entry = {
46
+ "timestamp": taipei_time.strftime("%Y-%m-%d %H:%M:%S"),
47
+ "search_type": search_type,
48
+ "results": results
49
+ }
50
+
51
+ # 根據搜尋類型添加不同的資訊
52
+ if search_type == "criteria":
53
+ history_entry["preferences"] = user_preferences
54
+ else: # description
55
+ history_entry["description"] = description
56
+
57
+ with open(self.history_file, 'r', encoding='utf-8') as f:
58
+ history = json.load(f)
59
+
60
+ history.append(history_entry)
61
+ if len(history) > 20:
62
+ history = history[-20:]
63
+
64
+ with open(self.history_file, 'w', encoding='utf-8') as f:
65
+ json.dump(history, f, ensure_ascii=False, indent=2)
66
+
67
+ return True
68
+ except Exception as e:
69
+ print(f"Error saving history: {str(e)}")
70
+ return False
71
+
72
+ def get_history(self) -> list:
73
+ """獲取搜尋歷史"""
74
+ try:
75
+ print("Attempting to read history") # Debug
76
+ with open(self.history_file, 'r', encoding='utf-8') as f:
77
+ data = json.load(f)
78
+ print(f"Read {len(data)} history entries") # Debug
79
+ return data if isinstance(data, list) else []
80
+ except Exception as e:
81
+ print(f"Error reading history: {str(e)}")
82
+ print(traceback.format_exc())
83
+ return []
84
+
85
+ def clear_all_history(self) -> bool:
86
+ """清除所有歷史紀錄"""
87
+ try:
88
+ print("Attempting to clear all history") # Debug
89
+ with open(self.history_file, 'w', encoding='utf-8') as f:
90
+ json.dump([], f)
91
+ print("History cleared successfully") # Debug
92
+ return True
93
+ except Exception as e:
94
+ print(f"Error clearing history: {str(e)}")
95
+ print(traceback.format_exc())
96
+ return False
search_history.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ import traceback
4
+ from typing import Optional, Dict, List
5
+ from history_manager import UserHistoryManager
6
+
7
+ class SearchHistoryComponent:
8
+ def __init__(self):
9
+ """初始化搜索歷史組件"""
10
+ self.history_manager = UserHistoryManager()
11
+
12
+ def format_history_html(self, history_data: Optional[List[Dict]] = None) -> str:
13
+ try:
14
+ if history_data is None:
15
+ history_data = self.history_manager.get_history()
16
+
17
+ if not history_data:
18
+ return """
19
+ <div style='
20
+ text-align: center;
21
+ padding: 40px 20px;
22
+ color: #666;
23
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.05), rgba(72, 187, 120, 0.05));
24
+ border-radius: 10px;
25
+ margin: 20px 0;
26
+ '>
27
+ <p style='
28
+ font-size: 1.1em;
29
+ margin: 0;
30
+ background: linear-gradient(90deg, #4299e1, #48bb78);
31
+ -webkit-background-clip: text;
32
+ -webkit-text-fill-color: transparent;
33
+ font-weight: 600;
34
+ '>
35
+ No search history yet. Try making some breed recommendations!
36
+ </p>
37
+ </div>
38
+ """
39
+
40
+ html = "<div class='history-container'>"
41
+
42
+ for entry in reversed(history_data):
43
+ timestamp = entry.get('timestamp', 'Unknown time')
44
+ search_type = entry.get('search_type', 'criteria')
45
+ results = entry.get('results', [])
46
+
47
+ # 根據搜尋類型使用不同的標題樣式
48
+ type_label = "Criteria History" if search_type == "criteria" else "Description History"
49
+ type_color = "#4299e1" if search_type == "criteria" else "#48bb78" # 藍色vs綠色
50
+
51
+ html += f"""
52
+ <div class="history-entry">
53
+ <div class="history-header" style="border-left: 4px solid {type_color}; padding-left: 10px;">
54
+ <span class="timestamp">🕒 {timestamp}</span>
55
+ <span class="search-type" style="color: {type_color}; font-weight: bold; margin-left: 10px;">
56
+ {type_label}
57
+ </span>
58
+ </div>
59
+ """
60
+
61
+ if search_type == "criteria":
62
+ # 原有的條件搜尋顯示邏輯
63
+ prefs = entry.get('preferences', {})
64
+ html += f"""
65
+ <div class="params-list">
66
+ <h4>Search Parameters:</h4>
67
+ <ul>
68
+ <li><span class="param-label">Living Space:</span> {prefs.get('living_space', 'N/A')}</li>
69
+ <li><span class="param-label">Exercise Time:</span> {prefs.get('exercise_time', 'N/A')} minutes</li>
70
+ <li><span class="param-label">Grooming:</span> {prefs.get('grooming_commitment', 'N/A')}</li>
71
+ <li><span class="param-label">Experience:</span> {prefs.get('experience_level', 'N/A')}</li>
72
+ <li><span class="param-label">Children at Home:</span> {"Yes" if prefs.get('has_children') else "No"}</li>
73
+ <li><span class="param-label">Noise Tolerance:</span> {prefs.get('noise_tolerance', 'N/A')}</li>
74
+ </ul>
75
+ </div>
76
+ """
77
+ else:
78
+ # Description 搜尋的顯示邏輯
79
+ description = entry.get('description', 'No description provided')
80
+ html += f"""
81
+ <div class="description-section">
82
+ <h4>Search Description:</h4>
83
+ <p class="user-description">{description}</p>
84
+ </div>
85
+ """
86
+
87
+ # 共用的結果顯示邏輯
88
+ html += """
89
+ <div class="results-list">
90
+ <h4>Top 10 Breed Matches:</h4>
91
+ <div class="breed-list">
92
+ """
93
+
94
+ if results:
95
+ for i, result in enumerate(results[:10], 1):
96
+ breed_name = result.get('breed', 'Unknown breed').replace('_', ' ')
97
+ score = result.get('overall_score', result.get('final_score', 0))
98
+ html += f"""
99
+ <div class="breed-item">
100
+ <div class="breed-info">
101
+ <span class="breed-rank">#{i}</span>
102
+ <span class="breed-name">{breed_name}</span>
103
+ <span class="breed-score">{score*100:.1f}%</span>
104
+ </div>
105
+ </div>
106
+ """
107
+
108
+ html += """
109
+ </div>
110
+ </div>
111
+ </div>
112
+ """
113
+
114
+ html += "</div>"
115
+ return html
116
+
117
+ except Exception as e:
118
+ print(f"Error formatting history: {str(e)}")
119
+ print(traceback.format_exc())
120
+ return f"""
121
+ <div style='text-align: center; padding: 20px; color: #dc2626;'>
122
+ Error formatting history. Please try refreshing the page.
123
+ <br>Error details: {str(e)}
124
+ </div>
125
+ """
126
+
127
+ def clear_history(self) -> str:
128
+ """清除所有搜尋歷史"""
129
+ try:
130
+ success = self.history_manager.clear_all_history()
131
+ print(f"Clear history result: {success}")
132
+ return self.format_history_html()
133
+ except Exception as e:
134
+ print(f"Error in clear_history: {str(e)}")
135
+ print(traceback.format_exc())
136
+ return "Error clearing history"
137
+
138
+ def refresh_history(self) -> str:
139
+ """刷新歷史記錄顯示"""
140
+ try:
141
+ return self.format_history_html()
142
+ except Exception as e:
143
+ print(f"Error in refresh_history: {str(e)}")
144
+ return "Error refreshing history"
145
+
146
+ def save_search(self, user_preferences: Optional[dict] = None,
147
+ results: list = None,
148
+ search_type: str = "criteria",
149
+ description: str = None) -> bool:
150
+ """保存搜索結果
151
+ Args:
152
+ user_preferences: 使用者偏好設定 (僅用於criteria搜尋)
153
+ results: 推薦結果列表
154
+ search_type: 搜尋類型 ("criteria" 或 "description")
155
+ description: 使用者輸入的描述 (僅用於description搜尋)
156
+ """
157
+ return self.history_manager.save_history(
158
+ user_preferences=user_preferences,
159
+ results=results,
160
+ search_type=search_type,
161
+ description=description
162
+ )
163
+
164
+ def create_history_component():
165
+ """只創建實例"""
166
+ return SearchHistoryComponent()
167
+
168
+ def create_history_tab(history_component: SearchHistoryComponent):
169
+ """創建歷史紀錄的頁面
170
+
171
+ Args:
172
+ history_component: 已创建的历史组件实例
173
+ """
174
+ with gr.TabItem("Recommendation Search History"):
175
+ gr.HTML("""
176
+ <div style='text-align: center; padding: 20px;'>
177
+ <h3 style='
178
+ color: #2D3748;
179
+ margin-bottom: 10px;
180
+ font-size: 1.5em;
181
+ background: linear-gradient(90deg, #4299e1, #48bb78);
182
+ -webkit-background-clip: text;
183
+ -webkit-text-fill-color: transparent;
184
+ font-weight: 600;
185
+ '>Search History</h3>
186
+ <div style='
187
+ text-align: center;
188
+ padding: 20px 0;
189
+ margin: 15px 0;
190
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
191
+ border-radius: 10px;
192
+ '>
193
+ <p style='
194
+ font-size: 1.2em;
195
+ margin: 0;
196
+ padding: 0 20px;
197
+ line-height: 1.5;
198
+ background: linear-gradient(90deg, #4299e1, #48bb78);
199
+ -webkit-background-clip: text;
200
+ -webkit-text-fill-color: transparent;
201
+ font-weight: 600;
202
+ '>
203
+ View your previous breed recommendations and search preferences
204
+ </p>
205
+ </div>
206
+ </div>
207
+ """)
208
+
209
+ with gr.Row():
210
+ with gr.Column(scale=4):
211
+ history_display = gr.HTML()
212
+
213
+ with gr.Row():
214
+ with gr.Column(scale=1):
215
+ clear_history_btn = gr.Button(
216
+ "🗑️ Clear History",
217
+ variant="secondary",
218
+ size="sm"
219
+ )
220
+ with gr.Column(scale=1):
221
+ refresh_btn = gr.Button(
222
+ "🔄 Refresh",
223
+ variant="secondary",
224
+ size="sm"
225
+ )
226
+
227
+ history_display.value = history_component.format_history_html()
228
+
229
+ clear_history_btn.click(
230
+ fn=history_component.clear_history,
231
+ outputs=[history_display],
232
+ api_name="clear_history"
233
+ )
234
+
235
+ refresh_btn.click(
236
+ fn=history_component.refresh_history,
237
+ outputs=[history_display],
238
+ api_name="refresh_history"
239
+ )