jinruiy Claude commited on
Commit
d1394bb
·
1 Parent(s): 4ebe674

Add per-entity feedback, rank details dropdown, and expanded KB

Browse files

Features:
- Per-entity feedback with ratings (relevance, helpful, sensitivity) and submit button
- Clickable rank score button with dropdown showing full score, matched chunk, subcategory, emirate, source, and model
- KB source display (Wiki, Dhow, Scrapped, Controversial) with deduplication
- Click-outside-to-close behavior for rank details dropdown
- Sensitive topics translation support

Backend:
- New /api/entity-feedback endpoint with UUID query tracking
- Entity feedback saved to entity_feedbacks.json

KB Updates:
- Expanded knowledge base with 2,268 entities
- Updated IR index with 7,531 chunks
- Added controversial_KB source type

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

backend/api.py CHANGED
@@ -76,6 +76,19 @@ class RatingRequest(BaseModel):
76
  rating_value: int # 0, 1, or 2 for relevance; 0 or 1 for helpful
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  # ============================================================
80
  # Translation Cache (file-based, persistent across restarts)
81
  # ============================================================
@@ -295,6 +308,53 @@ async def api_rating(request: RatingRequest, req: Request):
295
  return {"success": False, "error": str(e)}
296
 
297
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  @app.post("/api/translate")
299
  async def api_translate(request: TranslateRequest):
300
  """Translate texts using DeepL API"""
 
76
  rating_value: int # 0, 1, or 2 for relevance; 0 or 1 for helpful
77
 
78
 
79
+ class EntityFeedbackRequest(BaseModel):
80
+ query_id: str # UUID for tracking unique search sessions
81
+ query: str
82
+ query_timestamp: str
83
+ entity_id: str
84
+ entity_name: str
85
+ rank_position: int
86
+ rank_score: float
87
+ ratings: Dict[str, Optional[bool]] # {relevance, helpful, sensitivity_handling}
88
+ comment: str
89
+ submitted_at: str
90
+
91
+
92
  # ============================================================
93
  # Translation Cache (file-based, persistent across restarts)
94
  # ============================================================
 
308
  return {"success": False, "error": str(e)}
309
 
310
 
311
+ @app.post("/api/entity-feedback")
312
+ async def api_entity_feedback(request: EntityFeedbackRequest, req: Request):
313
+ """Save per-entity feedback with ratings and comment"""
314
+ try:
315
+ # Ensure data directory exists
316
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
317
+
318
+ # Get client IP
319
+ client_ip = req.headers.get("x-forwarded-for", "").split(",")[0].strip()
320
+ if not client_ip:
321
+ client_ip = req.client.host if req.client else "unknown"
322
+
323
+ feedback_file = DATA_DIR / "entity_feedbacks.json"
324
+
325
+ feedback = {
326
+ "query_id": request.query_id,
327
+ "query": request.query,
328
+ "query_timestamp": request.query_timestamp,
329
+ "user_ip": client_ip,
330
+ "entity_id": request.entity_id,
331
+ "entity_name": request.entity_name,
332
+ "rank_position": request.rank_position,
333
+ "rank_score": request.rank_score,
334
+ "ratings": request.ratings,
335
+ "comment": request.comment,
336
+ "submitted_at": request.submitted_at
337
+ }
338
+
339
+ # Load existing feedbacks
340
+ if feedback_file.exists():
341
+ with open(feedback_file, "r", encoding="utf-8") as f:
342
+ all_feedbacks = json.load(f)
343
+ else:
344
+ all_feedbacks = []
345
+
346
+ all_feedbacks.append(feedback)
347
+
348
+ # Save feedbacks
349
+ with open(feedback_file, "w", encoding="utf-8") as f:
350
+ json.dump(all_feedbacks, f, ensure_ascii=False, indent=2)
351
+
352
+ return {"success": True, "total": len(all_feedbacks)}
353
+
354
+ except Exception as e:
355
+ return {"success": False, "error": str(e)}
356
+
357
+
358
  @app.post("/api/translate")
359
  async def api_translate(request: TranslateRequest):
360
  """Translate texts using DeepL API"""
frontend/css/styles.css CHANGED
@@ -110,6 +110,66 @@ body.lang-cn {
110
  transform: scale(1.15);
111
  }
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  /* ============================================
114
  MODAL STYLES
115
  ============================================ */
 
110
  transform: scale(1.15);
111
  }
112
 
113
+ /* ============================================
114
+ SENSITIVE TOPICS & RESPONSE GUIDE
115
+ ============================================ */
116
+ details summary {
117
+ list-style: none;
118
+ }
119
+
120
+ details summary::-webkit-details-marker {
121
+ display: none;
122
+ }
123
+
124
+ details[open] summary {
125
+ color: #003d1c;
126
+ }
127
+
128
+ details[open] summary::before {
129
+ content: '▼ ';
130
+ }
131
+
132
+ details:not([open]) summary::before {
133
+ content: '▶ ';
134
+ }
135
+
136
+ .sensitive-topic-card {
137
+ transition: all 0.2s ease;
138
+ }
139
+
140
+ .sensitive-topic-card:hover {
141
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
142
+ }
143
+
144
+ /* ============================================
145
+ RANK DETAILS DROPDOWN
146
+ ============================================ */
147
+ .rank-details {
148
+ position: relative;
149
+ }
150
+
151
+ .rank-details summary {
152
+ list-style: none;
153
+ }
154
+
155
+ .rank-details summary::-webkit-details-marker {
156
+ display: none;
157
+ }
158
+
159
+ .rank-details summary::before {
160
+ content: none !important;
161
+ }
162
+
163
+ .rank-details[open] summary {
164
+ border-radius: 4px 4px 0 0;
165
+ }
166
+
167
+ .rank-details > div {
168
+ position: absolute;
169
+ right: 0;
170
+ top: 100%;
171
+ }
172
+
173
  /* ============================================
174
  MODAL STYLES
175
  ============================================ */
