jinruiy Claude commited on
Commit ·
d1394bb
1
Parent(s): 4ebe674
Add per-entity feedback, rank details dropdown, and expanded KB
Browse filesFeatures:
- 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 +60 -0
- frontend/css/styles.css +60 -0
- frontend/js/app.js +408 -33
- ir/cache/dense_index/chunk_metadata_bge-m3.json +0 -0
- ir/cache/dense_index/faiss_index_bge-m3.bin +2 -2
- ir/demo.py +340 -165
- uae_knowledge_build/data/unified_KB/alias_index.json +0 -0
- uae_knowledge_build/data/unified_KB/category_metadata.json +130 -231
- uae_knowledge_build/data/unified_KB/entities.json +0 -0
- uae_knowledge_build/data/unified_KB/sensitive_topics.json +0 -0
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
|
|
|
|
|
|
|
| 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 |
-
<
|
| 618 |
-
|
| 619 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
</div>
|
| 621 |
|
| 622 |
<!-- Card Body -->
|
|
@@ -629,20 +764,16 @@ function renderResults() {
|
|
| 629 |
</p>
|
| 630 |
</div>
|
| 631 |
|
| 632 |
-
<!--
|
| 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('
|
| 635 |
-
|
| 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-
|
| 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 (
|
| 742 |
// ============================================
|
| 743 |
-
window.setRating =
|
| 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
|
| 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 |
-
|
| 761 |
-
entity_id: result
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 765 |
})
|
| 766 |
});
|
| 767 |
|
| 768 |
const data = await response.json();
|
|
|
|
| 769 |
if (data.success) {
|
| 770 |
-
//
|
| 771 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
}
|
| 773 |
} catch (error) {
|
| 774 |
-
console.error('Failed to save
|
| 775 |
-
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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 |
-
(
|
| 621 |
"""
|
| 622 |
-
empty = "**#%d** - (no result)"
|
| 623 |
-
|
| 624 |
if not query.strip():
|
| 625 |
-
|
| 626 |
-
return (warn, empty % 2, empty % 3, empty % 4, empty % 5, "", "", "")
|
| 627 |
|
| 628 |
# Require category selection
|
| 629 |
if not category:
|
| 630 |
-
|
| 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 |
-
|
|
|
|
| 643 |
latency = (time.perf_counter() - start) * 1000
|
| 644 |
|
| 645 |
-
#
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
-
|
| 650 |
-
|
| 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 |
-
|
| 673 |
-
entity_outputs[1],
|
| 674 |
-
entity_outputs[2],
|
| 675 |
-
entity_outputs[3],
|
| 676 |
-
entity_outputs[4],
|
| 677 |
sensitive_text,
|
| 678 |
debug_text,
|
| 679 |
-
|
|
|
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 699 |
query: str,
|
| 700 |
category: str,
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
#
|
| 704 |
-
|
| 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
|
| 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 |
-
#
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
|
|
|
|
|
|
|
|
|
| 733 |
feedback = {
|
| 734 |
"timestamp": datetime.now().isoformat(),
|
| 735 |
"client_ip": client_ip,
|
| 736 |
"query": query,
|
| 737 |
"category": category or "Not selected",
|
| 738 |
-
"
|
| 739 |
-
"
|
| 740 |
-
"
|
| 741 |
-
"
|
| 742 |
-
"
|
|
|
|
| 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 |
-
|
| 764 |
-
|
|
|
|
| 765 |
except Exception as e:
|
| 766 |
-
return f"❌ Save failed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 1017 |
-
|
| 1018 |
-
|
| 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 |
-
#
|
| 1069 |
-
with gr.
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 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("
|
| 1144 |
|
| 1145 |
feedback_output = gr.Markdown()
|
| 1146 |
-
|
| 1147 |
-
#
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
entity_3_output,
|
| 1155 |
-
entity_4_output,
|
| 1156 |
-
entity_5_output,
|
| 1157 |
sensitive_output,
|
| 1158 |
debug_output,
|
| 1159 |
-
|
|
|
|
| 1160 |
]
|
| 1161 |
-
|
| 1162 |
submit_btn.click(
|
| 1163 |
fn=process_query,
|
| 1164 |
inputs=[query_input, category_dropdown],
|
| 1165 |
-
outputs=
|
| 1166 |
)
|
| 1167 |
|
| 1168 |
query_input.submit(
|
| 1169 |
fn=process_query,
|
| 1170 |
inputs=[query_input, category_dropdown],
|
| 1171 |
-
outputs=
|
| 1172 |
)
|
| 1173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1174 |
feedback_btn.click(
|
| 1175 |
-
fn=
|
| 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 |
-
"
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
},
|
| 41 |
{
|
| 42 |
"category_id": 2,
|
| 43 |
-
"name_en": "Constitutional Framework
|
| 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 |
-
"
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
},
|
| 114 |
{
|
| 115 |
"category_id": 4,
|
| 116 |
-
"name_en": "Royal Families
|
| 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 |
-
"
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
},
|
| 152 |
{
|
| 153 |
"category_id": 5,
|
| 154 |
-
"name_en": "Foreign Policy
|
| 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 |
-
"
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
},
|
| 190 |
{
|
| 191 |
"category_id": 6,
|
| 192 |
-
"name_en": "Controversial
|
| 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":
|
| 215 |
-
"person_count":
|
| 216 |
-
"organization_count":
|
| 217 |
-
"location_count":
|
| 218 |
-
"concept_count":
|
|
|
|
|
|
|
| 219 |
},
|
| 220 |
-
"
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
},
|
| 223 |
{
|
| 224 |
"category_id": 7,
|
| 225 |
-
"name_en": "Key Entities
|
| 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 |
-
"
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
},
|
| 267 |
{
|
| 268 |
"category_id": 8,
|
| 269 |
-
"name_en": "Social-Cultural
|
| 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 |
-
"
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|