| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | const CONFIG = { |
| | API_BASE: '/api', |
| | CATEGORIES: [ |
| | { id: '1', name: { en: 'State Basics', ar: 'أساسيات الدولة', cn: '国家基础' }, example: { en: 'What is the capital city of the UAE?', ar: 'ما هي عاصمة الإمارات؟', cn: '阿联酋的首都是什么?' } }, |
| | { id: '2', name: { en: 'Constitutional Framework', ar: 'الإطار الدستوري', cn: '宪法框架' }, example: { en: 'What is the role of the Federal Supreme Council?', ar: 'ما هو دور المجلس الاتحادي الأعلى؟', cn: '联邦最高委员会的作用是什么?' } }, |
| | { id: '3', name: { en: 'Current Leadership', ar: 'القيادة الحالية', cn: '现任领导层' }, example: { en: 'Who is the current President of the UAE?', ar: 'من هو الرئيس الحالي للإمارات؟', cn: '阿联酋现任总统是谁?' } }, |
| | { id: '4', name: { en: 'Royal Families', ar: 'العائلات الحاكمة', cn: '皇室家族' }, example: { en: 'Who is the founding father of the UAE?', ar: 'من هو الأب المؤسس للإمارات؟', cn: '阿联酋的建国之父是谁?' } }, |
| | { id: '5', name: { en: 'Foreign Policy', ar: 'السياسة الخارجية', cn: '外交政策' }, example: { en: 'What role does the UAE play in the Gulf region?', ar: 'ما هو دور الإمارات في منطقة الخليج؟', cn: '阿联酋在海湾地区扮演什么角色?' } }, |
| | { id: '6', name: { en: 'Controversial Issues', ar: 'القضايا الخلافية', cn: '争议性话题' }, example: { en: 'Human rights in UAE', ar: 'حقوق الإنسان في الإمارات', cn: '阿联酋的人权状况' } }, |
| | { id: '7', name: { en: 'Key Entities', ar: 'الكيانات الرئيسية', cn: '重要机构' }, example: { en: 'Who is the CEO of ADNOC?', ar: 'من هو الرئيس التنفيذي لأدنوك؟', cn: '阿德诺克的首席执行官是谁?' } }, |
| | { id: '8', name: { en: 'Social-Cultural Norms', ar: 'الأعراف الاجتماعية', cn: '社会文化规范' }, example: { en: 'What role does Islam play in the UAE?', ar: 'ما هو دور الإسلام في الإمارات؟', cn: '伊斯兰教在阿联酋扮演什么角色?' } } |
| | ] |
| | }; |
| |
|
| | |
| | |
| | |
| | const TRANSLATIONS = { |
| | en: { |
| | title: 'UAE Knowledge System', |
| | searchPlaceholder: 'Ask about UAE governance, leadership, or policies...', |
| | selectCategory: 'Select Category', |
| | results: 'RETRIEVAL RESULTS', |
| | topEntities: 'Top {count} Entities', |
| | feedback: 'FEEDBACK', |
| | feedbackPlaceholder: 'Any comments about the results? (optional)', |
| | submit: 'Submit', |
| | help: 'Help', |
| | noResults: 'No results found for your query', |
| | enterQuery: 'Enter a query above to search the UAE Knowledge Base', |
| | selectCategoryHint: 'Select a category and click Search to begin', |
| | mustKnowFacts: '✓ Must-Know Facts', |
| | sensitiveTopics: '⚠️ Sensitive Topics', |
| | sensitivityRating: 'Sensitivity', |
| | sensitivityHigh: '🔴 HIGH', |
| | sensitivityMedium: '🟡 MEDIUM', |
| | sensitivityLow: '🟢 LOW', |
| | noSensitiveTopics: 'No sensitive topics identified', |
| | problematicFraming: 'Problematic Framing', |
| | responseGuide: 'Response Guide', |
| | strategy: 'Strategy', |
| | tone: 'Tone', |
| | keyFacts: 'Key Facts', |
| | suggestedResponse: 'Suggested Response', |
| | relevance: 'Relevance?', |
| | helpful: 'Helpful?', |
| | sensitivityHandling: 'Sensitivity Handling?', |
| | feedbackForEntity: 'Your feedback on this entity...', |
| | submitEntityFeedback: 'Submit Feedback', |
| | feedbackSavedForEntity: 'Feedback saved for this entity ✓', |
| | detailedAnalysis: 'Detailed Analysis', |
| | fullEntityJson: 'Full Entity JSON', |
| | rankScore: 'Rank Score', |
| | fullScore: 'Full Score', |
| | matchedChunk: 'Matched Chunk', |
| | subcategory: 'Subcategory', |
| | emirate: 'Emirate', |
| | model: 'Model', |
| | source: 'Source', |
| | viewEntity: 'View Entity', |
| | entityData: 'Entity Data', |
| | pleaseEnterQuery: 'Please enter a search query', |
| | pleaseSelectCategory: 'Please select a category first', |
| | feedbackSaved: 'Feedback saved! Thank you.', |
| | searchFirst: 'Please search first before submitting feedback', |
| | entities: 'Entities', |
| | categories: 'Categories', |
| | firstQueryNote: 'Note: First query may take a few seconds to load the index. Subsequent queries will be fast.', |
| | |
| | showOriginal: 'Show Original (EN)', |
| | showTranslated: 'Show Translated', |
| | translating: 'Translating...', |
| | translationNotAvailable: 'Translation not available', |
| | translateTo: 'Translate', |
| | |
| | helpModalTitle: '📚 About UAE Knowledge System', |
| | helpWhatIsTitle: 'What is this system?', |
| | helpWhatIsText: 'The UAE Knowledge System is an <strong>Information Retrieval (IR) system</strong> designed to retrieve relevant knowledge about the United Arab Emirates from a curated knowledge base. This is <strong>NOT an LLM chatbot</strong> - it retrieves pre-written factual content.', |
| | helpNoticeTitle: '⚠️ Important Notice', |
| | helpNoticeText: 'The summaries and facts shown are <strong>retrieved from a knowledge base</strong>, not generated by an AI. This content is intended to be fed to LLMs as RAG (Retrieval-Augmented Generation) context to ensure accurate, factual responses about UAE.', |
| | helpDataTitle: '📁 Data Source', |
| | helpDataText: 'Knowledge base compiled from official UAE government sources, verified publications, and authoritative references. Last updated: <strong>February 2026</strong>', |
| | helpIRTitle: '🔧 Current IR Level', |
| | helpIRText: '<strong>Level 4: Dense Retrieval (bge-m3)</strong><br>Performance: 69% Precision@1, 88% Recall@5, ~30ms latency on GPU', |
| | helpCategoriesTitle: '📋 8 Knowledge Categories', |
| | helpVersion: 'Version 2.4.0 | Published February 2026 | Powered by <a href="https://www.librai.tech/" target="_blank" class="text-emerald-600 hover:underline">LibrAI</a>', |
| | |
| | helpCat1: 'State Basics', |
| | helpCat1Ex: 'Example: "What is the capital city of the UAE?"', |
| | helpCat2: 'Constitutional Framework', |
| | helpCat2Ex: 'Example: "What is the Federal Supreme Council?"', |
| | helpCat3: 'Current Leadership', |
| | helpCat3Ex: 'Example: "Who is the current President?"', |
| | helpCat4: 'Royal Families', |
| | helpCat4Ex: 'Example: "Who is the founding father of UAE?"', |
| | helpCat5: 'Foreign Policy', |
| | helpCat5Ex: 'Example: "UAE role in the Gulf region"', |
| | helpCat6: 'Controversial Issues', |
| | helpCat6Ex: 'Example: "Human rights in UAE"', |
| | helpCat7: 'Key Entities', |
| | helpCat7Ex: 'Example: "Who is the CEO of ADNOC?"', |
| | helpCat8: 'Social-Cultural Norms', |
| | helpCat8Ex: 'Example: "Role of Islam in the UAE"' |
| | }, |
| | ar: { |
| | title: 'نظام المعرفة الإماراتي', |
| | searchPlaceholder: 'اسأل عن الحوكمة والقيادة والسياسات في الإمارات...', |
| | selectCategory: 'اختر الفئة', |
| | results: 'نتائج الاسترجاع', |
| | topEntities: 'أفضل {count} كيانات', |
| | feedback: 'ملاحظات', |
| | feedbackPlaceholder: 'أي تعليقات على النتائج؟ (اختياري)', |
| | submit: 'إرسال', |
| | help: 'مساعدة', |
| | noResults: 'لم يتم العثور على نتائج', |
| | enterQuery: 'أدخل استعلامك للبحث في قاعدة المعرفة', |
| | selectCategoryHint: 'اختر فئة وانقر للبحث', |
| | mustKnowFacts: '✓ حقائق أساسية', |
| | sensitiveTopics: '⚠️ مواضيع حساسة', |
| | sensitivityRating: 'الحساسية', |
| | sensitivityHigh: '🔴 عالية', |
| | sensitivityMedium: '🟡 متوسطة', |
| | sensitivityLow: '🟢 منخفضة', |
| | noSensitiveTopics: 'لم يتم تحديد مواضيع حساسة', |
| | problematicFraming: 'الصياغة الإشكالية', |
| | responseGuide: 'دليل الاستجابة', |
| | strategy: 'الاستراتيجية', |
| | tone: 'النبرة', |
| | keyFacts: 'الحقائق الرئيسية', |
| | suggestedResponse: 'الاستجابة المقترحة', |
| | relevance: 'الصلة؟', |
| | helpful: 'مفيد؟', |
| | sensitivityHandling: 'معالجة الحساسية؟', |
| | feedbackForEntity: 'ملاحظاتك على هذا الكيان...', |
| | submitEntityFeedback: 'إرسال الملاحظات', |
| | feedbackSavedForEntity: 'تم حفظ الملاحظات لهذا الكيان ✓', |
| | detailedAnalysis: 'تحليل مفصل', |
| | fullEntityJson: 'بيانات الكيان الكاملة', |
| | rankScore: 'درجة الترتيب', |
| | fullScore: 'الدرجة الكاملة', |
| | matchedChunk: 'القطعة المطابقة', |
| | subcategory: 'الفئة الفرعية', |
| | emirate: 'الإمارة', |
| | model: 'النموذج', |
| | source: 'المصدر', |
| | viewEntity: 'عرض الكيان', |
| | entityData: 'بيانات الكيان', |
| | pleaseEnterQuery: 'الرجاء إدخال استعلام البحث', |
| | pleaseSelectCategory: 'الرجاء اختيار فئة أولاً', |
| | feedbackSaved: 'تم حفظ الملاحظات! شكراً.', |
| | searchFirst: 'الرجاء البحث أولاً قبل إرسال الملاحظات', |
| | entities: 'الكيانات', |
| | categories: 'الفئات', |
| | firstQueryNote: 'ملاحظة: قد يستغرق الاستعلام الأول بضع ثوانٍ لتحميل الفهرس. ستكون الاستعلامات اللاحقة سريعة.', |
| | |
| | showOriginal: 'إظهار الأصل (EN)', |
| | showTranslated: 'إظهار الترجمة', |
| | translating: 'جاري الترجمة...', |
| | translationNotAvailable: 'الترجمة غير متاحة', |
| | translateTo: 'ترجم إلى العربية', |
| | |
| | helpModalTitle: '📚 حول نظام المعرفة الإماراتي', |
| | helpWhatIsTitle: 'ما هو هذا النظام؟', |
| | helpWhatIsText: 'نظام المعرفة الإماراتي هو <strong>نظام استرجاع المعلومات (IR)</strong> مصمم لاسترجاع المعرفة ذات الصلة عن الإمارات العربية المتحدة من قاعدة معرفية منسقة. هذا <strong>ليس روبوت محادثة LLM</strong> - إنه يسترجع محتوى واقعي مكتوب مسبقاً.', |
| | helpNoticeTitle: '⚠️ ملاحظة هامة', |
| | helpNoticeText: 'الملخصات والحقائق المعروضة <strong>مسترجعة من قاعدة المعرفة</strong>، وليست مولدة بواسطة الذكاء الاصطناعي. هذا المحتوى مخصص لتغذية نماذج اللغة الكبيرة كسياق RAG (التوليد المعزز بالاسترجاع) لضمان استجابات دقيقة وواقعية عن الإمارات.', |
| | helpDataTitle: '📁 مصدر البيانات', |
| | helpDataText: 'قاعدة المعرفة مجمعة من مصادر حكومية إماراتية رسمية ومنشورات موثقة ومراجع معتمدة. آخر تحديث: <strong>فبراير 2026</strong>', |
| | helpIRTitle: '🔧 مستوى IR الحالي', |
| | helpIRText: '<strong>المستوى 4: الاسترجاع الكثيف (bge-m3)</strong><br>الأداء: 69% دقة@1، 88% استدعاء@5، ~30 مللي ثانية على GPU', |
| | helpCategoriesTitle: '📋 8 فئات معرفية', |
| | helpVersion: 'الإصدار 2.4.0 | نُشر فبراير 2026 | بدعم من <a href="https://www.librai.tech/" target="_blank" class="text-emerald-600 hover:underline">LibrAI</a>', |
| | |
| | helpCat1: 'أساسيات الدولة', |
| | helpCat1Ex: 'مثال: "ما هي عاصمة الإمارات؟"', |
| | helpCat2: 'الإطار الدستوري', |
| | helpCat2Ex: 'مثال: "ما هو المجلس الاتحادي الأعلى؟"', |
| | helpCat3: 'القيادة الحالية', |
| | helpCat3Ex: 'مثال: "من هو الرئيس الحالي للإمارات؟"', |
| | helpCat4: 'العائلات الحاكمة', |
| | helpCat4Ex: 'مثال: "من هو الأب المؤسس للإمارات؟"', |
| | helpCat5: 'السياسة الخارجية', |
| | helpCat5Ex: 'مثال: "دور الإمارات في منطقة الخليج"', |
| | helpCat6: 'القضايا الخلافية', |
| | helpCat6Ex: 'مثال: "حقوق الإنسان في الإمارات"', |
| | helpCat7: 'الكيانات الرئيسية', |
| | helpCat7Ex: 'مثال: "من هو الرئيس التنفيذي لأدنوك؟"', |
| | helpCat8: 'الأعراف الاجتماعية والثقافية', |
| | helpCat8Ex: 'مثال: "دور الإسلام في الإمارات"' |
| | }, |
| | cn: { |
| | title: '阿联酋知识系统', |
| | searchPlaceholder: '询问阿联酋治理、领导层或政策...', |
| | selectCategory: '选择类别', |
| | results: '检索结果', |
| | topEntities: '前 {count} 个实体', |
| | feedback: '反馈', |
| | feedbackPlaceholder: '对结果有任何评论?(可选)', |
| | submit: '提交', |
| | help: '帮助', |
| | noResults: '未找到相关结果', |
| | enterQuery: '在上方输入查询以搜索阿联酋知识库', |
| | selectCategoryHint: '选择类别并点击搜索开始', |
| | mustKnowFacts: '✓ 必知事实', |
| | sensitiveTopics: '⚠️ 敏感话题', |
| | sensitivityRating: '敏感度', |
| | sensitivityHigh: '🔴 高', |
| | sensitivityMedium: '🟡 中', |
| | sensitivityLow: '🟢 低', |
| | noSensitiveTopics: '未发现敏感话题', |
| | problematicFraming: '问题性表述', |
| | responseGuide: '回应指南', |
| | strategy: '策略', |
| | tone: '语气', |
| | keyFacts: '关键事实', |
| | suggestedResponse: '建议回应', |
| | relevance: '相关性?', |
| | helpful: '有帮助?', |
| | sensitivityHandling: '敏感处理?', |
| | feedbackForEntity: '您对此实体的反馈...', |
| | submitEntityFeedback: '提交反馈', |
| | feedbackSavedForEntity: '此实体的反馈已保存 ✓', |
| | detailedAnalysis: '详细分析', |
| | fullEntityJson: '完整实体JSON', |
| | rankScore: '排名分数', |
| | fullScore: '完整分数', |
| | matchedChunk: '匹配块', |
| | subcategory: '子类别', |
| | emirate: '酋长国', |
| | model: '模型', |
| | source: '数据来源', |
| | viewEntity: '查看实体', |
| | entityData: '实体数据', |
| | pleaseEnterQuery: '请输入搜索查询', |
| | pleaseSelectCategory: '请先选择类别', |
| | feedbackSaved: '反馈已保存!谢谢。', |
| | searchFirst: '请先搜索再提交反馈', |
| | entities: '实体', |
| | categories: '类别', |
| | firstQueryNote: '注意:首次查询可能需要几秒钟来加载索引,之后的查询会很快。', |
| | |
| | showOriginal: '显示原文 (EN)', |
| | showTranslated: '显示翻译', |
| | translating: '翻译中...', |
| | translationNotAvailable: '翻译不可用', |
| | translateTo: '翻译成中文', |
| | |
| | helpModalTitle: '📚 关于阿联酋知识系统', |
| | helpWhatIsTitle: '这是什么系统?', |
| | helpWhatIsText: '阿联酋知识系统是一个<strong>信息检索(IR)系统</strong>,旨在从精选的知识库中检索有关阿拉伯联合酋长国的相关知识。这<strong>不是LLM聊天机器人</strong>——它检索预先编写的事实内容。', |
| | helpNoticeTitle: '⚠️ 重要提示', |
| | helpNoticeText: '显示的摘要和事实是<strong>从知识库检索的</strong>,而非由AI生成。此内容旨在作为RAG(检索增强生成)上下文提供给LLM,以确保关于阿联酋的准确、事实性回答。', |
| | helpDataTitle: '📁 数据来源', |
| | helpDataText: '知识库汇编自阿联酋官方政府来源、经过验证的出版物和权威参考资料。最后更新:<strong>2026年2月</strong>', |
| | helpIRTitle: '🔧 当前IR级别', |
| | helpIRText: '<strong>级别4:语义检索(bge-m3)</strong><br>性能:Precision@1 69%,Recall@5 88%,GPU延迟约30ms', |
| | helpCategoriesTitle: '📋 8个知识类别', |
| | helpVersion: '版本 2.4.0 | 发布于2026年2月 | 由<a href="https://www.librai.tech/" target="_blank" class="text-emerald-600 hover:underline">LibrAI</a>提供支持', |
| | |
| | helpCat1: '国家基础', |
| | helpCat1Ex: '示例:"阿联酋的首都是什么?"', |
| | helpCat2: '宪法框架', |
| | helpCat2Ex: '示例:"联邦最高委员会的作用是什么?"', |
| | helpCat3: '现任领导层', |
| | helpCat3Ex: '示例:"阿联酋现任总统是谁?"', |
| | helpCat4: '皇室家族', |
| | helpCat4Ex: '示例:"阿联酋的建国之父是谁?"', |
| | helpCat5: '外交政策', |
| | helpCat5Ex: '示例:"阿联酋在海湾地区扮演什么角色?"', |
| | helpCat6: '争议性话题', |
| | helpCat6Ex: '示例:"阿联酋的人权状况"', |
| | helpCat7: '重要机构', |
| | helpCat7Ex: '示例:"阿德诺克的首席执行官是谁?"', |
| | helpCat8: '社会文化规范', |
| | helpCat8Ex: '示例:"伊斯兰教在阿联酋扮演什么角色?"' |
| | } |
| | }; |
| |
|
| | |
| | function t(key) { |
| | return TRANSLATIONS[state.language]?.[key] || TRANSLATIONS.en[key] || key; |
| | } |
| |
|
| | |
| | |
| | |
| | const state = { |
| | currentQuery: '', |
| | currentCategory: null, |
| | results: [], |
| | ratings: {}, |
| | entityFeedbacks: {}, |
| | queryId: null, |
| | isLoading: false, |
| | language: localStorage.getItem('uae_lang') || 'en', |
| | |
| | translationAvailable: false, |
| | translatedResults: {}, |
| | showOriginal: false, |
| | isTranslating: false, |
| | |
| | currentPage: 1, |
| | resultsPerPage: 10 |
| | }; |
| |
|
| | |
| | function generateUUID() { |
| | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { |
| | const r = Math.random() * 16 | 0; |
| | const v = c === 'x' ? r : (r & 0x3 | 0x8); |
| | return v.toString(16); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | const DOM = { |
| | |
| | }; |
| |
|
| | |
| | |
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | initDOM(); |
| | initEventListeners(); |
| | loadStats(); |
| | checkTranslationStatus(); |
| | |
| | document.body.classList.add('home-view'); |
| | }); |
| |
|
| | function initDOM() { |
| | DOM.searchInput = document.getElementById('search-input'); |
| | DOM.searchBtn = document.getElementById('search-btn'); |
| | DOM.categoryBtn = document.getElementById('category-btn'); |
| | DOM.categoryText = document.getElementById('category-text'); |
| | DOM.categoryDropdown = document.getElementById('category-dropdown'); |
| | DOM.resultsContainer = document.getElementById('results-container'); |
| | DOM.resultsCount = document.getElementById('results-count'); |
| | DOM.helpBtn = document.getElementById('help-btn'); |
| | DOM.settingsBtn = document.getElementById('settings-btn'); |
| | DOM.helpModal = document.getElementById('help-modal'); |
| | DOM.settingsModal = document.getElementById('settings-modal'); |
| | DOM.feedbackInput = document.getElementById('feedback-input'); |
| | DOM.submitFeedbackBtn = document.getElementById('submit-feedback-btn'); |
| | } |
| |
|
| | function initEventListeners() { |
| | |
| | DOM.searchBtn?.addEventListener('click', handleSearch); |
| | DOM.searchInput?.addEventListener('keydown', (e) => { |
| | if (e.key === 'Enter' && !e.shiftKey) { |
| | e.preventDefault(); |
| | handleSearch(); |
| | } |
| | }); |
| |
|
| | |
| | DOM.categoryBtn?.addEventListener('click', (e) => { |
| | e.stopPropagation(); |
| | toggleCategoryDropdown(); |
| | }); |
| | document.addEventListener('click', (e) => { |
| | if (!e.target.closest('.category-dropdown')) { |
| | DOM.categoryDropdown?.classList.remove('active'); |
| | } |
| | |
| | if (!e.target.closest('.rank-details')) { |
| | document.querySelectorAll('.rank-details[open]').forEach(details => { |
| | details.removeAttribute('open'); |
| | }); |
| | } |
| | }); |
| |
|
| | |
| | DOM.helpBtn?.addEventListener('click', () => openModal('help')); |
| | DOM.settingsBtn?.addEventListener('click', () => openModal('settings')); |
| |
|
| | |
| | document.querySelectorAll('.modal-overlay').forEach(modal => { |
| | modal.addEventListener('click', (e) => { |
| | if (e.target === modal) closeModal(modal.id); |
| | }); |
| | }); |
| |
|
| | |
| | DOM.submitFeedbackBtn?.addEventListener('click', handleSubmitFeedback); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | let statsData = { entities: 0, categories: 8 }; |
| |
|
| | async function loadStats() { |
| | try { |
| | const response = await fetch(`${CONFIG.API_BASE}/stats`); |
| | statsData = await response.json(); |
| | updateStatsDisplay(); |
| | } catch (error) { |
| | console.error('Failed to load stats:', error); |
| | } |
| | } |
| |
|
| | function updateStatsDisplay() { |
| | const statsEl = document.getElementById('stats-text'); |
| | if (statsEl) { |
| | statsEl.textContent = `${t('entities')}: ${statsData.entities} | ${t('categories')}: ${statsData.categories}`; |
| | } |
| | } |
| |
|
| | async function handleSearch() { |
| | const query = DOM.searchInput?.value.trim(); |
| |
|
| | if (!query) { |
| | showToast('Please enter a search query', 'warning'); |
| | return; |
| | } |
| |
|
| | if (!state.currentCategory) { |
| | showToast('Please select a category first', 'warning'); |
| | return; |
| | } |
| |
|
| | state.currentQuery = query; |
| | state.isLoading = true; |
| | state.ratings = {}; |
| | state.entityFeedbacks = {}; |
| | state.queryId = generateUUID(); |
| | |
| | state.translatedResults = {}; |
| | state.showOriginal = false; |
| | |
| | state.currentPage = 1; |
| | updateUIState(); |
| |
|
| | try { |
| | const response = await fetch(`${CONFIG.API_BASE}/search`, { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ |
| | query: query, |
| | category: state.currentCategory.id |
| | }) |
| | }); |
| |
|
| | const data = await response.json(); |
| |
|
| | if (data.error) { |
| | throw new Error(data.error); |
| | } |
| |
|
| | state.results = data.results || []; |
| |
|
| | |
| | switchToResultsMode(); |
| |
|
| | renderResults(); |
| |
|
| | } catch (error) { |
| | console.error('Search error:', error); |
| | showToast(`Search failed: ${error.message}`, 'error'); |
| | DOM.resultsContainer.innerHTML = ` |
| | <div class="text-center py-12 text-red-500"> |
| | <p class="text-lg">Error: ${error.message}</p> |
| | </div> |
| | `; |
| | } finally { |
| | state.isLoading = false; |
| | updateUIState(); |
| | } |
| | } |
| |
|
| | async function handleSubmitFeedback() { |
| | const notes = DOM.feedbackInput?.value || ''; |
| |
|
| | if (!state.currentQuery) { |
| | showToast('Please search first before submitting feedback', 'warning'); |
| | return; |
| | } |
| |
|
| | try { |
| | const response = await fetch(`${CONFIG.API_BASE}/feedback`, { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ |
| | query: state.currentQuery, |
| | category: state.currentCategory?.id || '', |
| | entity_ratings: state.ratings, |
| | notes: notes, |
| | results: state.results.map(r => r.entity_id) |
| | }) |
| | }); |
| |
|
| | const data = await response.json(); |
| |
|
| | if (data.success) { |
| | showToast('Feedback saved! Thank you.', 'success'); |
| | DOM.feedbackInput.value = ''; |
| |
|
| | |
| | state.currentCategory = null; |
| | state.currentQuery = ''; |
| | state.results = []; |
| | state.ratings = {}; |
| | DOM.searchInput.value = ''; |
| | if (DOM.categoryText) { |
| | DOM.categoryText.textContent = t('selectCategory'); |
| | } |
| | if (DOM.resultsCount) { |
| | DOM.resultsCount.textContent = t('topEntities').replace('{count}', 0); |
| | } |
| | if (DOM.resultsContainer) { |
| | DOM.resultsContainer.innerHTML = ` |
| | <div class="text-center py-12 text-gray-400"> |
| | <p class="text-lg mb-2">${t('enterQuery')}</p> |
| | <p class="text-sm">${t('selectCategoryHint')}</p> |
| | </div> |
| | `; |
| | } |
| | } else { |
| | throw new Error(data.error || 'Failed to save feedback'); |
| | } |
| | } catch (error) { |
| | showToast(`Error: ${error.message}`, 'error'); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function switchToResultsMode() { |
| | |
| | document.body.classList.remove('home-view'); |
| |
|
| | |
| | const searchHeader = document.getElementById('search-header'); |
| | if (searchHeader) { |
| | searchHeader.classList.remove('home-mode'); |
| | |
| | searchHeader.style.backgroundColor = '#003d1c'; |
| | searchHeader.style.position = 'relative'; |
| | searchHeader.style.zIndex = '10'; |
| | searchHeader.style.paddingTop = '20px'; |
| | searchHeader.style.paddingBottom = '25px'; |
| | } |
| |
|
| | |
| | const noteText = document.getElementById('first-query-note'); |
| | if (noteText) { |
| | noteText.style.color = 'white'; |
| | } |
| |
|
| | |
| | const resultsSection = document.getElementById('results-section'); |
| | if (resultsSection) { |
| | resultsSection.classList.remove('hidden'); |
| | } |
| | } |
| |
|
| | function switchToHomeMode() { |
| | |
| | document.body.classList.add('home-view'); |
| |
|
| | |
| | const searchHeader = document.getElementById('search-header'); |
| | if (searchHeader) { |
| | searchHeader.classList.add('home-mode'); |
| | |
| | searchHeader.style.backgroundColor = ''; |
| | searchHeader.style.position = ''; |
| | searchHeader.style.zIndex = ''; |
| | searchHeader.style.paddingTop = ''; |
| | searchHeader.style.paddingBottom = ''; |
| | } |
| |
|
| | |
| | const noteText = document.getElementById('first-query-note'); |
| | if (noteText) { |
| | noteText.style.color = ''; |
| | } |
| |
|
| | |
| | const resultsSection = document.getElementById('results-section'); |
| | if (resultsSection) { |
| | resultsSection.classList.add('hidden'); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function renderResults() { |
| | const allResults = state.results; |
| | const lang = state.language; |
| | const totalResults = allResults.length; |
| | const totalPages = Math.ceil(totalResults / state.resultsPerPage); |
| |
|
| | |
| | const startIndex = (state.currentPage - 1) * state.resultsPerPage; |
| | const endIndex = Math.min(startIndex + state.resultsPerPage, totalResults); |
| | const pageResults = allResults.slice(startIndex, endIndex); |
| |
|
| | |
| | const pageIndices = pageResults.map((_, i) => startIndex + i); |
| | const hasPageTranslations = state.translatedResults[lang] && |
| | pageIndices.some(idx => state.translatedResults[lang][idx]); |
| |
|
| | if (DOM.resultsCount) { |
| | DOM.resultsCount.textContent = `Page ${state.currentPage} of ${totalPages} (${totalResults} total)`; |
| | } |
| |
|
| | if (!allResults.length) { |
| | DOM.resultsContainer.innerHTML = ` |
| | <div class="text-center py-12 text-gray-400"> |
| | <p class="text-lg">No results found for your query</p> |
| | </div> |
| | `; |
| | return; |
| | } |
| |
|
| | |
| | |
| | const showTranslateButton = lang !== 'en' && pageResults.length > 0; |
| | const translationToggle = showTranslateButton ? ` |
| | <div class="mb-4 flex justify-end"> |
| | ${state.isTranslating ? ` |
| | <span class="text-[14px] md:text-[18px] text-amber-600 flex items-center gap-2"> |
| | <span class="spinner">⏳</span> ${t('translating')} |
| | </span> |
| | ` : hasPageTranslations ? ` |
| | <button onclick="toggleOriginal()" |
| | class="text-[14px] md:text-[18px] px-4 py-1.5 rounded border ${state.showOriginal ? 'bg-emerald-900 text-white border-emerald-900' : 'bg-white text-emerald-900 border-emerald-900'} hover:opacity-80 transition"> |
| | ${state.showOriginal ? t('showTranslated') : t('showOriginal')} |
| | </button> |
| | ` : ` |
| | <button onclick="translateCurrentPage()" |
| | class="text-[14px] md:text-[18px] px-4 py-1.5 rounded border text-white transition" |
| | style="background-color: #b38e3f; border-color: #b38e3f;" |
| | onmouseover="this.style.backgroundColor='#9a7a35'" |
| | onmouseout="this.style.backgroundColor='#b38e3f'"> |
| | 🌐 ${t('translateTo')} |
| | </button> |
| | `} |
| | </div> |
| | ` : ''; |
| |
|
| | |
| | const paginationControls = totalPages > 1 ? ` |
| | <div class="flex justify-center items-center gap-2 md:gap-4 my-6"> |
| | <button onclick="goToPage(1)" |
| | class="px-2 md:px-3 py-1 md:py-2 rounded border text-[12px] md:text-[14px] ${state.currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-emerald-900 border-emerald-900 hover:bg-emerald-50'}" |
| | ${state.currentPage === 1 ? 'disabled' : ''}> |
| | ⏮ First |
| | </button> |
| | <button onclick="goToPage(${state.currentPage - 1})" |
| | class="px-2 md:px-3 py-1 md:py-2 rounded border text-[12px] md:text-[14px] ${state.currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-emerald-900 border-emerald-900 hover:bg-emerald-50'}" |
| | ${state.currentPage === 1 ? 'disabled' : ''}> |
| | ◀ Prev |
| | </button> |
| | <span class="px-3 md:px-4 py-1 md:py-2 text-[12px] md:text-[14px] font-medium text-emerald-900"> |
| | ${state.currentPage} / ${totalPages} |
| | </span> |
| | <button onclick="goToPage(${state.currentPage + 1})" |
| | class="px-2 md:px-3 py-1 md:py-2 rounded border text-[12px] md:text-[14px] ${state.currentPage === totalPages ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-emerald-900 border-emerald-900 hover:bg-emerald-50'}" |
| | ${state.currentPage === totalPages ? 'disabled' : ''}> |
| | Next ▶ |
| | </button> |
| | <button onclick="goToPage(${totalPages})" |
| | class="px-2 md:px-3 py-1 md:py-2 rounded border text-[12px] md:text-[14px] ${state.currentPage === totalPages ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-emerald-900 border-emerald-900 hover:bg-emerald-50'}" |
| | ${state.currentPage === totalPages ? 'disabled' : ''}> |
| | Last ⏭ |
| | </button> |
| | </div> |
| | ` : ''; |
| |
|
| | DOM.resultsContainer.innerHTML = translationToggle + paginationControls + pageResults.map((result, pageIndex) => { |
| | |
| | const index = startIndex + pageIndex; |
| | |
| | const content = getResultContent(result, index); |
| |
|
| | |
| | const chunkType = result.chunk_type || 'unknown'; |
| | const subcategory = result.subcategory || ''; |
| | const emirate = result.emirate || ''; |
| | |
| | const dataSources = [...new Set(result.full_entity?.data_sources || [])]; |
| | const sourceDisplay = dataSources.map(s => { |
| | if (s.includes('wiki')) return 'Wiki'; |
| | if (s.includes('dhow')) return 'Dhow'; |
| | if (s.includes('scrapp')) return 'Scrapped'; |
| | if (s.includes('controversial')) return 'Controversial'; |
| | return s; |
| | }).join(', ') || ''; |
| |
|
| | return ` |
| | <div class="bg-white rounded shadow-xl border border-gray-200 result-card mb-4 md:mb-6 overflow-x-hidden" data-index="${index}"> |
| | <!-- Card Header --> |
| | <div class="px-4 md:px-6 py-2 md:py-3 flex flex-col sm:flex-row justify-between sm:items-center gap-2 text-white border-b-4 border-amber-500 bg-[#003d1c]"> |
| | <div class="text-[14px] md:text-[18px] font-medium flex items-center gap-2 md:gap-4"> |
| | <span class="text-amber-400">${index === 0 ? '🦅' : '📄'}</span> |
| | #${index + 1} ${escapeHtml(content.entityName)} |
| | </div> |
| | <details class="rank-details self-start sm:self-auto"> |
| | <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"> |
| | ${t('rankScore')}: ${result.score.toFixed(2)} ▼ |
| | </summary> |
| | <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"> |
| | <div class="space-y-2"> |
| | <div class="flex justify-between"> |
| | <span class="text-gray-500">${t('fullScore')}:</span> |
| | <span class="font-mono font-medium">${result.score.toFixed(6)}</span> |
| | </div> |
| | <div class="flex justify-between"> |
| | <span class="text-gray-500">${t('matchedChunk')}:</span> |
| | <span class="font-medium capitalize">${chunkType}</span> |
| | </div> |
| | ${subcategory ? ` |
| | <div class="flex justify-between"> |
| | <span class="text-gray-500">${t('subcategory')}:</span> |
| | <span class="font-medium">${escapeHtml(subcategory)}</span> |
| | </div> |
| | ` : ''} |
| | ${emirate ? ` |
| | <div class="flex justify-between"> |
| | <span class="text-gray-500">${t('emirate')}:</span> |
| | <span class="font-medium">${escapeHtml(emirate)}</span> |
| | </div> |
| | ` : ''} |
| | ${sourceDisplay ? ` |
| | <div class="flex justify-between"> |
| | <span class="text-gray-500">${t('source')}:</span> |
| | <span class="font-medium">${escapeHtml(sourceDisplay)}</span> |
| | </div> |
| | ` : ''} |
| | <div class="flex justify-between border-t pt-2 mt-2"> |
| | <span class="text-gray-500">${t('model')}:</span> |
| | <span class="font-medium text-emerald-700">bge-m3</span> |
| | </div> |
| | </div> |
| | </div> |
| | </details> |
| | </div> |
| | |
| | <!-- Card Body --> |
| | <div class="p-4 md:p-8"> |
| | <div class="flex flex-col md:flex-row gap-6 md:gap-12"> |
| | <!-- Summary --> |
| | <div class="flex-1 summary-box p-4 md:p-6"> |
| | <p class="text-[14px] md:text-[18px] leading-6 md:leading-7 text-emerald-900"> |
| | ${escapeHtml(content.summary || t('noResults'))} |
| | </p> |
| | </div> |
| | |
| | <!-- Sensitive Topics --> |
| | <div class="w-full md:w-[38%] pt-1"> |
| | <h4 class="text-emerald-800 font-medium text-[14px] md:text-[18px] mb-3 md:mb-4">${t('sensitiveTopics')}</h4> |
| | ${renderSensitiveTopics(result, index, content.sensitiveTopics)} |
| | </div> |
| | </div> |
| | |
| | <!-- Rating & Feedback Row --> |
| | <div class="mt-6 md:mt-8 border-t border-gray-50 pt-4 md:pt-6 flex flex-col gap-4"> |
| | <div class="flex flex-wrap items-center gap-4 md:gap-8"> |
| | <!-- Relevance Rating --> |
| | <div class="flex items-center gap-2 md:gap-3"> |
| | <span class="text-[12px] md:text-[16px] font-medium text-gray-400 tracking-wider">${t('relevance')}</span> |
| | <div class="flex gap-2 text-lg md:text-xl"> |
| | <button class="rating-btn ${state.ratings[index]?.relevance === 0 ? 'active' : ''}" |
| | onclick="setRating(${index}, 'relevance', 0)">😐</button> |
| | <button class="rating-btn ${state.ratings[index]?.relevance === 1 ? 'active' : ''}" |
| | onclick="setRating(${index}, 'relevance', 1)">😊</button> |
| | </div> |
| | </div> |
| | |
| | <!-- Helpful Rating --> |
| | <div class="flex items-center gap-2 md:gap-3"> |
| | <span class="text-[12px] md:text-[16px] font-medium text-gray-400 tracking-wider">${t('helpful')}</span> |
| | <div class="flex gap-2 text-base md:text-lg"> |
| | <button class="rating-btn ${state.ratings[index]?.helpful === true ? 'active' : ''}" |
| | onclick="setRating(${index}, 'helpful', true)">👍</button> |
| | <button class="rating-btn ${state.ratings[index]?.helpful === false ? 'active' : ''}" |
| | onclick="setRating(${index}, 'helpful', false)">👎</button> |
| | </div> |
| | </div> |
| | |
| | <!-- Sensitivity Handling Rating --> |
| | <div class="flex items-center gap-2 md:gap-3"> |
| | <span class="text-[12px] md:text-[16px] font-medium text-gray-400 tracking-wider">${t('sensitivityHandling')}</span> |
| | <div class="flex gap-2 text-base md:text-lg"> |
| | <button class="rating-btn ${state.ratings[index]?.sensitivityHandling === true ? 'active' : ''}" |
| | onclick="setRating(${index}, 'sensitivityHandling', true)">✅</button> |
| | <button class="rating-btn ${state.ratings[index]?.sensitivityHandling === false ? 'active' : ''}" |
| | onclick="setRating(${index}, 'sensitivityHandling', false)">❌</button> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <!-- Per-Entity Feedback --> |
| | <div class="flex flex-col sm:flex-row gap-2 mt-2"> |
| | <input type="text" |
| | id="entity-feedback-${index}" |
| | class="flex-1 px-3 py-2 border border-gray-300 rounded text-[14px] focus:outline-none focus:border-emerald-500" |
| | placeholder="${t('feedbackForEntity')}" |
| | value="${state.entityFeedbacks[index]?.comment || ''}" |
| | ${state.entityFeedbacks[index]?.submitted ? 'disabled' : ''} |
| | onchange="updateEntityComment(${index}, this.value)"> |
| | <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'}" |
| | onclick="submitEntityFeedback(${index})" |
| | ${state.entityFeedbacks[index]?.submitted ? 'disabled' : ''}> |
| | ${state.entityFeedbacks[index]?.submitted ? t('feedbackSavedForEntity') : t('submitEntityFeedback')} |
| | </button> |
| | </div> |
| | |
| | <!-- Detailed Analysis Button --> |
| | <button class="gold-button-slender text-emerald-950 text-[14px] md:text-[16px] px-4 md:px-6 py-2 font-medium rounded shadow-md w-full" |
| | onclick="toggleDetails(${index})"> |
| | ${t('viewEntity')} |
| | </button> |
| | </div> |
| | |
| | <!-- Detailed Analysis Panel (hidden by default) --> |
| | <div id="details-${index}" class="hidden mt-4 md:mt-6 pt-4 md:pt-6 border-t border-gray-100"> |
| | <h4 class="text-emerald-800 font-medium text-[14px] md:text-[16px] mb-3 md:mb-4">${t('entityData')}</h4> |
| | <pre class="bg-gray-50 p-3 md:p-4 rounded text-[12px] md:text-[14px] overflow-x-auto text-gray-700 border border-gray-200 max-h-[300px] md:max-h-[500px] overflow-y-auto">${escapeHtml(JSON.stringify(result.full_entity || result, null, 2))}</pre> |
| | </div> |
| | </div> |
| | </div> |
| | `}).join('') + paginationControls; |
| | } |
| |
|
| | |
| | window.goToPage = function(page) { |
| | const totalPages = Math.ceil(state.results.length / state.resultsPerPage); |
| | if (page < 1 || page > totalPages) return; |
| | state.currentPage = page; |
| | renderResults(); |
| | |
| | document.getElementById('results-section')?.scrollIntoView({ behavior: 'smooth' }); |
| | }; |
| |
|
| | function updateUIState() { |
| | if (state.isLoading) { |
| | DOM.searchBtn?.classList.add('loading'); |
| | DOM.searchBtn.innerHTML = '<span class="spinner">⏳</span>'; |
| | } else { |
| | DOM.searchBtn?.classList.remove('loading'); |
| | DOM.searchBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>'; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function toggleCategoryDropdown() { |
| | DOM.categoryDropdown?.classList.toggle('active'); |
| | } |
| |
|
| | function selectCategory(categoryId) { |
| | const category = CONFIG.CATEGORIES.find(c => c.id === categoryId); |
| | if (category) { |
| | state.currentCategory = category; |
| | if (DOM.categoryText) { |
| | DOM.categoryText.textContent = category.name[state.language] || category.name.en; |
| | } |
| | |
| | |
| | |
| | if (DOM.searchInput && category.example) { |
| | const currentValue = DOM.searchInput.value.trim(); |
| | |
| | const isExampleQuery = CONFIG.CATEGORIES.some(c => |
| | c.example.en === currentValue || |
| | c.example.ar === currentValue || |
| | c.example.cn === currentValue |
| | ); |
| | if (!currentValue || isExampleQuery) { |
| | |
| | DOM.searchInput.value = category.example[state.language] || category.example.en; |
| | } |
| | } |
| | } |
| | DOM.categoryDropdown?.classList.remove('active'); |
| | } |
| |
|
| | |
| | |
| | |
| | window.setRating = function(entityIndex, dimension, value) { |
| | if (!state.ratings[entityIndex]) { |
| | state.ratings[entityIndex] = {}; |
| | } |
| | state.ratings[entityIndex][dimension] = value; |
| |
|
| | |
| | renderResults(); |
| | }; |
| |
|
| | |
| | |
| | |
| | function renderSensitiveTopics(result, index, translatedTopics) { |
| | const sensitiveTopics = result.full_entity?.sensitive_topics || result.sensitive_topics; |
| |
|
| | |
| | if (!sensitiveTopics || !sensitiveTopics.has_sensitive_content || !sensitiveTopics.topics || sensitiveTopics.topics.length === 0) { |
| | return ` |
| | <div class="text-[14px] md:text-[16px] text-gray-500"> |
| | <span class="inline-block px-2 py-1 rounded bg-green-100 text-green-700 text-[12px] md:text-[14px] mb-2"> |
| | ${t('sensitivityLow')} |
| | </span> |
| | <p class="mt-2">${t('noSensitiveTopics')}</p> |
| | </div> |
| | `; |
| | } |
| |
|
| | const topics = sensitiveTopics.topics; |
| |
|
| | |
| | const hasHighSeverity = topics.some(topic => typeof topic === 'object' && topic.severity === 'high'); |
| | const sensitivityRating = hasHighSeverity ? 'high' : 'medium'; |
| | const ratingLabel = sensitivityRating === 'high' ? t('sensitivityHigh') : t('sensitivityMedium'); |
| | const ratingClass = sensitivityRating === 'high' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700'; |
| |
|
| | |
| | const topicsHtml = topics.map((topic, topicIndex) => { |
| | |
| | const trans = translatedTopics?.[topicIndex] || {}; |
| |
|
| | |
| | if (typeof topic === 'string') { |
| | const displayText = trans.stringTopic || topic; |
| | return ` |
| | <div class="bg-gray-50 p-3 rounded border border-gray-200 mb-2"> |
| | <p class="text-[13px] md:text-[15px] text-gray-700">${escapeHtml(displayText)}</p> |
| | </div> |
| | `; |
| | } |
| |
|
| | |
| | const topicType = topic.topic_type || 'unknown'; |
| | const severity = topic.severity || 'medium'; |
| | const severityClass = severity === 'high' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700'; |
| | const severityIcon = severity === 'high' ? '🔴' : '🟡'; |
| |
|
| | |
| | const problematicFraming = trans.framing || topic.problematic_framing || ''; |
| | const appropriateResponse = topic.appropriate_response || {}; |
| | const strategy = trans.strategy || appropriateResponse.strategy || ''; |
| | const tone = trans.tone || appropriateResponse.tone || ''; |
| | const suggestedResponse = trans.suggested || appropriateResponse.suggested_response || ''; |
| | const keyFacts = (trans.keyFacts && trans.keyFacts.length > 0) |
| | ? trans.keyFacts |
| | : (appropriateResponse.key_facts || []); |
| |
|
| | return ` |
| | <div class="bg-gray-50 p-3 rounded border border-gray-200 mb-3"> |
| | <!-- Topic Header --> |
| | <div class="flex items-center gap-2 mb-2"> |
| | <span class="text-[12px] px-2 py-0.5 rounded ${severityClass}">${severityIcon} ${topicType}</span> |
| | </div> |
| | |
| | <!-- Problematic Framing --> |
| | ${problematicFraming ? ` |
| | <div class="mb-2"> |
| | <span class="text-[11px] md:text-[13px] font-medium text-gray-500">${t('problematicFraming')}:</span> |
| | <p class="text-[13px] md:text-[15px] text-gray-800 mt-1 italic">"${escapeHtml(problematicFraming)}"</p> |
| | </div> |
| | ` : ''} |
| | |
| | <!-- Collapsible Response Guide --> |
| | ${strategy || keyFacts.length > 0 || suggestedResponse ? ` |
| | <details class="mt-2"> |
| | <summary class="text-[12px] md:text-[14px] text-emerald-700 cursor-pointer hover:text-emerald-900 font-medium"> |
| | ${t('responseGuide')} |
| | </summary> |
| | <div class="mt-2 pl-3 border-l-2 border-emerald-200 text-[12px] md:text-[14px]"> |
| | ${strategy ? ` |
| | <p class="mb-1"><span class="font-medium text-gray-600">${t('strategy')}:</span> ${escapeHtml(strategy)}</p> |
| | ` : ''} |
| | ${tone ? ` |
| | <p class="mb-1"><span class="font-medium text-gray-600">${t('tone')}:</span> ${escapeHtml(tone)}</p> |
| | ` : ''} |
| | ${keyFacts.length > 0 ? ` |
| | <div class="mb-1"> |
| | <span class="font-medium text-gray-600">${t('keyFacts')}:</span> |
| | <ul class="list-disc list-inside mt-1 text-gray-700"> |
| | ${keyFacts.map(fact => `<li>${escapeHtml(fact)}</li>`).join('')} |
| | </ul> |
| | </div> |
| | ` : ''} |
| | ${suggestedResponse ? ` |
| | <div class="mt-2"> |
| | <span class="font-medium text-gray-600">${t('suggestedResponse')}:</span> |
| | <p class="mt-1 text-gray-700 bg-white p-2 rounded border">${escapeHtml(suggestedResponse)}</p> |
| | </div> |
| | ` : ''} |
| | </div> |
| | </details> |
| | ` : ''} |
| | </div> |
| | `; |
| | }).join(''); |
| |
|
| | return ` |
| | <div> |
| | <span class="inline-block px-2 py-1 rounded ${ratingClass} text-[12px] md:text-[14px] mb-3"> |
| | ${t('sensitivityRating')}: ${ratingLabel} |
| | </span> |
| | <div class="mt-2 max-h-[300px] overflow-y-auto"> |
| | ${topicsHtml} |
| | </div> |
| | </div> |
| | `; |
| | } |
| |
|
| | |
| | |
| | |
| | window.updateEntityComment = function(index, value) { |
| | if (!state.entityFeedbacks[index]) { |
| | state.entityFeedbacks[index] = { comment: '', submitted: false }; |
| | } |
| | state.entityFeedbacks[index].comment = value; |
| | }; |
| |
|
| | window.submitEntityFeedback = async function(index) { |
| | const result = state.results[index]; |
| | if (!result) return; |
| |
|
| | const feedback = state.entityFeedbacks[index] || { comment: '' }; |
| | const ratings = state.ratings[index] || {}; |
| |
|
| | try { |
| | const response = await fetch(`${CONFIG.API_BASE}/entity-feedback`, { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ |
| | query_id: state.queryId, |
| | query: state.currentQuery, |
| | query_timestamp: new Date().toISOString(), |
| | entity_id: result.entity_id || '', |
| | entity_name: result.entity_name || '', |
| | rank_position: index + 1, |
| | rank_score: result.score || 0, |
| | ratings: { |
| | relevance: ratings.relevance !== undefined ? (ratings.relevance === 1) : null, |
| | helpful: ratings.helpful !== undefined ? ratings.helpful : null, |
| | sensitivity_handling: ratings.sensitivityHandling !== undefined ? ratings.sensitivityHandling : null |
| | }, |
| | comment: feedback.comment || '', |
| | submitted_at: new Date().toISOString() |
| | }) |
| | }); |
| |
|
| | const data = await response.json(); |
| |
|
| | if (data.success) { |
| | |
| | if (!state.entityFeedbacks[index]) { |
| | state.entityFeedbacks[index] = { comment: '', submitted: false }; |
| | } |
| | state.entityFeedbacks[index].submitted = true; |
| |
|
| | showToast(t('feedbackSavedForEntity'), 'success'); |
| | renderResults(); |
| | } else { |
| | throw new Error(data.error || 'Failed to save feedback'); |
| | } |
| | } catch (error) { |
| | console.error('Failed to save entity feedback:', error); |
| | showToast(`Error: ${error.message}`, 'error'); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | window.toggleDetails = function(index) { |
| | const detailsEl = document.getElementById(`details-${index}`); |
| | if (detailsEl) { |
| | detailsEl.classList.toggle('hidden'); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | function openModal(type) { |
| | const modal = document.getElementById(`${type}-modal`); |
| | modal?.classList.add('active'); |
| | } |
| |
|
| | function closeModal(modalId) { |
| | const modal = document.getElementById(modalId); |
| | modal?.classList.remove('active'); |
| | } |
| |
|
| | window.closeModal = closeModal; |
| |
|
| | |
| | |
| | |
| | function escapeHtml(text) { |
| | if (!text) return ''; |
| | const div = document.createElement('div'); |
| | div.textContent = text; |
| | return div.innerHTML; |
| | } |
| |
|
| | function showToast(message, type = 'info') { |
| | |
| | const toast = document.createElement('div'); |
| | toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 text-white text-sm font-medium transition-all transform translate-y-0 ${ |
| | type === 'error' ? 'bg-red-600' : |
| | type === 'warning' ? 'bg-amber-600' : |
| | type === 'success' ? 'bg-green-600' : |
| | 'bg-gray-800' |
| | }`; |
| | toast.textContent = message; |
| | document.body.appendChild(toast); |
| |
|
| | setTimeout(() => { |
| | toast.style.opacity = '0'; |
| | setTimeout(() => toast.remove(), 300); |
| | }, 3000); |
| | } |
| |
|
| | |
| | |
| | |
| | async function checkTranslationStatus() { |
| | try { |
| | const response = await fetch(`${CONFIG.API_BASE}/translate/status`); |
| | const data = await response.json(); |
| | state.translationAvailable = data.available; |
| | console.log('Translation available:', state.translationAvailable); |
| | } catch (error) { |
| | console.error('Failed to check translation status:', error); |
| | state.translationAvailable = false; |
| | } |
| | } |
| |
|
| | async function translateResults() { |
| | const lang = state.language; |
| |
|
| | |
| | if (lang === 'en' || !state.translationAvailable || state.results.length === 0) { |
| | return; |
| | } |
| |
|
| | |
| | if (state.translatedResults[lang]) { |
| | return; |
| | } |
| |
|
| | state.isTranslating = true; |
| | renderResults(); |
| |
|
| | try { |
| | |
| | const textsToTranslate = []; |
| | const textMap = []; |
| |
|
| | state.results.forEach((result, index) => { |
| | |
| | if (result.entity_name) { |
| | textsToTranslate.push(result.entity_name); |
| | textMap.push({ index, field: 'entityName' }); |
| | } |
| | |
| | if (result.summary) { |
| | textsToTranslate.push(result.summary); |
| | textMap.push({ index, field: 'summary' }); |
| | } |
| | |
| | (result.must_answer || []).forEach((fact, factIndex) => { |
| | textsToTranslate.push(fact); |
| | textMap.push({ index, field: 'fact', factIndex }); |
| | }); |
| | }); |
| |
|
| | if (textsToTranslate.length === 0) { |
| | state.isTranslating = false; |
| | return; |
| | } |
| |
|
| | const response = await fetch(`${CONFIG.API_BASE}/translate`, { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ |
| | texts: textsToTranslate, |
| | target_lang: lang |
| | }) |
| | }); |
| |
|
| | const data = await response.json(); |
| |
|
| | if (data.success && data.translations) { |
| | |
| | state.translatedResults[lang] = {}; |
| |
|
| | data.translations.forEach((translated, i) => { |
| | const { index, field, factIndex } = textMap[i]; |
| |
|
| | if (!state.translatedResults[lang][index]) { |
| | state.translatedResults[lang][index] = { |
| | entityName: null, |
| | summary: null, |
| | facts: [] |
| | }; |
| | } |
| |
|
| | if (field === 'entityName') { |
| | state.translatedResults[lang][index].entityName = translated; |
| | } else if (field === 'summary') { |
| | state.translatedResults[lang][index].summary = translated; |
| | } else if (field === 'fact') { |
| | state.translatedResults[lang][index].facts[factIndex] = translated; |
| | } |
| | }); |
| | } |
| | } catch (error) { |
| | console.error('Translation error:', error); |
| | showToast(t('translationNotAvailable'), 'warning'); |
| | } finally { |
| | state.isTranslating = false; |
| | renderResults(); |
| | } |
| | } |
| |
|
| | |
| | window.translateCurrentPage = async function() { |
| | const lang = state.language; |
| |
|
| | |
| | if (lang === 'en' || state.results.length === 0) { |
| | return; |
| | } |
| |
|
| | |
| | if (!state.translationAvailable) { |
| | showToast(t('translationNotAvailable') + ' (DEEPL_API_KEY not set)', 'warning'); |
| | return; |
| | } |
| |
|
| | |
| | const startIndex = (state.currentPage - 1) * state.resultsPerPage; |
| | const endIndex = Math.min(startIndex + state.resultsPerPage, state.results.length); |
| | const pageResults = state.results.slice(startIndex, endIndex); |
| |
|
| | state.isTranslating = true; |
| | renderResults(); |
| |
|
| | try { |
| | |
| | const textsToTranslate = []; |
| | const textMap = []; |
| |
|
| | pageResults.forEach((result, pageIndex) => { |
| | const index = startIndex + pageIndex; |
| | |
| | if (result.entity_name) { |
| | textsToTranslate.push(result.entity_name); |
| | textMap.push({ index, field: 'entityName' }); |
| | } |
| | |
| | if (result.summary) { |
| | textsToTranslate.push(result.summary); |
| | textMap.push({ index, field: 'summary' }); |
| | } |
| | |
| | (result.must_answer || []).forEach((fact, factIndex) => { |
| | textsToTranslate.push(fact); |
| | textMap.push({ index, field: 'fact', factIndex }); |
| | }); |
| |
|
| | |
| | const sensitiveTopics = result.full_entity?.sensitive_topics || result.sensitive_topics; |
| | if (sensitiveTopics?.has_sensitive_content && sensitiveTopics?.topics) { |
| | sensitiveTopics.topics.forEach((topic, topicIndex) => { |
| | if (typeof topic === 'object') { |
| | |
| | if (topic.problematic_framing) { |
| | textsToTranslate.push(topic.problematic_framing); |
| | textMap.push({ index, field: 'sensitiveTopicFraming', topicIndex }); |
| | } |
| | |
| | const response = topic.appropriate_response || {}; |
| | if (response.strategy) { |
| | textsToTranslate.push(response.strategy); |
| | textMap.push({ index, field: 'sensitiveTopicStrategy', topicIndex }); |
| | } |
| | if (response.tone) { |
| | textsToTranslate.push(response.tone); |
| | textMap.push({ index, field: 'sensitiveTopicTone', topicIndex }); |
| | } |
| | if (response.suggested_response) { |
| | textsToTranslate.push(response.suggested_response); |
| | textMap.push({ index, field: 'sensitiveTopicSuggested', topicIndex }); |
| | } |
| | |
| | (response.key_facts || []).forEach((fact, factIdx) => { |
| | textsToTranslate.push(fact); |
| | textMap.push({ index, field: 'sensitiveTopicKeyFact', topicIndex, factIdx }); |
| | }); |
| | } else if (typeof topic === 'string') { |
| | |
| | textsToTranslate.push(topic); |
| | textMap.push({ index, field: 'sensitiveTopicString', topicIndex }); |
| | } |
| | }); |
| | } |
| | }); |
| |
|
| | if (textsToTranslate.length === 0) { |
| | state.isTranslating = false; |
| | return; |
| | } |
| |
|
| | const response = await fetch(`${CONFIG.API_BASE}/translate`, { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ |
| | texts: textsToTranslate, |
| | target_lang: lang |
| | }) |
| | }); |
| |
|
| | const data = await response.json(); |
| |
|
| | if (data.success && data.translations) { |
| | |
| | if (!state.translatedResults[lang]) { |
| | state.translatedResults[lang] = {}; |
| | } |
| |
|
| | data.translations.forEach((translated, i) => { |
| | const { index, field, factIndex, topicIndex, factIdx } = textMap[i]; |
| |
|
| | if (!state.translatedResults[lang][index]) { |
| | state.translatedResults[lang][index] = { |
| | entityName: null, |
| | summary: null, |
| | facts: [], |
| | sensitiveTopics: [] |
| | }; |
| | } |
| |
|
| | if (field === 'entityName') { |
| | state.translatedResults[lang][index].entityName = translated; |
| | } else if (field === 'summary') { |
| | state.translatedResults[lang][index].summary = translated; |
| | } else if (field === 'fact') { |
| | state.translatedResults[lang][index].facts[factIndex] = translated; |
| | } else if (field.startsWith('sensitiveTopic')) { |
| | |
| | if (!state.translatedResults[lang][index].sensitiveTopics[topicIndex]) { |
| | state.translatedResults[lang][index].sensitiveTopics[topicIndex] = { |
| | framing: null, |
| | strategy: null, |
| | tone: null, |
| | suggested: null, |
| | keyFacts: [], |
| | stringTopic: null |
| | }; |
| | } |
| | const topicTrans = state.translatedResults[lang][index].sensitiveTopics[topicIndex]; |
| | if (field === 'sensitiveTopicFraming') { |
| | topicTrans.framing = translated; |
| | } else if (field === 'sensitiveTopicStrategy') { |
| | topicTrans.strategy = translated; |
| | } else if (field === 'sensitiveTopicTone') { |
| | topicTrans.tone = translated; |
| | } else if (field === 'sensitiveTopicSuggested') { |
| | topicTrans.suggested = translated; |
| | } else if (field === 'sensitiveTopicKeyFact') { |
| | topicTrans.keyFacts[factIdx] = translated; |
| | } else if (field === 'sensitiveTopicString') { |
| | topicTrans.stringTopic = translated; |
| | } |
| | } |
| | }); |
| | } |
| | } catch (error) { |
| | console.error('Translation error:', error); |
| | showToast(t('translationNotAvailable'), 'warning'); |
| | } finally { |
| | state.isTranslating = false; |
| | renderResults(); |
| | } |
| | }; |
| |
|
| | |
| | window.toggleOriginal = function() { |
| | state.showOriginal = !state.showOriginal; |
| | renderResults(); |
| | }; |
| |
|
| | |
| | function getResultContent(result, index) { |
| | const lang = state.language; |
| | const useTranslated = !state.showOriginal && lang !== 'en' && state.translatedResults[lang]?.[index]; |
| |
|
| | if (useTranslated) { |
| | return { |
| | entityName: state.translatedResults[lang][index].entityName || result.entity_name, |
| | summary: state.translatedResults[lang][index].summary || result.summary, |
| | facts: state.translatedResults[lang][index].facts.length > 0 |
| | ? state.translatedResults[lang][index].facts |
| | : (result.must_answer || []), |
| | sensitiveTopics: state.translatedResults[lang][index].sensitiveTopics || [] |
| | }; |
| | } |
| |
|
| | return { |
| | entityName: result.entity_name, |
| | summary: result.summary, |
| | facts: result.must_answer || [], |
| | sensitiveTopics: null |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | function updateLanguage() { |
| | const lang = state.language; |
| |
|
| | |
| | document.body.dir = lang === 'ar' ? 'rtl' : 'ltr'; |
| | document.body.classList.remove('lang-cn'); |
| | if (lang === 'cn') { |
| | document.body.classList.add('lang-cn'); |
| | } |
| |
|
| | |
| | const titleEl = document.querySelector('h1'); |
| | if (titleEl) titleEl.textContent = t('title'); |
| |
|
| | const searchInput = document.getElementById('search-input'); |
| | if (searchInput) searchInput.placeholder = t('searchPlaceholder'); |
| |
|
| | const categoryText = document.getElementById('category-text'); |
| | if (categoryText) { |
| | if (state.currentCategory) { |
| | categoryText.textContent = state.currentCategory.name[lang] || state.currentCategory.name.en; |
| | } else { |
| | categoryText.textContent = t('selectCategory'); |
| | } |
| | } |
| |
|
| | const resultsHeader = document.querySelector('main h2'); |
| | if (resultsHeader) { |
| | resultsHeader.innerHTML = `<span class="text-xl">🦅</span> ${t('results')}`; |
| | } |
| |
|
| | |
| | const feedbackLabel = document.getElementById('feedback-label'); |
| | if (feedbackLabel) { |
| | feedbackLabel.innerHTML = `<span>💬</span> ${t('feedback')}`; |
| | } |
| |
|
| | const feedbackInput = document.getElementById('feedback-input'); |
| | if (feedbackInput) feedbackInput.placeholder = t('feedbackPlaceholder'); |
| |
|
| | const submitBtn = document.getElementById('submit-feedback-btn'); |
| | if (submitBtn) submitBtn.textContent = t('submit'); |
| |
|
| | const helpBtn = document.getElementById('help-btn'); |
| | if (helpBtn) helpBtn.textContent = t('help'); |
| |
|
| | |
| | const firstQueryNote = document.getElementById('first-query-note'); |
| | if (firstQueryNote) firstQueryNote.textContent = t('firstQueryNote'); |
| |
|
| | |
| | updateStatsDisplay(); |
| |
|
| | |
| | const categoryOptions = document.querySelectorAll('.category-option'); |
| | categoryOptions.forEach((option, index) => { |
| | const cat = CONFIG.CATEGORIES[index]; |
| | if (cat) { |
| | option.textContent = `${index + 1}. ${cat.name[lang] || cat.name.en}`; |
| | } |
| | }); |
| |
|
| | |
| | if (DOM.resultsCount) { |
| | const count = state.results.length; |
| | DOM.resultsCount.textContent = t('topEntities').replace('{count}', count); |
| | } |
| |
|
| | |
| | if (state.results.length === 0 && DOM.resultsContainer) { |
| | DOM.resultsContainer.innerHTML = ` |
| | <div class="text-center py-12 text-gray-400"> |
| | <p class="text-lg mb-2">${t('enterQuery')}</p> |
| | <p class="text-sm">${t('selectCategoryHint')}</p> |
| | </div> |
| | `; |
| | } else if (state.results.length > 0) { |
| | |
| | renderResults(); |
| | } |
| |
|
| | |
| | document.querySelectorAll('.lang-toggle').forEach(btn => { |
| | btn.classList.remove('active'); |
| | }); |
| | document.getElementById(`lang-${lang}`)?.classList.add('active'); |
| |
|
| | |
| | updateHelpModal(); |
| | } |
| |
|
| | function updateHelpModal() { |
| | |
| | const helpTitle = document.getElementById('help-modal-title'); |
| | if (helpTitle) helpTitle.textContent = t('helpModalTitle'); |
| |
|
| | |
| | const whatIsTitle = document.getElementById('help-what-is-title'); |
| | if (whatIsTitle) whatIsTitle.textContent = t('helpWhatIsTitle'); |
| |
|
| | const whatIsText = document.getElementById('help-what-is-text'); |
| | if (whatIsText) whatIsText.innerHTML = t('helpWhatIsText'); |
| |
|
| | const noticeTitle = document.getElementById('help-notice-title'); |
| | if (noticeTitle) noticeTitle.textContent = t('helpNoticeTitle'); |
| |
|
| | const noticeText = document.getElementById('help-notice-text'); |
| | if (noticeText) noticeText.innerHTML = t('helpNoticeText'); |
| |
|
| | const dataTitle = document.getElementById('help-data-title'); |
| | if (dataTitle) dataTitle.textContent = t('helpDataTitle'); |
| |
|
| | const dataText = document.getElementById('help-data-text'); |
| | if (dataText) dataText.innerHTML = t('helpDataText'); |
| |
|
| | const irTitle = document.getElementById('help-ir-title'); |
| | if (irTitle) irTitle.textContent = t('helpIRTitle'); |
| |
|
| | const irText = document.getElementById('help-ir-text'); |
| | if (irText) irText.innerHTML = t('helpIRText'); |
| |
|
| | const categoriesTitle = document.getElementById('help-categories-title'); |
| | if (categoriesTitle) categoriesTitle.textContent = t('helpCategoriesTitle'); |
| |
|
| | const versionText = document.getElementById('help-version'); |
| | if (versionText) versionText.innerHTML = t('helpVersion'); |
| |
|
| | |
| | for (let i = 1; i <= 8; i++) { |
| | const catName = document.getElementById(`help-cat-${i}-name`); |
| | const catEx = document.getElementById(`help-cat-${i}-ex`); |
| | if (catName) catName.textContent = `${i}. ${t(`helpCat${i}`)}`; |
| | if (catEx) catEx.textContent = t(`helpCat${i}Ex`); |
| | } |
| | } |
| |
|
| | window.setLanguage = function(lang) { |
| | state.language = lang; |
| | localStorage.setItem('uae_lang', lang); |
| | updateLanguage(); |
| |
|
| | |
| | const langNames = { en: 'English', ar: 'العربية', cn: '中文' }; |
| | showToast(`Language: ${langNames[lang]}`, 'info'); |
| | }; |
| |
|
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | |
| | setTimeout(() => updateLanguage(), 100); |
| | }); |
| |
|
| | |
| | window.goHome = function() { |
| | |
| | state.currentQuery = ''; |
| | state.currentCategory = null; |
| | state.results = []; |
| | state.ratings = {}; |
| | state.translatedResults = {}; |
| | state.showOriginal = false; |
| |
|
| | |
| | if (DOM.searchInput) DOM.searchInput.value = ''; |
| | if (DOM.categoryText) DOM.categoryText.textContent = t('selectCategory'); |
| |
|
| | |
| | switchToHomeMode(); |
| | }; |
| |
|
| | |
| | window.selectCategory = selectCategory; |
| | window.translateResults = translateResults; |