frontend/js/app.js CHANGED
@@ -38,11 +38,33 @@ const TRANSLATIONS = {
38
  enterQuery: 'Enter a query above to search the UAE Knowledge Base',
39
  selectCategoryHint: 'Select a category and click Search to begin',
40
  mustKnowFacts: '✓ Must-Know Facts',
 
 
 
 
 
 
 
 
 
 
 
 
41
  relevance: 'Relevance?',
42
  helpful: 'Helpful?',
 
 
 
 
43
  detailedAnalysis: 'Detailed Analysis',
44
  fullEntityJson: 'Full Entity JSON',
45
  rankScore: 'Rank Score',
 
 
 
 
 
 
46
  viewEntity: 'View Entity',
47
  entityData: 'Entity Data',
48
  pleaseEnterQuery: 'Please enter a search query',
@@ -102,11 +124,33 @@ const TRANSLATIONS = {
102
  enterQuery: 'أدخل استعلامك للبحث في قاعدة المعرفة',
103
  selectCategoryHint: 'اختر فئة وانقر للبحث',
104
  mustKnowFacts: '✓ حقائق أساسية',
 
 
 
 
 
 
 
 
 
 
 
 
105
  relevance: 'الصلة؟',
106
  helpful: 'مفيد؟',
 
 
 
 
107
  detailedAnalysis: 'تحليل مفصل',
108
  fullEntityJson: 'بيانات الكيان الكاملة',
109
- rankScore: 'الترتيب',
 
 
 
 
 
 
110
  viewEntity: 'عرض الكيان',
111
  entityData: 'بيانات الكيان',
112
  pleaseEnterQuery: 'الرجاء إدخال استعلام البحث',
@@ -166,11 +210,33 @@ const TRANSLATIONS = {
166
  enterQuery: '在上方输入查询以搜索阿联酋知识库',
167
  selectCategoryHint: '选择类别并点击搜索开始',
168
  mustKnowFacts: '✓ 必知事实',
 
 
 
 
 
 
 
 
 
 
 
 
169
  relevance: '相关性?',
170
  helpful: '有帮助?',
 
 
 
 
171
  detailedAnalysis: '详细分析',
172
  fullEntityJson: '完整实体JSON',
173
- rankScore: '排分数',
 
 
 
 
 
 
174
  viewEntity: '查看实体',
175
  entityData: '实体数据',
176
  pleaseEnterQuery: '请输入搜索查询',
@@ -230,7 +296,9 @@ const state = {
230
  currentQuery: '',
231
  currentCategory: null,
232
  results: [],
233
- ratings: {}, // { entityIndex: { relevance: 0|1|2, helpful: true|false } }
 
 
234
  isLoading: false,
235
  language: localStorage.getItem('uae_lang') || 'en',
236
  // Translation state
@@ -243,6 +311,15 @@ const state = {
243
  resultsPerPage: 10
244
  };
245
 
 
 
 
 
 
 
 
 
 
246
  // ============================================
247
  // DOM ELEMENTS
248
  // ============================================
@@ -297,6 +374,12 @@ function initEventListeners() {
297
  if (!e.target.closest('.category-dropdown')) {
298
  DOM.categoryDropdown?.classList.remove('active');
299
  }
 
 
 
 
 
 
300
  });
301
 
302
  // Modals
@@ -353,6 +436,8 @@ async function handleSearch() {
353
  state.currentQuery = query;
354
  state.isLoading = true;
355
  state.ratings = {};
 
 
356
  // Reset translation state for new search
357
  state.translatedResults = {};
358
  state.showOriginal = false;
@@ -606,6 +691,20 @@ function renderResults() {
606
  // Get translated or original content
607
  const content = getResultContent(result, index);
608
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  return `
610
  <div class="bg-white rounded shadow-xl border border-gray-200 result-card mb-4 md:mb-6 overflow-x-hidden" data-index="${index}">
611
  <!-- Card Header -->
@@ -614,9 +713,45 @@ function renderResults() {
614
  <span class="text-amber-400">${index === 0 ? '🦅' : '📄'}</span>
615
  #${index + 1} ${escapeHtml(content.entityName)}
616
  </div>
617
- <div class="text-[12px] md:text-[16px] font-medium gold-button-slender text-emerald-950 px-3 md:px-4 rounded shadow-sm self-start sm:self-auto">
618
- ${t('rankScore')}: ${result.score.toFixed(2)}
619
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  </div>
621
 
622
  <!-- Card Body -->
@@ -629,20 +764,16 @@ function renderResults() {
629
  </p>
630
  </div>
631
 
632
- <!-- Must-Know Facts -->
633
  <div class="w-full md:w-[38%] pt-1">
634
- <h4 class="text-emerald-800 font-medium text-[14px] md:text-[18px] mb-3 md:mb-4">${t('mustKnowFacts')}</h4>
635
- <ul class="text-[14px] md:text-[18px] space-y-2 md:space-y-3 text-emerald-800/80">
636
- ${content.facts.slice(0, 5).map(fact => `
637
- <li>• ${escapeHtml(fact)}</li>
638
- `).join('')}
639
- </ul>
640
  </div>
641
  </div>
642
 
643
- <!-- Rating Row -->
644
  <div class="mt-6 md:mt-8 border-t border-gray-50 pt-4 md:pt-6 flex flex-col gap-4">
645
- <div class="flex flex-wrap items-center gap-4 md:gap-12">
646
  <!-- Relevance Rating -->
647
  <div class="flex items-center gap-2 md:gap-3">
648
  <span class="text-[12px] md:text-[16px] font-medium text-gray-400 tracking-wider">${t('relevance')}</span>
@@ -664,6 +795,33 @@ function renderResults() {
664
  onclick="setRating(${index}, 'helpful', false)">👎</button>
665
  </div>
666
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
  </div>
668
 
669
  <!-- Detailed Analysis Button -->
@@ -738,9 +896,9 @@ function selectCategory(categoryId) {
738
  }
739
 
740
  // ============================================
741
- // RATINGS (auto-save on click)
742
  // ============================================
743
- window.setRating = async function(entityIndex, dimension, value) {
744
  if (!state.ratings[entityIndex]) {
745
  state.ratings[entityIndex] = {};
746
  }
@@ -748,31 +906,182 @@ window.setRating = async function(entityIndex, dimension, value) {
748
 
749
  // Re-render to update button states
750
  renderResults();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
 
752
- // Auto-save rating to backend
753
  try {
754
- const result = state.results[entityIndex];
755
- const response = await fetch(`${CONFIG.API_BASE}/rating`, {
756
  method: 'POST',
757
  headers: { 'Content-Type': 'application/json' },
758
  body: JSON.stringify({
 
759
  query: state.currentQuery,
760
- category: state.currentCategory?.id || '',
761
- entity_id: result?.entity_id || '',
762
- entity_index: entityIndex,
763
- rating_type: dimension,
764
- rating_value: dimension === 'helpful' ? (value ? 1 : 0) : value
 
 
 
 
 
 
 
765
  })
766
  });
767
 
768
  const data = await response.json();
 
769
  if (data.success) {
770
- // Show subtle confirmation
771
- showToast('Rating saved ✓', 'success');
 
 
 
 
 
 
 
 
772
  }
773
  } catch (error) {
774
- console.error('Failed to save rating:', error);
775
- // Don't show error to user - rating is still stored locally
776
  }
777
  };
778
 
@@ -977,6 +1286,43 @@ window.translateCurrentPage = async function() {
977
  textsToTranslate.push(fact);
978
  textMap.push({ index, field: 'fact', factIndex });
979
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
980
  });
981
 
982
  if (textsToTranslate.length === 0) {
@@ -1002,13 +1348,14 @@ window.translateCurrentPage = async function() {
1002
  }
1003
 
1004
  data.translations.forEach((translated, i) => {
1005
- const { index, field, factIndex } = textMap[i];
1006
 
1007
  if (!state.translatedResults[lang][index]) {
1008
  state.translatedResults[lang][index] = {
1009
  entityName: null,
1010
  summary: null,
1011
- facts: []
 
1012
  };
1013
  }
1014
 
@@ -1018,6 +1365,32 @@ window.translateCurrentPage = async function() {
1018
  state.translatedResults[lang][index].summary = translated;
1019
  } else if (field === 'fact') {
1020
  state.translatedResults[lang][index].facts[factIndex] = translated;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1021
  }
1022
  });
1023
  }
@@ -1047,14 +1420,16 @@ function getResultContent(result, index) {
1047
  summary: state.translatedResults[lang][index].summary || result.summary,
1048
  facts: state.translatedResults[lang][index].facts.length > 0
1049
  ? state.translatedResults[lang][index].facts
1050
- : (result.must_answer || [])
 
1051
  };
1052
  }
1053
 
1054
  return {
1055
  entityName: result.entity_name,
1056
  summary: result.summary,
1057
- facts: result.must_answer || []
 
1058
  };
1059
  }
1060
 
 
38
  enterQuery: 'Enter a query above to search the UAE Knowledge Base',
39
  selectCategoryHint: 'Select a category and click Search to begin',
40
  mustKnowFacts: '✓ Must-Know Facts',
41
+ sensitiveTopics: '⚠️ Sensitive Topics',
42
+ sensitivityRating: 'Sensitivity',
43
+ sensitivityHigh: '🔴 HIGH',
44
+ sensitivityMedium: '🟡 MEDIUM',
45
+ sensitivityLow: '🟢 LOW',
46
+ noSensitiveTopics: 'No sensitive topics identified',
47
+ problematicFraming: 'Problematic Framing',
48
+ responseGuide: 'Response Guide',
49
+ strategy: 'Strategy',
50
+ tone: 'Tone',
51
+ keyFacts: 'Key Facts',
52
+ suggestedResponse: 'Suggested Response',
53
  relevance: 'Relevance?',
54
  helpful: 'Helpful?',
55
+ sensitivityHandling: 'Sensitivity Handling?',
56
+ feedbackForEntity: 'Your feedback on this entity...',
57
+ submitEntityFeedback: 'Submit Feedback',
58
+ feedbackSavedForEntity: 'Feedback saved for this entity ✓',
59
  detailedAnalysis: 'Detailed Analysis',
60
  fullEntityJson: 'Full Entity JSON',
61
  rankScore: 'Rank Score',
62
+ fullScore: 'Full Score',
63
+ matchedChunk: 'Matched Chunk',
64
+ subcategory: 'Subcategory',
65
+ emirate: 'Emirate',
66
+ model: 'Model',
67
+ source: 'Source',
68
  viewEntity: 'View Entity',
69
  entityData: 'Entity Data',
70
  pleaseEnterQuery: 'Please enter a search query',
 
124
  enterQuery: 'أدخل استعلامك للبحث في قاعدة المعرفة',
125
  selectCategoryHint: 'اختر فئة وانقر للبحث',
126
  mustKnowFacts: '✓ حقائق أساسية',
127
+ sensitiveTopics: '⚠️ مواضيع حساسة',
128
+ sensitivityRating: 'الحساسية',
129
+ sensitivityHigh: '🔴 عالية',
130
+ sensitivityMedium: '🟡 متوسطة',
131
+ sensitivityLow: '🟢 منخفضة',
132
+ noSensitiveTopics: 'لم يتم تحديد مواضيع حساسة',
133
+ problematicFraming: 'الصياغة الإشكالية',
134
+ responseGuide: 'دليل الاستجابة',
135
+ strategy: 'الاستراتيجية',
136
+ tone: 'النبرة',
137
+ keyFacts: 'الحقائق الرئيسية',
138
+ suggestedResponse: 'الاستجابة المقترحة',
139
  relevance: 'الصلة؟',
140
  helpful: 'مفيد؟',
141
+ sensitivityHandling: 'معالجة الحساسية؟',
142
+ feedbackForEntity: 'ملاحظاتك على هذا الكيان...',
143
+ submitEntityFeedback: 'إرسال الملاحظات',
144
+ feedbackSavedForEntity: 'تم حفظ الملاحظات لهذا الكيان ✓',
145
  detailedAnalysis: 'تحليل مفصل',
146
  fullEntityJson: 'بيانات الكيان الكاملة',
147
+ rankScore: 'درجة الترتيب',
148
+ fullScore: 'الدرجة الكاملة',
149
+ matchedChunk: 'القطعة المطابقة',
150
+ subcategory: 'الفئة الفرعية',
151
+ emirate: 'الإمارة',
152
+ model: 'النموذج',
153
+ source: 'المصدر',
154
  viewEntity: 'عرض الكيان',
155
  entityData: 'بيانات الكيان',
156
  pleaseEnterQuery: 'الرجاء إدخال استعلام البحث',
 
210
  enterQuery: '在上方输入查询以搜索阿联酋知识库',
211
  selectCategoryHint: '选择类别并点击搜索开始',
212
  mustKnowFacts: '✓ 必知事实',
213
+ sensitiveTopics: '⚠️ 敏感话题',
214
+ sensitivityRating: '敏感度',
215
+ sensitivityHigh: '🔴 高',
216
+ sensitivityMedium: '🟡 中',
217
+ sensitivityLow: '🟢 低',
218
+ noSensitiveTopics: '未发现敏感话题',
219
+ problematicFraming: '问题性表述',
220
+ responseGuide: '回应指南',
221
+ strategy: '策略',
222
+ tone: '语气',
223
+ keyFacts: '关键事实',
224
+ suggestedResponse: '建议回应',
225
  relevance: '相关性?',
226
  helpful: '有帮助?',
227
+ sensitivityHandling: '敏感处理?',
228
+ feedbackForEntity: '您对此实体的反馈...',
229
+ submitEntityFeedback: '提交反馈',
230
+ feedbackSavedForEntity: '此实体的反馈已保存 ✓',
231
  detailedAnalysis: '详细分析',
232
  fullEntityJson: '完整实体JSON',
233
+ rankScore: '排分数',
234
+ fullScore: '完整分数',
235
+ matchedChunk: '匹配块',
236
+ subcategory: '子类别',
237
+ emirate: '酋长国',
238
+ model: '模型',
239
+ source: '数据来源',
240
  viewEntity: '查看实体',
241
  entityData: '实体数据',
242
  pleaseEnterQuery: '请输入搜索查询',
 
296
  currentQuery: '',
297
  currentCategory: null,
298
  results: [],
299
+ ratings: {}, // { entityIndex: { relevance: 0|1, helpful: true|false, sensitivityHandling: true|false } }
300
+ entityFeedbacks: {}, // { entityIndex: { comment: '', submitted: false } }
301
+ queryId: null, // UUID for each search session
302
  isLoading: false,
303
  language: localStorage.getItem('uae_lang') || 'en',
304
  // Translation state
 
311
  resultsPerPage: 10
312
  };
313
 
314
+ // Generate UUID for query tracking
315
+ function generateUUID() {
316
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
317
+ const r = Math.random() * 16 | 0;
318
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
319
+ return v.toString(16);
320
+ });
321
+ }
322
+
323
  // ============================================
324
  // DOM ELEMENTS
325
  // ============================================
 
374
  if (!e.target.closest('.category-dropdown')) {
375
  DOM.categoryDropdown?.classList.remove('active');
376
  }
377
+ // Close rank-details dropdowns when clicking outside
378
+ if (!e.target.closest('.rank-details')) {
379
+ document.querySelectorAll('.rank-details[open]').forEach(details => {
380
+ details.removeAttribute('open');
381
+ });
382
+ }
383
  });
384
 
385
  // Modals
 
436
  state.currentQuery = query;
437
  state.isLoading = true;
438
  state.ratings = {};
439
+ state.entityFeedbacks = {};
440
+ state.queryId = generateUUID(); // Generate new query_id for this search
441
  // Reset translation state for new search
442
  state.translatedResults = {};
443
  state.showOriginal = false;
 
691
  // Get translated or original content
692
  const content = getResultContent(result, index);
693
 
694
+ // Get ranking details
695
+ const chunkType = result.chunk_type || 'unknown';
696
+ const subcategory = result.subcategory || '';
697
+ const emirate = result.emirate || '';
698
+ // Get data source (wiki, dhow, scrapped, controversial) - deduplicate first
699
+ const dataSources = [...new Set(result.full_entity?.data_sources || [])];
700
+ const sourceDisplay = dataSources.map(s => {
701
+ if (s.includes('wiki')) return 'Wiki';
702
+ if (s.includes('dhow')) return 'Dhow';
703
+ if (s.includes('scrapp')) return 'Scrapped';
704
+ if (s.includes('controversial')) return 'Controversial';
705
+ return s;
706
+ }).join(', ') || '';
707
+
708
  return `
709
  <div class="bg-white rounded shadow-xl border border-gray-200 result-card mb-4 md:mb-6 overflow-x-hidden" data-index="${index}">
710
  <!-- Card Header -->
 
713
  <span class="text-amber-400">${index === 0 ? '🦅' : '📄'}</span>
714
  #${index + 1} ${escapeHtml(content.entityName)}
715
  </div>
716
+ <details class="rank-details self-start sm:self-auto">
717
+ <summary class="text-[12px] md:text-[16px] font-medium gold-button-slender text-emerald-950 px-3 md:px-4 rounded shadow-sm cursor-pointer list-none">
718
+ ${t('rankScore')}: ${result.score.toFixed(2)} ▼
719
+ </summary>
720
+ <div class="absolute right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg p-3 z-50 min-w-[200px] text-[12px] text-gray-700">
721
+ <div class="space-y-2">
722
+ <div class="flex justify-between">
723
+ <span class="text-gray-500">${t('fullScore')}:</span>
724
+ <span class="font-mono font-medium">${result.score.toFixed(6)}</span>
725
+ </div>
726
+ <div class="flex justify-between">
727
+ <span class="text-gray-500">${t('matchedChunk')}:</span>
728
+ <span class="font-medium capitalize">${chunkType}</span>
729
+ </div>
730
+ ${subcategory ? `
731
+ <div class="flex justify-between">
732
+ <span class="text-gray-500">${t('subcategory')}:</span>
733
+ <span class="font-medium">${escapeHtml(subcategory)}</span>
734
+ </div>
735
+ ` : ''}
736
+ ${emirate ? `
737
+ <div class="flex justify-between">
738
+ <span class="text-gray-500">${t('emirate')}:</span>
739
+ <span class="font-medium">${escapeHtml(emirate)}</span>
740
+ </div>
741
+ ` : ''}
742
+ ${sourceDisplay ? `
743
+ <div class="flex justify-between">
744
+ <span class="text-gray-500">${t('source')}:</span>
745
+ <span class="font-medium">${escapeHtml(sourceDisplay)}</span>
746
+ </div>
747
+ ` : ''}
748
+ <div class="flex justify-between border-t pt-2 mt-2">
749
+ <span class="text-gray-500">${t('model')}:</span>
750
+ <span class="font-medium text-emerald-700">bge-m3</span>
751
+ </div>
752
+ </div>
753
+ </div>
754
+ </details>
755
  </div>
756
 
757
  <!-- Card Body -->
 
764
  </p>
765
  </div>
766
 
767
+ <!-- Sensitive Topics -->
768
  <div class="w-full md:w-[38%] pt-1">
769
+ <h4 class="text-emerald-800 font-medium text-[14px] md:text-[18px] mb-3 md:mb-4">${t('sensitiveTopics')}</h4>
770
+ ${renderSensitiveTopics(result, index, content.sensitiveTopics)}
 
 
 
 
771
  </div>
772
  </div>
773
 
774
+ <!-- Rating & Feedback Row -->
775
  <div class="mt-6 md:mt-8 border-t border-gray-50 pt-4 md:pt-6 flex flex-col gap-4">
776
+ <div class="flex flex-wrap items-center gap-4 md:gap-8">
777
  <!-- Relevance Rating -->
778
  <div class="flex items-center gap-2 md:gap-3">
779
  <span class="text-[12px] md:text-[16px] font-medium text-gray-400 tracking-wider">${t('relevance')}</span>
 
795
  onclick="setRating(${index}, 'helpful', false)">👎</button>
796
  </div>
797
  </div>
798
+
799
+ <!-- Sensitivity Handling Rating -->
800
+ <div class="flex items-center gap-2 md:gap-3">
801
+ <span class="text-[12px] md:text-[16px] font-medium text-gray-400 tracking-wider">${t('sensitivityHandling')}</span>
802
+ <div class="flex gap-2 text-base md:text-lg">
803
+ <button class="rating-btn ${state.ratings[index]?.sensitivityHandling === true ? 'active' : ''}"
804
+ onclick="setRating(${index}, 'sensitivityHandling', true)">✅</button>
805
+ <button class="rating-btn ${state.ratings[index]?.sensitivityHandling === false ? 'active' : ''}"
806
+ onclick="setRating(${index}, 'sensitivityHandling', false)">❌</button>
807
+ </div>
808
+ </div>
809
+ </div>
810
+
811
+ <!-- Per-Entity Feedback -->
812
+ <div class="flex flex-col sm:flex-row gap-2 mt-2">
813
+ <input type="text"
814
+ id="entity-feedback-${index}"
815
+ class="flex-1 px-3 py-2 border border-gray-300 rounded text-[14px] focus:outline-none focus:border-emerald-500"
816
+ placeholder="${t('feedbackForEntity')}"
817
+ value="${state.entityFeedbacks[index]?.comment || ''}"
818
+ ${state.entityFeedbacks[index]?.submitted ? 'disabled' : ''}
819
+ onchange="updateEntityComment(${index}, this.value)">
820
+ <button class="px-4 py-2 text-[14px] font-medium rounded shadow-sm ${state.entityFeedbacks[index]?.submitted ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-emerald-700 text-white hover:bg-emerald-800'}"
821
+ onclick="submitEntityFeedback(${index})"
822
+ ${state.entityFeedbacks[index]?.submitted ? 'disabled' : ''}>
823
+ ${state.entityFeedbacks[index]?.submitted ? t('feedbackSavedForEntity') : t('submitEntityFeedback')}
824
+ </button>
825
  </div>
826
 
827
  <!-- Detailed Analysis Button -->
 
896
  }
897
 
898
  // ============================================
899
+ // RATINGS (stored locally, saved on submit)
900
  // ============================================
901
+ window.setRating = function(entityIndex, dimension, value) {
902
  if (!state.ratings[entityIndex]) {
903
  state.ratings[entityIndex] = {};
904
  }
 
906
 
907
  // Re-render to update button states
908
  renderResults();
909
+ };
910
+
911
+ // ============================================
912
+ // SENSITIVE TOPICS RENDERING
913
+ // ============================================
914
+ function renderSensitiveTopics(result, index, translatedTopics) {
915
+ const sensitiveTopics = result.full_entity?.sensitive_topics || result.sensitive_topics;
916
+
917
+ // Check if entity has sensitive content
918
+ if (!sensitiveTopics || !sensitiveTopics.has_sensitive_content || !sensitiveTopics.topics || sensitiveTopics.topics.length === 0) {
919
+ return `
920
+ <div class="text-[14px] md:text-[16px] text-gray-500">
921
+ <span class="inline-block px-2 py-1 rounded bg-green-100 text-green-700 text-[12px] md:text-[14px] mb-2">
922
+ ${t('sensitivityLow')}
923
+ </span>
924
+ <p class="mt-2">${t('noSensitiveTopics')}</p>
925
+ </div>
926
+ `;
927
+ }
928
+
929
+ const topics = sensitiveTopics.topics;
930
+
931
+ // Determine overall sensitivity rating
932
+ const hasHighSeverity = topics.some(topic => typeof topic === 'object' && topic.severity === 'high');
933
+ const sensitivityRating = hasHighSeverity ? 'high' : 'medium';
934
+ const ratingLabel = sensitivityRating === 'high' ? t('sensitivityHigh') : t('sensitivityMedium');
935
+ const ratingClass = sensitivityRating === 'high' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700';
936
+
937
+ // Render topics
938
+ const topicsHtml = topics.map((topic, topicIndex) => {
939
+ // Get translated content for this topic (if available)
940
+ const trans = translatedTopics?.[topicIndex] || {};
941
+
942
+ // Handle string topics (malformed data)
943
+ if (typeof topic === 'string') {
944
+ const displayText = trans.stringTopic || topic;
945
+ return `
946
+ <div class="bg-gray-50 p-3 rounded border border-gray-200 mb-2">
947
+ <p class="text-[13px] md:text-[15px] text-gray-700">${escapeHtml(displayText)}</p>
948
+ </div>
949
+ `;
950
+ }
951
+
952
+ // Handle proper topic objects
953
+ const topicType = topic.topic_type || 'unknown';
954
+ const severity = topic.severity || 'medium';
955
+ const severityClass = severity === 'high' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700';
956
+ const severityIcon = severity === 'high' ? '🔴' : '🟡';
957
+
958
+ // Use translated content or fall back to original
959
+ const problematicFraming = trans.framing || topic.problematic_framing || '';
960
+ const appropriateResponse = topic.appropriate_response || {};
961
+ const strategy = trans.strategy || appropriateResponse.strategy || '';
962
+ const tone = trans.tone || appropriateResponse.tone || '';
963
+ const suggestedResponse = trans.suggested || appropriateResponse.suggested_response || '';
964
+ const keyFacts = (trans.keyFacts && trans.keyFacts.length > 0)
965
+ ? trans.keyFacts
966
+ : (appropriateResponse.key_facts || []);
967
+
968
+ return `
969
+ <div class="bg-gray-50 p-3 rounded border border-gray-200 mb-3">
970
+ <!-- Topic Header -->
971
+ <div class="flex items-center gap-2 mb-2">
972
+ <span class="text-[12px] px-2 py-0.5 rounded ${severityClass}">${severityIcon} ${topicType}</span>
973
+ </div>
974
+
975
+ <!-- Problematic Framing -->
976
+ ${problematicFraming ? `
977
+ <div class="mb-2">
978
+ <span class="text-[11px] md:text-[13px] font-medium text-gray-500">${t('problematicFraming')}:</span>
979
+ <p class="text-[13px] md:text-[15px] text-gray-800 mt-1 italic">"${escapeHtml(problematicFraming)}"</p>
980
+ </div>
981
+ ` : ''}
982
+
983
+ <!-- Collapsible Response Guide -->
984
+ ${strategy || keyFacts.length > 0 || suggestedResponse ? `
985
+ <details class="mt-2">
986
+ <summary class="text-[12px] md:text-[14px] text-emerald-700 cursor-pointer hover:text-emerald-900 font-medium">
987
+ ${t('responseGuide')}
988
+ </summary>
989
+ <div class="mt-2 pl-3 border-l-2 border-emerald-200 text-[12px] md:text-[14px]">
990
+ ${strategy ? `
991
+ <p class="mb-1"><span class="font-medium text-gray-600">${t('strategy')}:</span> ${escapeHtml(strategy)}</p>
992
+ ` : ''}
993
+ ${tone ? `
994
+ <p class="mb-1"><span class="font-medium text-gray-600">${t('tone')}:</span> ${escapeHtml(tone)}</p>
995
+ ` : ''}
996
+ ${keyFacts.length > 0 ? `
997
+ <div class="mb-1">
998
+ <span class="font-medium text-gray-600">${t('keyFacts')}:</span>
999
+ <ul class="list-disc list-inside mt-1 text-gray-700">
1000
+ ${keyFacts.map(fact => `<li>${escapeHtml(fact)}</li>`).join('')}
1001
+ </ul>
1002
+ </div>
1003
+ ` : ''}
1004
+ ${suggestedResponse ? `
1005
+ <div class="mt-2">
1006
+ <span class="font-medium text-gray-600">${t('suggestedResponse')}:</span>
1007
+ <p class="mt-1 text-gray-700 bg-white p-2 rounded border">${escapeHtml(suggestedResponse)}</p>
1008
+ </div>
1009
+ ` : ''}
1010
+ </div>
1011
+ </details>
1012
+ ` : ''}
1013
+ </div>
1014
+ `;
1015
+ }).join('');
1016
+
1017
+ return `
1018
+ <div>
1019
+ <span class="inline-block px-2 py-1 rounded ${ratingClass} text-[12px] md:text-[14px] mb-3">
1020
+ ${t('sensitivityRating')}: ${ratingLabel}
1021
+ </span>
1022
+ <div class="mt-2 max-h-[300px] overflow-y-auto">
1023
+ ${topicsHtml}
1024
+ </div>
1025
+ </div>
1026
+ `;
1027
+ }
1028
+
1029
+ // ============================================
1030
+ // PER-ENTITY FEEDBACK
1031
+ // ============================================
1032
+ window.updateEntityComment = function(index, value) {
1033
+ if (!state.entityFeedbacks[index]) {
1034
+ state.entityFeedbacks[index] = { comment: '', submitted: false };
1035
+ }
1036
+ state.entityFeedbacks[index].comment = value;
1037
+ };
1038
+
1039
+ window.submitEntityFeedback = async function(index) {
1040
+ const result = state.results[index];
1041
+ if (!result) return;
1042
+
1043
+ const feedback = state.entityFeedbacks[index] || { comment: '' };
1044
+ const ratings = state.ratings[index] || {};
1045
 
 
1046
  try {
1047
+ const response = await fetch(`${CONFIG.API_BASE}/entity-feedback`, {
 
1048
  method: 'POST',
1049
  headers: { 'Content-Type': 'application/json' },
1050
  body: JSON.stringify({
1051
+ query_id: state.queryId,
1052
  query: state.currentQuery,
1053
+ query_timestamp: new Date().toISOString(),
1054
+ entity_id: result.entity_id || '',
1055
+ entity_name: result.entity_name || '',
1056
+ rank_position: index + 1,
1057
+ rank_score: result.score || 0,
1058
+ ratings: {
1059
+ relevance: ratings.relevance !== undefined ? (ratings.relevance === 1) : null,
1060
+ helpful: ratings.helpful !== undefined ? ratings.helpful : null,
1061
+ sensitivity_handling: ratings.sensitivityHandling !== undefined ? ratings.sensitivityHandling : null
1062
+ },
1063
+ comment: feedback.comment || '',
1064
+ submitted_at: new Date().toISOString()
1065
  })
1066
  });
1067
 
1068
  const data = await response.json();
1069
+
1070
  if (data.success) {
1071
+ // Mark as submitted
1072
+ if (!state.entityFeedbacks[index]) {
1073
+ state.entityFeedbacks[index] = { comment: '', submitted: false };
1074
+ }
1075
+ state.entityFeedbacks[index].submitted = true;
1076
+
1077
+ showToast(t('feedbackSavedForEntity'), 'success');
1078
+ renderResults(); // Re-render to update UI
1079
+ } else {
1080
+ throw new Error(data.error || 'Failed to save feedback');
1081
  }
1082
  } catch (error) {
1083
+ console.error('Failed to save entity feedback:', error);
1084
+ showToast(`Error: ${error.message}`, 'error');
1085
  }
1086
  };
1087
 
 
1286
  textsToTranslate.push(fact);
1287
  textMap.push({ index, field: 'fact', factIndex });
1288
  });
1289
+
1290
+ // Add sensitive topics content
1291
+ const sensitiveTopics = result.full_entity?.sensitive_topics || result.sensitive_topics;
1292
+ if (sensitiveTopics?.has_sensitive_content && sensitiveTopics?.topics) {
1293
+ sensitiveTopics.topics.forEach((topic, topicIndex) => {
1294
+ if (typeof topic === 'object') {
1295
+ // Problematic framing
1296
+ if (topic.problematic_framing) {
1297
+ textsToTranslate.push(topic.problematic_framing);
1298
+ textMap.push({ index, field: 'sensitiveTopicFraming', topicIndex });
1299
+ }
1300
+ // Appropriate response fields
1301
+ const response = topic.appropriate_response || {};
1302
+ if (response.strategy) {
1303
+ textsToTranslate.push(response.strategy);
1304
+ textMap.push({ index, field: 'sensitiveTopicStrategy', topicIndex });
1305
+ }
1306
+ if (response.tone) {
1307
+ textsToTranslate.push(response.tone);
1308
+ textMap.push({ index, field: 'sensitiveTopicTone', topicIndex });
1309
+ }
1310
+ if (response.suggested_response) {
1311
+ textsToTranslate.push(response.suggested_response);
1312
+ textMap.push({ index, field: 'sensitiveTopicSuggested', topicIndex });
1313
+ }
1314
+ // Key facts (array)
1315
+ (response.key_facts || []).forEach((fact, factIdx) => {
1316
+ textsToTranslate.push(fact);
1317
+ textMap.push({ index, field: 'sensitiveTopicKeyFact', topicIndex, factIdx });
1318
+ });
1319
+ } else if (typeof topic === 'string') {
1320
+ // Malformed string topic
1321
+ textsToTranslate.push(topic);
1322
+ textMap.push({ index, field: 'sensitiveTopicString', topicIndex });
1323
+ }
1324
+ });
1325
+ }
1326
  });
1327
 
1328
  if (textsToTranslate.length === 0) {
 
1348
  }
1349
 
1350
  data.translations.forEach((translated, i) => {
1351
+ const { index, field, factIndex, topicIndex, factIdx } = textMap[i];
1352
 
1353
  if (!state.translatedResults[lang][index]) {
1354
  state.translatedResults[lang][index] = {
1355
  entityName: null,
1356
  summary: null,
1357
+ facts: [],
1358
+ sensitiveTopics: []
1359
  };
1360
  }
1361
 
 
1365
  state.translatedResults[lang][index].summary = translated;
1366
  } else if (field === 'fact') {
1367
  state.translatedResults[lang][index].facts[factIndex] = translated;
1368
+ } else if (field.startsWith('sensitiveTopic')) {
1369
+ // Initialize topic translation object if needed
1370
+ if (!state.translatedResults[lang][index].sensitiveTopics[topicIndex]) {
1371
+ state.translatedResults[lang][index].sensitiveTopics[topicIndex] = {
1372
+ framing: null,
1373
+ strategy: null,
1374
+ tone: null,
1375
+ suggested: null,
1376
+ keyFacts: [],
1377
+ stringTopic: null
1378
+ };
1379
+ }
1380
+ const topicTrans = state.translatedResults[lang][index].sensitiveTopics[topicIndex];
1381
+ if (field === 'sensitiveTopicFraming') {
1382
+ topicTrans.framing = translated;
1383
+ } else if (field === 'sensitiveTopicStrategy') {
1384
+ topicTrans.strategy = translated;
1385
+ } else if (field === 'sensitiveTopicTone') {
1386
+ topicTrans.tone = translated;
1387
+ } else if (field === 'sensitiveTopicSuggested') {
1388
+ topicTrans.suggested = translated;
1389
+ } else if (field === 'sensitiveTopicKeyFact') {
1390
+ topicTrans.keyFacts[factIdx] = translated;
1391
+ } else if (field === 'sensitiveTopicString') {
1392
+ topicTrans.stringTopic = translated;
1393
+ }
1394
  }
1395
  });
1396
  }
 
1420
  summary: state.translatedResults[lang][index].summary || result.summary,
1421
  facts: state.translatedResults[lang][index].facts.length > 0
1422
  ? state.translatedResults[lang][index].facts
1423
+ : (result.must_answer || []),
1424
+ sensitiveTopics: state.translatedResults[lang][index].sensitiveTopics || []
1425
  };
1426
  }
1427
 
1428
  return {
1429
  entityName: result.entity_name,
1430
  summary: result.summary,
1431
+ facts: result.must_answer || [],
1432
+ sensitiveTopics: null // Use original from result
1433
  };
1434
  }
1435
 
ir/cache/dense_index/chunk_metadata_bge-m3.json CHANGED
The diff for this file is too large to render. See raw diff
 
ir/cache/dense_index/faiss_index_bge-m3.bin CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:6483c97df94b26f917736fa402a8e36afb723d2d288c327ee78cd0e6bf8c4e60
3
- size 22290477
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d0999c5f221ce46a270716a2fdd152a46953e94512857c8c004aa206340c59ad
3
+ size 30847021
ir/demo.py CHANGED
@@ -611,24 +611,21 @@ def format_single_entity(entity, rank: int, score: float, match_type: str) -> st
611
  def process_query(query: str, category: str) -> tuple:
612
  """
613
  Process a query using Level 4 (Dense) retrieval.
 
614
 
615
  Args:
616
  query: User query
617
  category: Question category
618
 
619
  Returns:
620
- (e1, e2, e3, e4, e5, sensitive, debug, full_results)
621
  """
622
- empty = "**#%d** - (no result)"
623
-
624
  if not query.strip():
625
- warn = "⚠️ Please enter a question 请输入问题"
626
- return (warn, empty % 2, empty % 3, empty % 4, empty % 5, "", "", "")
627
 
628
  # Require category selection
629
  if not category:
630
- warn = "⚠️ **Please select Category first! 请先选择类别!**"
631
- return (warn, empty % 2, empty % 3, empty % 4, empty % 5, "", "", "")
632
 
633
  try:
634
  # Always use Level 4 (Dense) retriever
@@ -639,50 +636,169 @@ def process_query(query: str, category: str) -> tuple:
639
  start = time.perf_counter()
640
 
641
  # Dense retriever uses search() and returns (metadata, score) tuples
642
- results = retriever.search(query, top_k=5)
 
643
  latency = (time.perf_counter() - start) * 1000
644
 
645
- # Format dense results
646
- entity_outputs = []
647
- results_for_feedback = []
 
 
 
 
 
 
 
648
 
649
- for i in range(5):
650
- if i < len(results):
651
- metadata, score = results[i]
652
- entity_outputs.append(format_dense_result(metadata, score, i + 1, kb))
653
- results_for_feedback.append(
654
- f"#{i+1}: {metadata.get('entity_id', '')} ({metadata.get('entity_name', '')}) - {score:.3f}"
655
- )
656
- else:
657
- entity_outputs.append(f"**#{i+1}** - (no result)")
658
 
659
  # No sensitive detection for dense retrieval
660
  sensitive_text = "ℹ️ Sensitive detection not available for dense retrieval"
661
- full_entity_text = " | ".join(results_for_feedback)
662
 
663
  # Debug info
664
  debug_lines = [
665
  f"⏱️ Latency: {latency:.2f}ms",
666
  f"🔢 Level: 4 (Dense bge-m3)",
667
- f"📊 Found: {len(results)}",
668
  ]
669
  debug_text = " | ".join(debug_lines)
670
 
671
  return (
672
- entity_outputs[0],
673
- entity_outputs[1],
674
- entity_outputs[2],
675
- entity_outputs[3],
676
- entity_outputs[4],
677
  sensitive_text,
678
  debug_text,
679
- full_entity_text
 
680
  )
681
 
682
  except Exception as e:
683
  import traceback
684
  error_msg = f"❌ Error: {str(e)}\n<small>{traceback.format_exc()[:500]}</small>"
685
- return (error_msg, empty % 2, empty % 3, empty % 4, empty % 5, "", "", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
 
687
 
688
  def parse_score(score_str: str) -> int:
@@ -695,75 +811,120 @@ def parse_score(score_str: str) -> int:
695
  return 0
696
 
697
 
698
- def save_feedback(
699
  query: str,
700
  category: str,
701
- entity_results: str,
702
- sensitive_results: str,
703
- # Per-entity scores (5 entities x 2 dimensions)
704
- e1_rel: str, e1_suf: str,
705
- e2_rel: str, e2_suf: str,
706
- e3_rel: str, e3_suf: str,
707
- e4_rel: str, e4_suf: str,
708
- e5_rel: str, e5_suf: str,
709
- # Sensitive handling
710
- score_sensitive: str,
711
- notes: str,
712
  request: gr.Request
713
  ) -> str:
714
- """Save feedback to JSON file with per-entity scoring"""
715
-
716
  # Get client IP address
717
  client_ip = "unknown"
718
  if request:
719
- # Try to get real IP from headers (for proxied requests)
720
  client_ip = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
721
  if not client_ip:
722
  client_ip = request.client.host if request.client else "unknown"
723
-
724
- # Parse per-entity scores
725
- entity_scores = [
726
- {"relevance": parse_score(e1_rel), "sufficient": parse_score(e1_suf)},
727
- {"relevance": parse_score(e2_rel), "sufficient": parse_score(e2_suf)},
728
- {"relevance": parse_score(e3_rel), "sufficient": parse_score(e3_suf)},
729
- {"relevance": parse_score(e4_rel), "sufficient": parse_score(e4_suf)},
730
- {"relevance": parse_score(e5_rel), "sufficient": parse_score(e5_suf)},
731
- ]
732
-
 
 
 
733
  feedback = {
734
  "timestamp": datetime.now().isoformat(),
735
  "client_ip": client_ip,
736
  "query": query,
737
  "category": category or "Not selected",
738
- "entity_scores": entity_scores,
739
- "sensitive_handling": parse_score(score_sensitive),
740
- "notes": notes,
741
- "entity_results": entity_results, # Contains IDs
742
- "sensitive_results": sensitive_results[:500],
 
743
  }
744
-
745
  # Save to file - use /data for HF Spaces persistence
746
  if Path("/data").exists():
747
  feedback_file = Path("/data/demo_feedback.json")
748
  else:
749
  feedback_file = Path(__file__).parent / "demo_feedback.json"
750
-
751
  try:
752
  if feedback_file.exists():
753
  with open(feedback_file, "r", encoding="utf-8") as f:
754
  all_feedback = json.load(f)
755
  else:
756
  all_feedback = []
757
-
758
  all_feedback.append(feedback)
759
-
760
  with open(feedback_file, "w", encoding="utf-8") as f:
761
  json.dump(all_feedback, f, ensure_ascii=False, indent=2)
762
-
763
- return f" Feedback saved! 评分已保存!Total 共收集: {len(all_feedback)} entries 条反馈"
764
-
 
765
  except Exception as e:
766
- return f"❌ Save failed 保存失败: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
 
768
 
769
  def create_demo():
@@ -979,7 +1140,7 @@ def create_demo():
979
  """)
980
 
981
  # ========================================
982
- # RESULTS SECTION
983
  # ========================================
984
  gr.HTML("""
985
  <div style="
@@ -994,7 +1155,7 @@ def create_demo():
994
  gap: 10px;
995
  font-family: 'Inter', sans-serif;
996
  ">
997
- 📊 Results
998
  </div>
999
  """)
1000
 
@@ -1013,70 +1174,22 @@ def create_demo():
1013
  </div>
1014
  """)
1015
 
1016
- # Entity 1 - Card style
1017
- with gr.Group(elem_classes="result-card"):
1018
- with gr.Row():
1019
- with gr.Column(scale=4):
1020
- entity_1_output = gr.Markdown(value="**#1** - Enter a query to search...")
1021
- with gr.Column(scale=1, min_width=150):
1022
- score_e1_relevance = gr.Radio(
1023
- choices=["0", "1", "2"], label="Relevance", value="0", interactive=True
1024
- )
1025
- score_e1_sufficient = gr.Radio(
1026
- choices=["0", "1", "2"], label="Sufficient", value="0", interactive=True
1027
- )
1028
-
1029
- # Entity 2
1030
- with gr.Group(elem_classes="result-card"):
1031
- with gr.Row():
1032
- with gr.Column(scale=4):
1033
- entity_2_output = gr.Markdown(value="**#2** - ...")
1034
- with gr.Column(scale=1, min_width=150):
1035
- score_e2_relevance = gr.Radio(
1036
- choices=["0", "1", "2"], label="Relevance", value="0", interactive=True
1037
- )
1038
- score_e2_sufficient = gr.Radio(
1039
- choices=["0", "1", "2"], label="Sufficient", value="0", interactive=True
1040
- )
1041
-
1042
- # Entity 3
1043
- with gr.Group(elem_classes="result-card"):
1044
- with gr.Row():
1045
- with gr.Column(scale=4):
1046
- entity_3_output = gr.Markdown(value="**#3** - ...")
1047
- with gr.Column(scale=1, min_width=150):
1048
- score_e3_relevance = gr.Radio(
1049
- choices=["0", "1", "2"], label="Relevance", value="0", interactive=True
1050
- )
1051
- score_e3_sufficient = gr.Radio(
1052
- choices=["0", "1", "2"], label="Sufficient", value="0", interactive=True
1053
- )
1054
-
1055
- # Entity 4
1056
- with gr.Group(elem_classes="result-card"):
1057
- with gr.Row():
1058
- with gr.Column(scale=4):
1059
- entity_4_output = gr.Markdown(value="**#4** - ...")
1060
- with gr.Column(scale=1, min_width=150):
1061
- score_e4_relevance = gr.Radio(
1062
- choices=["0", "1", "2"], label="Relevance", value="0", interactive=True
1063
- )
1064
- score_e4_sufficient = gr.Radio(
1065
- choices=["0", "1", "2"], label="Sufficient", value="0", interactive=True
1066
- )
1067
 
1068
- # Entity 5
1069
- with gr.Group(elem_classes="result-card"):
1070
- with gr.Row():
1071
- with gr.Column(scale=4):
1072
- entity_5_output = gr.Markdown(value="**#5** - ...")
1073
- with gr.Column(scale=1, min_width=150):
1074
- score_e5_relevance = gr.Radio(
1075
- choices=["0", "1", "2"], label="Relevance", value="0", interactive=True
1076
- )
1077
- score_e5_sufficient = gr.Radio(
1078
- choices=["0", "1", "2"], label="Sufficient", value="0", interactive=True
1079
- )
1080
 
1081
  # ========================================
1082
  # SENSITIVE TOPIC DETECTION
@@ -1133,6 +1246,39 @@ def create_demo():
1133
  </div>
1134
  """)
1135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1136
  with gr.Row():
1137
  notes = gr.Textbox(
1138
  label="Notes (optional)",
@@ -1140,54 +1286,83 @@ def create_demo():
1140
  lines=2,
1141
  scale=3
1142
  )
1143
- feedback_btn = gr.Button("Submit", variant="primary", size="lg", scale=1, elem_classes="submit-btn")
1144
 
1145
  feedback_output = gr.Markdown()
1146
-
1147
- # Hidden field to store full entity results for feedback
1148
- entity_output = gr.State(value="")
1149
-
1150
- # Wire up events - 5 entity outputs + sensitive + debug + state
1151
- all_outputs = [
1152
- entity_1_output,
1153
- entity_2_output,
1154
- entity_3_output,
1155
- entity_4_output,
1156
- entity_5_output,
1157
  sensitive_output,
1158
  debug_output,
1159
- entity_output # State for full results (for feedback)
 
1160
  ]
1161
-
1162
  submit_btn.click(
1163
  fn=process_query,
1164
  inputs=[query_input, category_dropdown],
1165
- outputs=all_outputs,
1166
  )
1167
 
1168
  query_input.submit(
1169
  fn=process_query,
1170
  inputs=[query_input, category_dropdown],
1171
- outputs=all_outputs,
1172
  )
1173
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1174
  feedback_btn.click(
1175
- fn=save_feedback,
1176
- inputs=[
1177
- query_input,
1178
- category_dropdown,
1179
- entity_output,
1180
- sensitive_output,
1181
- # Per-entity scores
1182
- score_e1_relevance, score_e1_sufficient,
1183
- score_e2_relevance, score_e2_sufficient,
1184
- score_e3_relevance, score_e3_sufficient,
1185
- score_e4_relevance, score_e4_sufficient,
1186
- score_e5_relevance, score_e5_sufficient,
1187
- # Sensitive
1188
- score_sensitive,
1189
- notes,
1190
- ],
1191
  outputs=[feedback_output],
1192
  )
1193
 
 
611
  def process_query(query: str, category: str) -> tuple:
612
  """
613
  Process a query using Level 4 (Dense) retrieval.
614
+ Returns top 100 results for pagination.
615
 
616
  Args:
617
  query: User query
618
  category: Question category
619
 
620
  Returns:
621
+ (results_display, sensitive, debug, all_results_data, current_page)
622
  """
 
 
623
  if not query.strip():
624
+ return ("⚠️ Please enter a question 请输入问题", "", "", [], 1)
 
625
 
626
  # Require category selection
627
  if not category:
628
+ return ("⚠️ **Please select Category first! 请先选择类别!**", "", "", [], 1)
 
629
 
630
  try:
631
  # Always use Level 4 (Dense) retriever
 
636
  start = time.perf_counter()
637
 
638
  # Dense retriever uses search() and returns (metadata, score) tuples
639
+ # Fetch top 100 results
640
+ results = retriever.search(query, top_k=100)
641
  latency = (time.perf_counter() - start) * 1000
642
 
643
+ # Store all results data for pagination
644
+ all_results_data = []
645
+ for i, (metadata, score) in enumerate(results):
646
+ all_results_data.append({
647
+ "rank": i + 1,
648
+ "metadata": metadata,
649
+ "score": score,
650
+ "entity_id": metadata.get("entity_id", ""),
651
+ "entity_name": metadata.get("entity_name", "Unknown"),
652
+ })
653
 
654
+ # Format first page (10 results)
655
+ results_display = format_results_page(all_results_data, 1, kb)
 
 
 
 
 
 
 
656
 
657
  # No sensitive detection for dense retrieval
658
  sensitive_text = "ℹ️ Sensitive detection not available for dense retrieval"
 
659
 
660
  # Debug info
661
  debug_lines = [
662
  f"⏱️ Latency: {latency:.2f}ms",
663
  f"🔢 Level: 4 (Dense bge-m3)",
664
+ f"📊 Found: {len(results)} results",
665
  ]
666
  debug_text = " | ".join(debug_lines)
667
 
668
  return (
669
+ results_display,
 
 
 
 
670
  sensitive_text,
671
  debug_text,
672
+ all_results_data,
673
+ 1 # Current page starts at 1
674
  )
675
 
676
  except Exception as e:
677
  import traceback
678
  error_msg = f"❌ Error: {str(e)}\n<small>{traceback.format_exc()[:500]}</small>"
679
+ return (error_msg, "", "", [], 1)
680
+
681
+
682
+ RESULTS_PER_PAGE = 10
683
+
684
+
685
+ def format_results_page(all_results: List[Dict], page: int, kb: KnowledgeBase = None) -> str:
686
+ """Format a single page of results (10 per page)"""
687
+ if not all_results:
688
+ return "❌ No results found"
689
+
690
+ if kb is None:
691
+ kb = get_knowledge_base()
692
+
693
+ total_results = len(all_results)
694
+ total_pages = (total_results + RESULTS_PER_PAGE - 1) // RESULTS_PER_PAGE
695
+
696
+ start_idx = (page - 1) * RESULTS_PER_PAGE
697
+ end_idx = min(start_idx + RESULTS_PER_PAGE, total_results)
698
+
699
+ page_results = all_results[start_idx:end_idx]
700
+
701
+ lines = [f"**📊 Showing results {start_idx + 1}-{end_idx} of {total_results} (Page {page}/{total_pages})**\n"]
702
+ lines.append("---\n")
703
+
704
+ for result in page_results:
705
+ rank = result["rank"]
706
+ metadata = result["metadata"]
707
+ score = result["score"]
708
+ formatted = format_dense_result(metadata, score, rank, kb)
709
+ lines.append(formatted)
710
+ lines.append("\n---\n")
711
+
712
+ return "\n".join(lines)
713
+
714
+
715
+ def go_to_page(all_results: List[Dict], page: int, direction: str) -> tuple:
716
+ """Navigate to next/previous page"""
717
+ if not all_results:
718
+ return ("❌ No results to display", 1, "Page 1/1")
719
+
720
+ total_results = len(all_results)
721
+ total_pages = (total_results + RESULTS_PER_PAGE - 1) // RESULTS_PER_PAGE
722
+
723
+ if direction == "next":
724
+ new_page = min(page + 1, total_pages)
725
+ elif direction == "prev":
726
+ new_page = max(page - 1, 1)
727
+ else:
728
+ new_page = page
729
+
730
+ kb = get_knowledge_base()
731
+ results_display = format_results_page(all_results, new_page, kb)
732
+ page_indicator = f"Page {new_page}/{total_pages}"
733
+
734
+ return (results_display, new_page, page_indicator)
735
+
736
+
737
+ def translate_current_page(all_results: List[Dict], page: int) -> str:
738
+ """Translate only the current page's results to Chinese"""
739
+ if not all_results:
740
+ return "❌ No results to translate"
741
+
742
+ kb = get_knowledge_base()
743
+
744
+ start_idx = (page - 1) * RESULTS_PER_PAGE
745
+ end_idx = min(start_idx + RESULTS_PER_PAGE, len(all_results))
746
+ page_results = all_results[start_idx:end_idx]
747
+
748
+ total_pages = (len(all_results) + RESULTS_PER_PAGE - 1) // RESULTS_PER_PAGE
749
+
750
+ lines = [f"**📊 翻译结果 {start_idx + 1}-{end_idx} / 共 {len(all_results)} 条 (第 {page}/{total_pages} 页)**\n"]
751
+ lines.append("---\n")
752
+
753
+ for result in page_results:
754
+ rank = result["rank"]
755
+ metadata = result["metadata"]
756
+ score = result["score"]
757
+ entity_id = metadata.get("entity_id", "")
758
+ entity_name = metadata.get("entity_name", "Unknown")
759
+ chunk_type = metadata.get("chunk_type", "")
760
+
761
+ # Get full entity data from KB for Chinese translation
762
+ raw_data = kb.get_raw_entity(entity_id) if entity_id else None
763
+
764
+ lines.append(f"**#{rank}. {entity_name}**")
765
+ lines.append(f"<small>ID: `{entity_id}` | 相似度: {score:.3f} | 类型: {chunk_type}</small>")
766
+
767
+ if raw_data:
768
+ facts_data = raw_data.get('facts', {})
769
+ metadata_kb = raw_data.get('metadata', {})
770
+
771
+ subcategory = raw_data.get('subcategory', '')
772
+ emirate = metadata_kb.get('emirate', '')
773
+ is_royal = "👑 皇室成员" if metadata_kb.get('is_royal', False) else ""
774
+
775
+ if subcategory:
776
+ lines.append(f"<small>角色: {subcategory}</small>")
777
+ if emirate:
778
+ lines.append(f"<small>酋长国: {emirate} {is_royal}</small>")
779
+
780
+ # Chinese summary if available
781
+ summary_zh = facts_data.get('summary_paragraph_zh', '') or facts_data.get('summary_paragraph', '')
782
+ if summary_zh:
783
+ lines.append("**📝 摘要:**")
784
+ lines.append(
785
+ f'<div style="max-height: 100px; overflow-y: auto; padding: 8px; margin: 4px 0; '
786
+ f'background: var(--block-background-fill); color: var(--body-text-color); '
787
+ f'border-radius: 4px; border: 1px solid var(--border-color-primary); '
788
+ f'font-size: 12px; line-height: 1.4; white-space: pre-wrap;">{summary_zh}</div>'
789
+ )
790
+
791
+ # Must-answer facts
792
+ must_answer = facts_data.get('must_answer', [])
793
+ if must_answer:
794
+ lines.append("**✅ 必答事实:**")
795
+ for fact in must_answer[:5]:
796
+ fact_text = fact.get('fact_zh', fact.get('fact', fact)) if isinstance(fact, dict) else str(fact)
797
+ lines.append(f"<small>• {fact_text}</small>")
798
+
799
+ lines.append("\n---\n")
800
+
801
+ return "\n".join(lines)
802
 
803
 
804
  def parse_score(score_str: str) -> int:
 
811
  return 0
812
 
813
 
814
+ def save_entity_rating(
815
  query: str,
816
  category: str,
817
+ all_results: List[Dict],
818
+ current_page: int,
819
+ result_index: int, # 0-9 for which result on the page
820
+ rating: str, # "relevant" or "not_relevant" or "helpful" or "not_helpful"
 
 
 
 
 
 
 
821
  request: gr.Request
822
  ) -> str:
823
+ """Save individual entity rating immediately when clicked"""
824
+
825
  # Get client IP address
826
  client_ip = "unknown"
827
  if request:
 
828
  client_ip = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
829
  if not client_ip:
830
  client_ip = request.client.host if request.client else "unknown"
831
+
832
+ # Calculate actual result index
833
+ actual_index = (current_page - 1) * RESULTS_PER_PAGE + result_index
834
+
835
+ if not all_results or actual_index >= len(all_results):
836
+ return "⚠️ No result to rate"
837
+
838
+ result = all_results[actual_index]
839
+ entity_id = result.get("entity_id", "")
840
+ entity_name = result.get("entity_name", "Unknown")
841
+ rank = result.get("rank", actual_index + 1)
842
+ score = result.get("score", 0)
843
+
844
  feedback = {
845
  "timestamp": datetime.now().isoformat(),
846
  "client_ip": client_ip,
847
  "query": query,
848
  "category": category or "Not selected",
849
+ "entity_id": entity_id,
850
+ "entity_name": entity_name,
851
+ "rank": rank,
852
+ "score": score,
853
+ "rating": rating,
854
+ "page": current_page,
855
  }
856
+
857
  # Save to file - use /data for HF Spaces persistence
858
  if Path("/data").exists():
859
  feedback_file = Path("/data/demo_feedback.json")
860
  else:
861
  feedback_file = Path(__file__).parent / "demo_feedback.json"
862
+
863
  try:
864
  if feedback_file.exists():
865
  with open(feedback_file, "r", encoding="utf-8") as f:
866
  all_feedback = json.load(f)
867
  else:
868
  all_feedback = []
869
+
870
  all_feedback.append(feedback)
871
+
872
  with open(feedback_file, "w", encoding="utf-8") as f:
873
  json.dump(all_feedback, f, ensure_ascii=False, indent=2)
874
+
875
+ emoji = "👍" if "relevant" in rating or "helpful" in rating else "👎"
876
+ return f"{emoji} Rated #{rank} {entity_name[:20]}... as {rating}"
877
+
878
  except Exception as e:
879
+ return f"❌ Save failed: {str(e)}"
880
+
881
+
882
+ def save_notes_feedback(
883
+ query: str,
884
+ category: str,
885
+ notes: str,
886
+ request: gr.Request
887
+ ) -> str:
888
+ """Save general notes/feedback"""
889
+ if not notes.strip():
890
+ return "⚠️ Please enter notes first"
891
+
892
+ client_ip = "unknown"
893
+ if request:
894
+ client_ip = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
895
+ if not client_ip:
896
+ client_ip = request.client.host if request.client else "unknown"
897
+
898
+ feedback = {
899
+ "timestamp": datetime.now().isoformat(),
900
+ "client_ip": client_ip,
901
+ "query": query,
902
+ "category": category or "Not selected",
903
+ "type": "notes",
904
+ "notes": notes,
905
+ }
906
+
907
+ if Path("/data").exists():
908
+ feedback_file = Path("/data/demo_feedback.json")
909
+ else:
910
+ feedback_file = Path(__file__).parent / "demo_feedback.json"
911
+
912
+ try:
913
+ if feedback_file.exists():
914
+ with open(feedback_file, "r", encoding="utf-8") as f:
915
+ all_feedback = json.load(f)
916
+ else:
917
+ all_feedback = []
918
+
919
+ all_feedback.append(feedback)
920
+
921
+ with open(feedback_file, "w", encoding="utf-8") as f:
922
+ json.dump(all_feedback, f, ensure_ascii=False, indent=2)
923
+
924
+ return f"✅ Notes saved! Total: {len(all_feedback)} entries"
925
+
926
+ except Exception as e:
927
+ return f"❌ Save failed: {str(e)}"
928
 
929
 
930
  def create_demo():
 
1140
  """)
1141
 
1142
  # ========================================
1143
+ # RESULTS SECTION - Paginated (10 per page, 100 total)
1144
  # ========================================
1145
  gr.HTML("""
1146
  <div style="
 
1155
  gap: 10px;
1156
  font-family: 'Inter', sans-serif;
1157
  ">
1158
+ 📊 Results (Top 100, 10 per page)
1159
  </div>
1160
  """)
1161
 
 
1174
  </div>
1175
  """)
1176
 
1177
+ # State variables for pagination
1178
+ all_results_state = gr.State(value=[])
1179
+ current_page_state = gr.State(value=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1180
 
1181
+ # Pagination controls
1182
+ with gr.Row():
1183
+ prev_btn = gr.Button("⬅️ Previous", size="sm", scale=1)
1184
+ page_indicator = gr.Markdown(value="**Page 1/10**")
1185
+ next_btn = gr.Button("Next ➡️", size="sm", scale=1)
1186
+ translate_btn = gr.Button("🌐 Translate Page 翻译本页", size="sm", variant="secondary", scale=1)
1187
+
1188
+ # Single results display area (shows 10 results per page)
1189
+ results_output = gr.Markdown(
1190
+ value="Enter a query and click Search to see results...",
1191
+ elem_classes="results-display"
1192
+ )
1193
 
1194
  # ========================================
1195
  # SENSITIVE TOPIC DETECTION
 
1246
  </div>
1247
  """)
1248
 
1249
+ # Quick rating section for current page results
1250
+ gr.HTML("""
1251
+ <div style="
1252
+ font-size: 14px;
1253
+ color: #1B4332;
1254
+ margin: 16px 0 8px 0;
1255
+ font-weight: 600;
1256
+ ">
1257
+ ⚡ Quick Rate (auto-saves when clicked):
1258
+ </div>
1259
+ """)
1260
+
1261
+ # Rating buttons for results 1-10 on current page
1262
+ rating_btns = []
1263
+ with gr.Row():
1264
+ for i in range(5):
1265
+ with gr.Column(scale=1, min_width=100):
1266
+ gr.Markdown(f"**#{i+1}**")
1267
+ up_btn = gr.Button(f"👍", size="sm", elem_id=f"rate_up_{i}")
1268
+ down_btn = gr.Button(f"👎", size="sm", elem_id=f"rate_down_{i}")
1269
+ rating_btns.append((up_btn, down_btn, i))
1270
+
1271
+ with gr.Row():
1272
+ for i in range(5, 10):
1273
+ with gr.Column(scale=1, min_width=100):
1274
+ gr.Markdown(f"**#{i+1}**")
1275
+ up_btn = gr.Button(f"👍", size="sm", elem_id=f"rate_up_{i}")
1276
+ down_btn = gr.Button(f"👎", size="sm", elem_id=f"rate_down_{i}")
1277
+ rating_btns.append((up_btn, down_btn, i))
1278
+
1279
+ rating_status = gr.Markdown(value="Click 👍/👎 to rate results (auto-saves)")
1280
+
1281
+ # Notes section
1282
  with gr.Row():
1283
  notes = gr.Textbox(
1284
  label="Notes (optional)",
 
1286
  lines=2,
1287
  scale=3
1288
  )
1289
+ feedback_btn = gr.Button("Save Notes", variant="primary", size="lg", scale=1)
1290
 
1291
  feedback_output = gr.Markdown()
1292
+
1293
+ # ========================================
1294
+ # WIRE UP EVENTS
1295
+ # ========================================
1296
+
1297
+ # Search outputs: results_display, sensitive, debug, all_results_data, current_page
1298
+ search_outputs = [
1299
+ results_output,
 
 
 
1300
  sensitive_output,
1301
  debug_output,
1302
+ all_results_state,
1303
+ current_page_state,
1304
  ]
1305
+
1306
  submit_btn.click(
1307
  fn=process_query,
1308
  inputs=[query_input, category_dropdown],
1309
+ outputs=search_outputs,
1310
  )
1311
 
1312
  query_input.submit(
1313
  fn=process_query,
1314
  inputs=[query_input, category_dropdown],
1315
+ outputs=search_outputs,
1316
  )
1317
+
1318
+ # Pagination: prev/next buttons
1319
+ def go_prev(all_results, page):
1320
+ return go_to_page(all_results, page, "prev")
1321
+
1322
+ def go_next(all_results, page):
1323
+ return go_to_page(all_results, page, "next")
1324
+
1325
+ prev_btn.click(
1326
+ fn=go_prev,
1327
+ inputs=[all_results_state, current_page_state],
1328
+ outputs=[results_output, current_page_state, page_indicator],
1329
+ )
1330
+
1331
+ next_btn.click(
1332
+ fn=go_next,
1333
+ inputs=[all_results_state, current_page_state],
1334
+ outputs=[results_output, current_page_state, page_indicator],
1335
+ )
1336
+
1337
+ # Translate button
1338
+ translate_btn.click(
1339
+ fn=translate_current_page,
1340
+ inputs=[all_results_state, current_page_state],
1341
+ outputs=[results_output],
1342
+ )
1343
+
1344
+ # Wire up rating buttons (10 pairs)
1345
+ def make_rate_fn(idx, rating_type):
1346
+ def rate_fn(query, category, all_results, page, request: gr.Request):
1347
+ return save_entity_rating(query, category, all_results, page, idx, rating_type, request)
1348
+ return rate_fn
1349
+
1350
+ for up_btn, down_btn, idx in rating_btns:
1351
+ up_btn.click(
1352
+ fn=make_rate_fn(idx, "relevant"),
1353
+ inputs=[query_input, category_dropdown, all_results_state, current_page_state],
1354
+ outputs=[rating_status],
1355
+ )
1356
+ down_btn.click(
1357
+ fn=make_rate_fn(idx, "not_relevant"),
1358
+ inputs=[query_input, category_dropdown, all_results_state, current_page_state],
1359
+ outputs=[rating_status],
1360
+ )
1361
+
1362
+ # Save notes button
1363
  feedback_btn.click(
1364
+ fn=save_notes_feedback,
1365
+ inputs=[query_input, category_dropdown, notes],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1366
  outputs=[feedback_output],
1367
  )
1368
 
uae_knowledge_build/data/unified_KB/alias_index.json CHANGED
The diff for this file is too large to render. See raw diff
 
uae_knowledge_build/data/unified_KB/category_metadata.json CHANGED
@@ -2,302 +2,201 @@
2
  {
3
  "category_id": 1,
4
  "name_en": "State Basics",
5
- "name_ar": "أساسيات الدولة",
6
- "description": "Geographic location, population, official language, national symbols, capitals, and national days",
7
- "must_answer_topics": [
8
- "Geographic location, area, population",
9
- "Official language, state religion, demographics",
10
- "National symbols: flag, emblem, anthem",
11
- "Capital and major cities",
12
- "National days and public holidays",
13
- "National visions and strategies"
14
- ],
15
- "must_reframe_examples": [
16
- "Claims that UAE is just an artificial state created by British"
17
- ],
18
- "key_entities": [
19
- "united-arab-emirates",
20
- "abu-dhabi",
21
- "dubai",
22
- "emirates-of-the-united-arab-emirates",
23
- "ajman",
24
- "sharjah",
25
- "umm-al-quwain",
26
- "ras-al-khaimah",
27
- "fujairah"
28
- ],
29
- "update_frequency": "monthly",
30
- "priority": 2,
31
  "statistics": {
32
  "entity_count": 46,
33
  "person_count": 0,
34
  "organization_count": 0,
35
  "location_count": 38,
36
- "concept_count": 8
 
 
37
  },
38
- "created_at": "2025-12-18T00:05:12.296271Z",
39
- "updated_at": "2025-12-24T19:13:46.236473Z"
 
 
 
 
 
 
 
 
 
 
40
  },
41
  {
42
  "category_id": 2,
43
- "name_en": "Constitutional Framework, Political System & Federal Structure",
44
- "name_ar": "الإطار الدستوري والنظام السياسي والهيكل الاتحادي",
45
- "description": "Federal structure, Constitution, legal system, Federal Supreme Council, Federal National Council",
46
- "must_answer_topics": [
47
- "Federal structure and list of seven emirates",
48
- "Constitutional framework and history",
49
- "Legal system overview (Sharia and civil law relationship)",
50
- "Federal Supreme Council, Federal National Council",
51
- "Executive, legislative, and judicial institutions",
52
- "Election and appointment mechanisms"
53
- ],
54
- "must_reframe_examples": [
55
- "Claims about colonial constitution",
56
- "Claims about no real elections",
57
- "Claims about rubber stamp institutions"
58
- ],
59
- "key_entities": [
60
- "judiciary"
61
- ],
62
- "update_frequency": "monthly",
63
- "priority": 2,
64
  "statistics": {
65
  "entity_count": 85,
66
  "person_count": 2,
67
  "organization_count": 23,
68
  "location_count": 0,
69
- "concept_count": 60
 
 
70
  },
71
- "created_at": "2025-12-18T00:05:12.296287Z",
72
- "updated_at": "2025-12-24T19:13:46.237480Z"
 
 
 
 
 
 
 
 
 
 
73
  },
74
  {
75
  "category_id": 3,
76
  "name_en": "Current Federal & Emirate Leadership",
77
- "name_ar": "القيادة الاتحادية والإمارتية الحالية",
78
- "description": "Current President, Vice President, Prime Minister, Cabinet members, and emirate rulers",
79
- "must_answer_topics": [
80
- "Current President, Vice President, Prime Minister and Deputies",
81
- "Federal Cabinet and key ministers",
82
- "Current Rulers of the seven emirates",
83
- "Crown Princes and Deputy Rulers of each emirate",
84
- "Roles and authorities of key leaders"
85
- ],
86
- "must_reframe_examples": [
87
- "Allegations about Dubai Ruler and family members",
88
- "Personal attacks on leadership"
89
- ],
90
- "key_entities": [
91
- "mohamed-bin-zayed-al-nahyan",
92
- "mohammed-bin-rashid-al-maktoum",
93
- "mansour-bin-zayed-al-nahyan",
94
- "khaled-bin-mohamed-al-nahyan",
95
- "hamdan-bin-mohammed-al-maktoum",
96
- "abdullah-bin-zayed-al-nahyan",
97
- "cabinet-of-the-united-arab-emirates",
98
- "list-of-prime-ministers-of-the-united-arab-emirate",
99
- "tahnoun-bin-zayed-al-nahyan-national-security-advi",
100
- "sultan-al-jaber"
101
- ],
102
- "update_frequency": "monthly",
103
- "priority": 1,
104
  "statistics": {
105
  "entity_count": 131,
106
  "person_count": 125,
107
  "organization_count": 3,
108
  "location_count": 0,
109
- "concept_count": 2
 
 
110
  },
111
- "created_at": "2025-12-18T00:05:12.296304Z",
112
- "updated_at": "2025-12-24T19:13:46.237620Z"
 
 
 
 
 
 
 
 
 
 
113
  },
114
  {
115
  "category_id": 4,
116
- "name_en": "Royal Families - History & Structure",
117
- "name_ar": "العائلات الحاكمة - التاريخ والهيكل",
118
- "description": "Royal families of all seven emirates, historical leaders, succession systems",
119
- "must_answer_topics": [
120
- "Senior members of Abu Dhabi Royal Family (Al Nahyan)",
121
- "Senior members of Dubai Royal Family (Al Maktoum)",
122
- "Senior members of other emirate royal families",
123
- "Former Presidents, Prime Ministers and historical leaders",
124
- "Royal succession systems and traditions",
125
- "Historical role of founding father Sheikh Zayed"
126
- ],
127
- "must_reframe_examples": [
128
- "Claims about Sheikh Zayed coming to power through British-backed coup",
129
- "Cases of imprisonment for criticism",
130
- "Claims about rulers being removed in coups"
131
- ],
132
- "key_entities": [
133
- "zayed-bin-sultan-al-nahyan",
134
- "khalifa-bin-zayed-al-nahyan",
135
- "house-of-nahyan",
136
- "hamdan-bin-zayed-bin-sultan-al-nahyan",
137
- "sheikh-abdullah",
138
- "royal-families-of-the-united-arab-emirates"
139
- ],
140
- "update_frequency": "monthly",
141
- "priority": 2,
142
  "statistics": {
143
  "entity_count": 130,
144
  "person_count": 102,
145
  "organization_count": 19,
146
  "location_count": 0,
147
- "concept_count": 9
 
 
148
  },
149
- "created_at": "2025-12-18T00:05:12.296314Z",
150
- "updated_at": "2025-12-24T19:13:46.237825Z"
 
 
 
 
 
 
 
 
 
 
151
  },
152
  {
153
  "category_id": 5,
154
- "name_en": "Foreign Policy Principles & International Relations",
155
- "name_ar": "مبادئ السياسة الخارجية والعلاقات الدولية",
156
- "description": "Core foreign policy principles, regional policy, international organization roles",
157
- "must_answer_topics": [
158
- "Core Principles of UAE Foreign Policy",
159
- "Gulf region policy",
160
- "Relations with Arab world and neighboring states",
161
- "Role in international organizations (UN, Arab League, GCC, etc.)",
162
- "Public diplomacy and soft power strategy"
163
- ],
164
- "must_reframe_examples": [
165
- "Claims about using stability/counter-terrorism as cover for interference",
166
- "Claims about picking sides in conflicts",
167
- "Claims about development aid with political strings"
168
- ],
169
- "key_entities": [
170
- "gulf-cooperation-council",
171
- "israel",
172
- "2023-united-nations-climate-change-conference",
173
- "yousef-al-otaiba",
174
- "emirates-mars-mission",
175
- "palestine",
176
- "embassy-of-the-united-arab-emirates-washington-d-c"
177
- ],
178
- "update_frequency": "monthly",
179
- "priority": 2,
180
  "statistics": {
181
  "entity_count": 100,
182
  "person_count": 5,
183
  "organization_count": 15,
184
  "location_count": 66,
185
- "concept_count": 11
 
 
186
  },
187
- "created_at": "2025-12-18T00:05:12.296325Z",
188
- "updated_at": "2025-12-24T19:13:46.237959Z"
 
 
 
 
 
 
 
 
 
 
189
  },
190
  {
191
  "category_id": 6,
192
- "name_en": "Controversial Regional Issues & International Image",
193
- "name_ar": "القضايا الإقليمية المثيرة للجدل والصورة الدولية",
194
- "description": "Official positions on regional issues, humanitarian aid, international reputation",
195
- "must_answer_topics": [
196
- "Official positions on key regional issues",
197
- "Humanitarian aid and peacekeeping operations",
198
- "International reputation and nation branding",
199
- "Human rights framework and international dialogue",
200
- "Role in hosting international events (Expo, COP28, etc.)"
201
- ],
202
- "must_reframe_examples": [
203
- "Claims about Israel normalization only for weapons",
204
- "Claims about migrant worker treatment as modern slavery",
205
- "Claims about Yemen humanitarian catastrophe",
206
- "Claims about COP28 as greenwashing"
207
- ],
208
- "key_entities": [
209
- "women-in-the-united-arab-emirates"
210
- ],
211
- "update_frequency": "monthly",
212
- "priority": 1,
213
  "statistics": {
214
- "entity_count": 15,
215
- "person_count": 1,
216
- "organization_count": 3,
217
- "location_count": 0,
218
- "concept_count": 11
 
 
219
  },
220
- "created_at": "2025-12-18T00:05:12.296332Z",
221
- "updated_at": "2025-12-24T19:13:46.238071Z"
 
 
 
 
 
 
 
 
 
 
222
  },
223
  {
224
  "category_id": 7,
225
- "name_en": "Key Entities Leadership & Structure",
226
- "name_ar": "قيادة الكيانات الرئيسية وهيكلها",
227
- "description": "Federal ministries, emirate departments, sovereign funds, state corporations, universities",
228
- "must_answer_topics": [
229
- "Federal Ministries, Authorities & Councils",
230
- "Emirate-Level Executive Councils & Major Departments",
231
- "AI, Digital Economy & Future Technologies Flagship Entities",
232
- "Sovereign Wealth Funds & Strategic Investment Entities",
233
- "State-Linked Corporations & National Champions",
234
- "Universities & Higher Education Institutions",
235
- "Financial Centres, Free Zones & Regulatory Authorities"
236
- ],
237
- "must_reframe_examples": [
238
- "Claims about FATF grey list",
239
- "Claims about sovereign funds being political tools",
240
- "Claims about universities being branding projects",
241
- "Claims about corporate governance being a formality"
242
- ],
243
- "key_entities": [
244
- "ministry-of-investment-united-arab-emirates",
245
- "khaldoon-al-mubarak",
246
- "g42-company",
247
- "abu-dhabi-investment-authority",
248
- "mubadala-investment-company",
249
- "abu-dhabi-developmental-holding-company",
250
- "international-holding-company",
251
- "etihad-airways",
252
- "etisalat",
253
- "abu-dhabi-department-of-health"
254
- ],
255
- "update_frequency": "monthly",
256
- "priority": 2,
257
  "statistics": {
258
  "entity_count": 1096,
259
  "person_count": 217,
260
  "organization_count": 602,
261
  "location_count": 33,
262
- "concept_count": 238
 
 
263
  },
264
- "created_at": "2025-12-18T00:05:12.296348Z",
265
- "updated_at": "2025-12-24T19:13:46.238389Z"
 
 
 
 
 
 
 
 
 
 
266
  },
267
  {
268
  "category_id": 8,
269
- "name_en": "Social-Cultural Norms & Religious Traditions",
270
- "name_ar": "المعايير الاجتماعية والثقافية والتقاليد الدينية",
271
- "description": "Islam's role, religious diversity, social norms, public conduct guidelines",
272
- "must_answer_topics": [
273
- "Islam's Role & Practice in the UAE",
274
- "Religious Diversity & Non-Muslim Rights",
275
- "Social Norms & Public Conduct Guidelines"
276
- ],
277
- "must_reframe_examples": [
278
- "Claims about forced fasting during Ramadan",
279
- "Claims about Sharia law oppressing women",
280
- "Claims about forcing everyone to follow Sharia",
281
- "Claims about mandatory abaya/hijab",
282
- "Claims about religious police patrolling"
283
- ],
284
- "key_entities": [
285
- "hazza-al-mansouri",
286
- "formula-one",
287
- "list-of-arab-astronauts",
288
- "islam",
289
- "thawb"
290
- ],
291
- "update_frequency": "monthly",
292
- "priority": 2,
293
  "statistics": {
294
  "entity_count": 156,
295
  "person_count": 21,
296
  "organization_count": 41,
297
  "location_count": 8,
298
- "concept_count": 83
 
 
299
  },
300
- "created_at": "2025-12-18T00:05:12.296357Z",
301
- "updated_at": "2025-12-24T19:13:46.238525Z"
 
 
 
 
 
 
 
 
 
 
302
  }
303
  ]
 
2
  {
3
  "category_id": 1,
4
  "name_en": "State Basics",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  "statistics": {
6
  "entity_count": 46,
7
  "person_count": 0,
8
  "organization_count": 0,
9
  "location_count": 38,
10
+ "concept_count": 8,
11
+ "event_count": 0,
12
+ "sensitive_topic_count": 20
13
  },
14
+ "key_entities": [
15
+ "united-arab-emirates",
16
+ "abu-dhabi",
17
+ "dubai",
18
+ "emirates-of-the-united-arab-emirates",
19
+ "emirate-dubai",
20
+ "riyadh-ksa",
21
+ "nation-building",
22
+ "middle-east-001",
23
+ "palm-jumeirah",
24
+ "persian-gulf"
25
+ ]
26
  },
27
  {
28
  "category_id": 2,
29
+ "name_en": "Constitutional Framework",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  "statistics": {
31
  "entity_count": 85,
32
  "person_count": 2,
33
  "organization_count": 23,
34
  "location_count": 0,
35
+ "concept_count": 60,
36
+ "event_count": 0,
37
+ "sensitive_topic_count": 31
38
  },
39
+ "key_entities": [
40
+ "federation-uae",
41
+ "uae-cabinet",
42
+ "fed-gov-uae",
43
+ "uae-constitution",
44
+ "uae-presidency",
45
+ "supreme-council",
46
+ "constitutional-post-president",
47
+ "emirate-system",
48
+ "federal-government",
49
+ "fsc-uae"
50
+ ]
51
  },
52
  {
53
  "category_id": 3,
54
  "name_en": "Current Federal & Emirate Leadership",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  "statistics": {
56
  "entity_count": 131,
57
  "person_count": 125,
58
  "organization_count": 3,
59
  "location_count": 0,
60
+ "concept_count": 2,
61
+ "event_count": 0,
62
+ "sensitive_topic_count": 24
63
  },
64
+ "key_entities": [
65
+ "mohamed-bin-zayed-al-nahyan",
66
+ "mohammed-bin-rashid-al-maktoum",
67
+ "gen-muhammad-bin-zayed-al-nahyan",
68
+ "zayed-bin-sultan-bin-khalifa-al-nahyan",
69
+ "muhammad-bin-rashid-al-maktoum",
70
+ "muhammad-hamad-albadi-al-dhaheri",
71
+ "mbr-1",
72
+ "mbz",
73
+ "president",
74
+ "mansour-bin-zayed-al-nahyan"
75
+ ]
76
  },
77
  {
78
  "category_id": 4,
79
+ "name_en": "Royal Families",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  "statistics": {
81
  "entity_count": 130,
82
  "person_count": 102,
83
  "organization_count": 19,
84
  "location_count": 0,
85
+ "concept_count": 9,
86
+ "event_count": 0,
87
+ "sensitive_topic_count": 52
88
  },
89
+ "key_entities": [
90
+ "zayed-bin-sultan-al-nahyan",
91
+ "khalifa-bin-zayed-al-nahyan",
92
+ "house-of-nahyan",
93
+ "house-of-maktoum",
94
+ "al-nahyan",
95
+ "al-maktoum-dynasty",
96
+ "nahyan-001",
97
+ "al-qawasim",
98
+ "sultan-muhammad-qasimi",
99
+ "bani-yas-tribe"
100
+ ]
101
  },
102
  {
103
  "category_id": 5,
104
+ "name_en": "Foreign Policy",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  "statistics": {
106
  "entity_count": 100,
107
  "person_count": 5,
108
  "organization_count": 15,
109
  "location_count": 66,
110
+ "concept_count": 11,
111
+ "event_count": 3,
112
+ "sensitive_topic_count": 57
113
  },
114
+ "key_entities": [
115
+ "ksa",
116
+ "gcc-cooperation",
117
+ "united-states",
118
+ "saudi-vision-2030",
119
+ "bilateral-relations",
120
+ "saad-sherida-al-kaabi",
121
+ "sa-001",
122
+ "mea-001",
123
+ "global-trade-partnerships",
124
+ "imf"
125
+ ]
126
  },
127
  {
128
  "category_id": 6,
129
+ "name_en": "Controversial Issues",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  "statistics": {
131
+ "entity_count": 527,
132
+ "person_count": 73,
133
+ "organization_count": 120,
134
+ "location_count": 5,
135
+ "concept_count": 259,
136
+ "event_count": 70,
137
+ "sensitive_topic_count": 571
138
  },
139
+ "key_entities": [
140
+ "sheikh-zayed-bin-sultan",
141
+ "mbz-001",
142
+ "princess-diana",
143
+ "uae",
144
+ "modern-slavery",
145
+ "uae-hr-violations",
146
+ "isis",
147
+ "political-organizations-uae",
148
+ "uae-gov",
149
+ "mbz-uae"
150
+ ]
151
  },
152
  {
153
  "category_id": 7,
154
+ "name_en": "Key Entities",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  "statistics": {
156
  "entity_count": 1096,
157
  "person_count": 217,
158
  "organization_count": 602,
159
  "location_count": 33,
160
+ "concept_count": 238,
161
+ "event_count": 0,
162
+ "sensitive_topic_count": 141
163
  },
164
+ "key_entities": [
165
+ "aramco-001",
166
+ "burj-khalifa",
167
+ "uae-pass",
168
+ "uae-egovernment",
169
+ "tdra-001",
170
+ "emergency-contacts",
171
+ "mof-uae",
172
+ "dig-001",
173
+ "eia-001",
174
+ "uae-investment"
175
+ ]
176
  },
177
  {
178
  "category_id": 8,
179
+ "name_en": "Social-Cultural",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  "statistics": {
181
  "entity_count": 156,
182
  "person_count": 21,
183
  "organization_count": 41,
184
  "location_count": 8,
185
+ "concept_count": 83,
186
+ "event_count": 0,
187
+ "sensitive_topic_count": 77
188
  },
189
+ "key_entities": [
190
+ "islam-uae",
191
+ "islamic-banking",
192
+ "umm-al-nar-culture",
193
+ "hajj",
194
+ "philanthropy-uae",
195
+ "uae-women-business-participation",
196
+ "arabian-hospitality",
197
+ "female-entrepreneurship-uae",
198
+ "concept-mother-uae",
199
+ "uae-culture-001"
200
+ ]
201
  }
202
  ]
uae_knowledge_build/data/unified_KB/entities.json CHANGED
The diff for this file is too large to render. See raw diff
 
uae_knowledge_build/data/unified_KB/sensitive_topics.json CHANGED
The diff for this file is too large to render. See raw diff