Spaces:
Running
Running
| import * as i18n from './i18n.js'; | |
| const { | |
| t, | |
| initI18n, | |
| bindLanguageSelector, | |
| onLanguageChange, | |
| getCurrentLanguage, | |
| setLanguage, | |
| } = i18n; | |
| const setLanguageGuard = i18n.setLanguageGuard || (() => {}); | |
| import { closeIcon, eyeIcon, trashIcon } from './constants.js'; | |
| import { | |
| ensureAuthenticated, | |
| startAuthWatcher, | |
| clearAuthUser, | |
| } from './auth.js'; | |
| ensureAuthenticated(); | |
| startAuthWatcher(); | |
| const logoutButton = document.getElementById('btn-logout'); | |
| if (logoutButton) { | |
| logoutButton.addEventListener('click', () => { | |
| clearAuthUser(); | |
| window.location.href = 'login.html'; | |
| }); | |
| } | |
| const inputElement = document.getElementById('file-input'); | |
| const textInputElement = document.getElementById('text-input-additional'); | |
| const socialMediaInputElement = document.getElementById('input-social'); | |
| const titleInputElement = document.getElementById('input-title'); | |
| const locationInputElement = document.getElementById('input-location'); | |
| const categoryInputElement = document.getElementById('input-category'); | |
| const violenceLevelInputElement = document.getElementById('input-violence'); | |
| const outputElement = document.getElementById('output-area'); | |
| const btnClear = document.getElementById('btn-clear-face-check'); | |
| const btnSubmit = document.getElementById('btn-submit-face-check'); | |
| const btnSave = document.getElementById('btn-save-face-check'); | |
| const wrapperFilesElement = document.getElementById('wrapper-files-face-check'); | |
| const wrapperUploadElement = document.getElementById( | |
| 'wrapper-upload-face-check' | |
| ); | |
| const wrapperModalElement = document.getElementById( | |
| 'wrapper-modal-1760410479496' | |
| ); | |
| const contentModalElement = document.getElementById('model-1760410479496'); | |
| const dropZones = document.getElementsByClassName('drop-zone'); | |
| const fileRemoves = document.getElementsByClassName('file-remove'); | |
| const statusFaceCheckElement = document.getElementById('status-face-check'); | |
| const tabMenuElement = document.getElementById('menu-1762855257376'); | |
| const tabItems = tabMenuElement.getElementsByClassName('item'); | |
| const livechat = document.getElementById('live-chat-1765253515534'); | |
| export let output; | |
| initI18n(); | |
| bindLanguageSelector('#language-select'); | |
| const languageApiMap = { | |
| en: 'English', | |
| ja: 'Japanese', | |
| vi: 'Vietnamese', | |
| }; | |
| const languageLabelMap = { | |
| en: () => t('languageEnglish') || 'English', | |
| ja: () => t('languageJapanese') || 'Japanese', | |
| vi: () => t('languageVietnamese') || 'Vietnamese', | |
| }; | |
| const languageDropdown = document.getElementById('language-dropdown'); | |
| const languageToggle = document.getElementById('language-toggle'); | |
| const languageOptions = document.querySelectorAll('.language-option'); | |
| const languageSelectedLabel = document.getElementById( | |
| 'language-selected-label' | |
| ); | |
| const hiddenLanguageSelect = document.getElementById('language-select'); | |
| function setLanguageControlsDisabled(disabled) { | |
| if (languageToggle) { | |
| languageToggle.disabled = disabled; | |
| languageToggle.classList.toggle('btn-disabled', disabled); | |
| } | |
| if (hiddenLanguageSelect) { | |
| hiddenLanguageSelect.disabled = disabled; | |
| } | |
| languageOptions.forEach((option) => { | |
| option.classList.toggle('btn-disabled', disabled); | |
| option.setAttribute('aria-disabled', disabled.toString()); | |
| }); | |
| } | |
| function setLanguageUI(lang) { | |
| const labelFn = languageLabelMap[lang]; | |
| if (languageSelectedLabel && labelFn) { | |
| languageSelectedLabel.textContent = labelFn(); | |
| } | |
| languageOptions.forEach((option) => { | |
| const isActive = option.dataset.lang === lang; | |
| option.classList.toggle('active', isActive); | |
| option.setAttribute('aria-selected', isActive.toString()); | |
| }); | |
| if (hiddenLanguageSelect) { | |
| hiddenLanguageSelect.value = lang; | |
| } | |
| if (languageToggle) { | |
| languageToggle.setAttribute('aria-expanded', 'false'); | |
| } | |
| if (languageDropdown) { | |
| languageDropdown.classList.remove('open'); | |
| } | |
| } | |
| function setupLanguageDropdown() { | |
| if (!languageDropdown || !languageToggle) return; | |
| languageToggle.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| const isOpen = languageDropdown.classList.toggle('open'); | |
| languageToggle.setAttribute('aria-expanded', isOpen.toString()); | |
| }); | |
| languageOptions.forEach((option) => { | |
| option.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| const lang = option.dataset.lang; | |
| if (lang) { | |
| setLanguage(lang); | |
| } | |
| }); | |
| }); | |
| document.addEventListener('click', (event) => { | |
| if (!languageDropdown.classList.contains('open')) return; | |
| if (!languageDropdown.contains(event.target)) { | |
| languageDropdown.classList.remove('open'); | |
| languageToggle.setAttribute('aria-expanded', 'false'); | |
| } | |
| }); | |
| setLanguageUI(getCurrentLanguage()); | |
| } | |
| setupLanguageDropdown(); | |
| const formInputs = [ | |
| textInputElement, | |
| socialMediaInputElement, | |
| titleInputElement, | |
| locationInputElement, | |
| categoryInputElement, | |
| violenceLevelInputElement, | |
| ]; | |
| function setDefaultOutputPlaceholder() { | |
| outputElement.innerHTML = ` | |
| <p class="opacity-50" data-i18n="verificationPlaceholder" data-i18n-dynamic="true"> | |
| ${t('verificationPlaceholder')} | |
| </p>`; | |
| } | |
| function updateSubmitButtonLabel() { | |
| if (!btnSubmit) return; | |
| btnSubmit.innerText = isLoading.value | |
| ? t('processingLabel') | |
| : t('submitButton'); | |
| } | |
| onLanguageChange((lang) => { | |
| setLanguageUI(lang); | |
| updateSubmitButtonLabel(); | |
| if (!data.value) { | |
| setDefaultOutputPlaceholder(); | |
| return; | |
| } | |
| if (selectedTab.value === 'verified_evidence') { | |
| const activeChild = selectedTabChildren.value || 'source_details'; | |
| const content = | |
| data.value.readme_content[selectedTab.value]?.[activeChild]?.content || | |
| data.value.readme_content[selectedTab.value]?.content || | |
| ''; | |
| outputElement.innerHTML = | |
| buildVerifiedEvidenceMenu(activeChild) + marked.parse(content); | |
| } | |
| }); | |
| function toggleFormInputs(disabled) { | |
| formInputs.forEach((el) => { | |
| if (!el) return; | |
| el.disabled = disabled; | |
| el.classList.toggle('btn-disabled', disabled); | |
| }); | |
| } | |
| export const clientId = | |
| Date.now().toString(36) + Math.random().toString(36).substring(2, 8); | |
| export const apiBaseUrl = ''; | |
| export let estTimeStep1 = 0; | |
| export let estTimeStep2 = 0; | |
| let currentPreviewURL = null; | |
| const mediaPreviewUrls = new Map(); | |
| const renderer = new marked.Renderer(); | |
| renderer.link = function ({ href, text }) { | |
| return `<a class="image-bg" href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`; | |
| }; | |
| let isReplaced = true; | |
| let isReplacedBlock = true; | |
| renderer.text = function ({ text }) { | |
| const hiddenTexts = []; | |
| const noReplaces = [ | |
| //EN | |
| '**Tags:**', | |
| '**Filename:**', | |
| '**Authenticity Assessment:**', | |
| '**Verification Methods:**', | |
| '**Details:**', | |
| '**Verification Tools & Methods:**', | |
| '**Synthetic Type (if applicable):**', | |
| '**Other Artifacts:**', | |
| '**Supporting Sources:**', | |
| '**Cross-Checking Information:**', | |
| '**Other Info:**', | |
| //JA | |
| '**タグ:**', | |
| '**真正性評価:**', | |
| '**検証方法:**', | |
| '**詳細:**', | |
| '**サポートソース:**', | |
| '**クロスチェック情報:**', | |
| '**その他の情報:**', | |
| //Vi | |
| '**Thẻ:**', | |
| '**Đánh giá tính xác thực:**', | |
| '**Phương pháp xác minh:**', | |
| '**Chi tiết:**', | |
| '**Nguồn hỗ trợ:**', | |
| '**Thông tin đối chiếu chéo:**', | |
| '**Thông tin khác:**', | |
| ]; | |
| isReplaced = !noReplaces.some((item) => text.includes(item)); | |
| if (isReplacedBlock) { | |
| isReplacedBlock = !text.includes('**Supporting Sources:**'); | |
| } | |
| const normalizedText = text.trim().replace(/^[-*]\s*/, ''); | |
| if (hiddenTexts.some((item) => normalizedText === item)) { | |
| return ''; | |
| } | |
| if (!isReplaced) { | |
| const inlineHtml = | |
| typeof marked.parseInline === 'function' | |
| ? marked.parseInline(text) | |
| : marked.Renderer.prototype.text.call(renderer, text); | |
| return inlineHtml; | |
| } | |
| const replaced = text.replace( | |
| /(?:\b([\w-]+)\s*)?(?:\[((?:Image\s+[^\]]+|Source\s+[^\]]+|URL\s+[^\]]+|[^,\]]+(?:\s*,\s*[^,\]]+)*))\]|\((Sources?\s+[^)]+)\))/gi, | |
| (match, beforeWord, insideSquare, insideSource) => { | |
| const beforeWordLower = (beforeWord || '').toLowerCase(); | |
| const insideLower = (insideSquare || '').toLowerCase(); | |
| const isImages = ['image', 'images', 'photo', 'photos']; | |
| if ( | |
| isImages.includes(beforeWordLower) || | |
| isImages.some((item) => insideLower.includes(item)) | |
| ) { | |
| const listImage = insideSquare | |
| .split(',') | |
| .map( | |
| (item) => | |
| `<span class="${ | |
| isReplacedBlock ? 'image-bg' : '' | |
| }" onClick="previewImage('${item}')">${item}</span>` | |
| ); | |
| return `${ | |
| beforeWord ? beforeWord + ' ' : '' | |
| } <span>[${listImage.join(', ')}]</span>`; | |
| } | |
| const listUrl = (insideSquare || insideSource) | |
| .split(',') | |
| .map( | |
| (item) => | |
| `<span class="${ | |
| isReplacedBlock ? 'image-bg' : '' | |
| }" onClick=goToEvidence()>${item}</span>` | |
| ); | |
| return `${beforeWord ? beforeWord + ' ' : ''} \n<span>[${listUrl.join( | |
| ', ' | |
| )}]</span>`; | |
| } | |
| ); | |
| return replaced; | |
| }; | |
| marked.setOptions({ | |
| renderer, | |
| breaks: true, | |
| }); | |
| let files = {}; | |
| Object.defineProperty(files, 'value', { | |
| set(newValue) { | |
| const activeIds = new Set( | |
| newValue.map((file) => `${file.name}|${file.size}|${file.lastModified}`) | |
| ); | |
| mediaPreviewUrls.forEach((url, id) => { | |
| if (!activeIds.has(id)) { | |
| URL.revokeObjectURL(url); | |
| mediaPreviewUrls.delete(id); | |
| } | |
| }); | |
| if (newValue.length > 0) { | |
| const displayNames = newValue | |
| .map((file) => { | |
| const fileId = `${file.name}|${file.size}|${file.lastModified}`; | |
| if (!mediaPreviewUrls.has(fileId)) { | |
| mediaPreviewUrls.set(fileId, URL.createObjectURL(file)); | |
| } | |
| const previewUrl = mediaPreviewUrls.get(fileId); | |
| const previewMarkup = file.type.startsWith('video/') | |
| ? `<div class="file-thumbnail file-thumbnail-video"> | |
| <video src="${previewUrl}" preload="metadata" muted playsinline></video> | |
| </div>` | |
| : `<img class="file-thumbnail file-thumbnail-image" src="${previewUrl}" alt="${file.name}" />`; | |
| return `<div onclick="previewFile(event, '${fileId}')" id="${fileId}" class="file-item pointer"> | |
| ${previewMarkup} | |
| <span class="file-name" data-full-name="${file.name}">${file.name}</span> | |
| <div class="preview">${eyeIcon}</div> | |
| <div onclick="removeFile(event, '${fileId}', 'media')" class="file-remove" data-file="${fileId}">${trashIcon}</div> | |
| </div>`; | |
| }) | |
| .join(''); | |
| wrapperFilesElement.innerHTML = displayNames; | |
| wrapperFilesElement.classList.remove('display-none'); | |
| // Add tooltips only for truncated text | |
| setTimeout(() => { | |
| document.querySelectorAll('.file-name').forEach((span) => { | |
| const fullName = span.getAttribute('data-full-name'); | |
| if (fullName && span.scrollWidth > span.clientWidth) { | |
| span.setAttribute('title', fullName); | |
| } | |
| }); | |
| }, 0); | |
| } else { | |
| wrapperFilesElement.innerHTML = ''; | |
| wrapperFilesElement.classList.add('display-none'); | |
| } | |
| this._value = newValue; | |
| }, | |
| get() { | |
| return this._value; | |
| }, | |
| }); | |
| files.value = []; | |
| let data = {}; | |
| Object.defineProperty(data, 'value', { | |
| set(newValue) { | |
| if (newValue) { | |
| btnSave.classList.add('active'); | |
| console.log(newValue); | |
| output = newValue; | |
| // livechat.classList.remove('display-none'); | |
| } else { | |
| btnSave.classList.remove('active'); | |
| output = ''; | |
| // livechat.classList.add('display-none'); | |
| } | |
| this._value = newValue; | |
| }, | |
| get() { | |
| return this._value; | |
| }, | |
| }); | |
| // data.value = {} | |
| function buildVerifiedEvidenceMenu(activeChild = 'source_details') { | |
| const labels = { | |
| source_details: t('sourceDetailsTab'), | |
| where: t('whereTab'), | |
| when: t('whenTab'), | |
| who: t('whoTab'), | |
| why: t('whyTab'), | |
| }; | |
| const childTabs = ['source_details', 'where', 'when', 'who', 'why']; | |
| const items = childTabs | |
| .map( | |
| (key) => | |
| `<div onclick="changeTabChildren('${key}', this)" class="item ${ | |
| activeChild === key ? 'active' : '' | |
| }">${labels[key]}</div>` | |
| ) | |
| .join(''); | |
| return `<div class="menu-children menu pt-0 mb-4">${items}</div>`; | |
| } | |
| let selectedTab = {}; | |
| Object.defineProperty(selectedTab, 'value', { | |
| set(newValue) { | |
| if (data.value) { | |
| const menuItems = | |
| newValue === 'verified_evidence' | |
| ? buildVerifiedEvidenceMenu( | |
| selectedTabChildren.value || 'source_details' | |
| ) | |
| : ''; | |
| outputElement.innerHTML = | |
| menuItems + | |
| marked.parse( | |
| data.value.readme_content[newValue]?.content || | |
| data.value.readme_content[newValue]?.source_details?.content || | |
| '' | |
| ); | |
| } | |
| isReplacedBlock = true; | |
| this._value = newValue; | |
| }, | |
| get() { | |
| return this._value; | |
| }, | |
| }); | |
| selectedTab.value = 'case_summary'; | |
| let selectedTabChildren = {}; | |
| Object.defineProperty(selectedTabChildren, 'value', { | |
| set(newValue) { | |
| if (data.value) { | |
| const menuItems = buildVerifiedEvidenceMenu(newValue); | |
| outputElement.innerHTML = | |
| menuItems + | |
| marked.parse( | |
| data.value.readme_content[selectedTab.value]?.[newValue]?.content || | |
| '' | |
| ); | |
| } | |
| isReplacedBlock = true; | |
| this._value = newValue; | |
| }, | |
| get() { | |
| return this._value; | |
| }, | |
| }); | |
| selectedTabChildren.value = 'source_details'; | |
| let isLoading = {}; | |
| Object.defineProperty(isLoading, 'value', { | |
| set(newValue) { | |
| if (newValue) { | |
| btnClear.disabled = true; | |
| btnSave.disabled = true; | |
| btnSubmit.disabled = true; | |
| inputElement.disabled = true; | |
| toggleFormInputs(true); | |
| Array.from(dropZones).forEach((dropZone) => { | |
| dropZone.disabled = true; | |
| dropZone.classList.add('btn-disabled'); | |
| }); | |
| Array.from(fileRemoves).forEach((fileRemove) => { | |
| fileRemove.disabled = true; | |
| fileRemove.classList.add('btn-disabled'); | |
| }); | |
| btnClear.classList.add('btn-disabled'); | |
| btnSave.classList.add('btn-disabled'); | |
| btnSubmit.classList.add('btn-disabled'); | |
| Array.from(tabItems).forEach((tabItem) => { | |
| tabItem.disabled = true; | |
| tabItem.classList.add('btn-disabled', 'no-hover'); | |
| }); | |
| setLanguageControlsDisabled(true); | |
| } else { | |
| btnClear.disabled = false; | |
| btnSave.disabled = false; | |
| btnSubmit.disabled = false; | |
| inputElement.disabled = false; | |
| toggleFormInputs(false); | |
| Array.from(dropZones).forEach((dropZone) => { | |
| dropZone.disabled = false; | |
| dropZone.classList.remove('btn-disabled'); | |
| }); | |
| Array.from(fileRemoves).forEach((fileRemove) => { | |
| fileRemove.disabled = false; | |
| fileRemove.classList.remove('btn-disabled'); | |
| }); | |
| btnClear.classList.remove('btn-disabled'); | |
| btnSave.classList.remove('btn-disabled'); | |
| btnSubmit.classList.remove('btn-disabled'); | |
| Array.from(tabItems).forEach((tabItem) => { | |
| tabItem.disabled = false; | |
| tabItem.classList.remove('btn-disabled', 'no-hover'); | |
| }); | |
| setLanguageControlsDisabled(false); | |
| } | |
| this._value = newValue; | |
| updateSubmitButtonLabel(); | |
| }, | |
| get() { | |
| return this._value; | |
| }, | |
| }); | |
| updateSubmitButtonLabel(); | |
| setDefaultOutputPlaceholder(); | |
| inputElement.addEventListener('change', (event) => { | |
| const allowedExtensions = [ | |
| '.png', | |
| '.jpg', | |
| '.jpeg', | |
| '.gif', | |
| '.webp', | |
| '.svg', | |
| '.mov', | |
| '.mp4', | |
| ]; | |
| const selectedFiles = Array.from(event.target.files); | |
| const invalidFiles = selectedFiles.filter((file) => { | |
| return !allowedExtensions.some((ext) => | |
| file.name.toLowerCase().endsWith(ext) | |
| ); | |
| }); | |
| if (invalidFiles.length > 0) { | |
| createTemplateModal( | |
| `${t('allowedFormats')}${allowedExtensions.join(', ')}` | |
| ); | |
| inputElement.value = ''; | |
| return; | |
| } | |
| for (const file of selectedFiles) { | |
| if ( | |
| !files.value.some((f) => f.name === file.name && f.size === file.size) | |
| ) { | |
| files.value = [...files.value, file]; | |
| } | |
| } | |
| inputElement.value = ''; | |
| }); | |
| // Drag and drop for media files | |
| const mediaDropZone = document.querySelector('label[for="file-input"]'); | |
| mediaDropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| mediaDropZone.classList.add('drag-over'); | |
| }); | |
| mediaDropZone.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| mediaDropZone.classList.remove('drag-over'); | |
| }); | |
| mediaDropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| mediaDropZone.classList.remove('drag-over'); | |
| const droppedFiles = Array.from(e.dataTransfer.files); | |
| const allowedExtensions = [ | |
| '.png', | |
| '.jpg', | |
| '.jpeg', | |
| '.gif', | |
| '.webp', | |
| '.svg', | |
| '.mov', | |
| '.mp4', | |
| ]; | |
| const invalidFiles = droppedFiles.filter((file) => { | |
| return !allowedExtensions.some((ext) => | |
| file.name.toLowerCase().endsWith(ext) | |
| ); | |
| }); | |
| if (invalidFiles.length > 0) { | |
| createTemplateModal( | |
| `${t('allowedFormats')}${allowedExtensions.join(', ')}` | |
| ); | |
| return; | |
| } | |
| for (const file of droppedFiles) { | |
| if ( | |
| !files.value.some((f) => f.name === file.name && f.size === file.size) | |
| ) { | |
| files.value = [...files.value, file]; | |
| } | |
| } | |
| }); | |
| btnSubmit.addEventListener('click', () => handleSubmit()); | |
| btnClear.addEventListener('click', () => reset()); | |
| btnSave.addEventListener('click', () => handleDownload('report.md')); | |
| function reset() { | |
| inputElement.value = ''; | |
| if (textInputElement) textInputElement.value = ''; | |
| if (socialMediaInputElement) socialMediaInputElement.value = ''; | |
| if (titleInputElement) titleInputElement.value = ''; | |
| if (locationInputElement) locationInputElement.value = ''; | |
| if (categoryInputElement) categoryInputElement.value = ''; | |
| if (violenceLevelInputElement) violenceLevelInputElement.value = ''; | |
| setDefaultOutputPlaceholder(); | |
| files.value = []; | |
| data.value = null; | |
| } | |
| function hasExistingData() { | |
| return !!data.value; | |
| } | |
| function showLanguageChangeConfirm(nextLang) { | |
| const content = ` | |
| <p class="modal-body-text">${t('languageChangeConfirmMessage')}</p> | |
| <div class="modal-actions"> | |
| <button class="btn btn-clear w-fit" onclick="closeModal()">${t('languageChangeCancel')}</button> | |
| <button class="btn btn-submit w-fit" onclick="confirmLanguageChange('${nextLang}')">${t('languageChangeConfirm')}</button> | |
| </div> | |
| `; | |
| createTemplateModal(content, t('languageChangeConfirmTitle')); | |
| } | |
| window.confirmLanguageChange = function (lang) { | |
| reset(); | |
| setLanguage(lang, { force: true }); | |
| closeModal(); | |
| }; | |
| setLanguageGuard((nextLang, currentLang) => { | |
| if (nextLang === currentLang) return true; | |
| if (!hasExistingData()) return true; | |
| setLanguageUI(currentLang); | |
| showLanguageChangeConfirm(nextLang); | |
| return false; | |
| }); | |
| const pollIntervalMs = 5000; | |
| const maxPolls = 1000; | |
| async function runFaceCheckPolling(formData, clientId) { | |
| try { | |
| const start = await axios.post( | |
| apiBaseUrl + 'v1/face_check/polling', | |
| formData, | |
| { | |
| headers: { | |
| 'Content-Type': 'multipart/form-data', | |
| 'X-Client-ID': clientId, | |
| }, | |
| } | |
| ); | |
| const { job_id, status } = start.data; | |
| if (!job_id) return { error: 'Missing job_id from polling start' }; | |
| if (status === 'failed') return { error: 'Job failed immediately' }; | |
| let attempts = 0; | |
| while (attempts < maxPolls) { | |
| await new Promise((r) => setTimeout(r, pollIntervalMs)); | |
| attempts += 1; | |
| const res = await axios.get( | |
| apiBaseUrl + `v1/face_check/polling/${job_id}` | |
| ); | |
| if (res.data.status === 'succeeded') return { result: res.data.result }; | |
| if (res.data.status === 'failed') { | |
| return { error: res.data.error || 'Job failed' }; | |
| } | |
| } | |
| return { error: 'Polling timeout' }; | |
| } catch (error) { | |
| console.error('Face check polling failed', error); | |
| statusFaceCheckElement.classList.add('display-none'); | |
| return { error: error?.message || 'Job failed' }; | |
| } | |
| } | |
| function createDefaultMetadataFile() { | |
| const randomSuffix = Math.random().toString(36).slice(2, 10); | |
| const fileName = `metadata-${Date.now()}-${randomSuffix}.json`; | |
| const meta = { | |
| title: (titleInputElement?.value || '').trim(), | |
| location: (locationInputElement?.value || '').trim(), | |
| category: (categoryInputElement?.value || '').trim(), | |
| 'violence level': (violenceLevelInputElement?.value || '').trim(), | |
| description: (textInputElement?.value || '').trim(), | |
| 'social media link': (socialMediaInputElement?.value || '').trim(), | |
| }; | |
| const content = JSON.stringify(meta, null, 2); | |
| return new File([content], fileName, { type: 'application/json' }); | |
| } | |
| function sanitizeTextareaValue(raw) { | |
| return (raw || '').replace(/\r?\n/g, ' ').trim(); | |
| } | |
| async function handleSubmit() { | |
| if (isLoading.value) return; | |
| data.value = null; | |
| setDefaultOutputPlaceholder(); | |
| if (files.value.length === 0) { | |
| createTemplateModal(t('uploadRequirement')); | |
| return; | |
| } | |
| isLoading.value = true; | |
| statusFaceCheckElement.classList.remove('display-none'); | |
| const metadataFile = createDefaultMetadataFile(); | |
| const formData = new FormData(); | |
| formData.append('metadata_file', metadataFile); | |
| formData.append( | |
| 'additional_text', | |
| sanitizeTextareaValue(textInputElement?.value) | |
| ); | |
| const selectedLanguage = getCurrentLanguage(); | |
| formData.append('language', languageApiMap[selectedLanguage] || 'English'); | |
| const mediainfo = await new Promise((resolve) => { | |
| MediaInfo.mediaInfoFactory({ format: 'JSON' }, resolve); | |
| }); | |
| let isGetTimed = false; | |
| for (const [index, file] of files.value.entries()) { | |
| const readChunk = async (chunkSize, offset) => | |
| new Uint8Array( | |
| await file.slice(offset, offset + chunkSize).arrayBuffer() | |
| ); | |
| try { | |
| if (file.type.startsWith('video/') && !isGetTimed) { | |
| const result = await mediainfo.analyzeData(file.size, readChunk); | |
| const data = JSON.parse(result); | |
| const video = | |
| data.media.track.find((t) => t['@type'] === 'Video') || {}; | |
| const duration = +video.Duration || 0; | |
| const frameRate = +video.FrameRate || 0; | |
| const heightV = +video.Height || 0; | |
| const widthV = +video.Width || 0; | |
| const calcEstimatedTimeCode = Math.ceil( | |
| (duration * frameRate * heightV * widthV * 3.17e-7) / 60 | |
| ); | |
| estTimeStep1 = calcEstimatedTimeCode; | |
| isGetTimed = true; | |
| } | |
| formData.append('files', file); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| } | |
| } | |
| estTimeStep1 = estTimeStep1 + 5; | |
| estTimeStep2 = 5; | |
| try { | |
| const { result, error } = await runFaceCheckPolling(formData, clientId); | |
| if (error || !result) { | |
| outputElement.innerText = t('processingError'); | |
| isLoading.value = false; | |
| return; | |
| } | |
| data.value = result; | |
| const menuItems = | |
| selectedTab.value === 'verified_evidence' | |
| ? buildVerifiedEvidenceMenu( | |
| selectedTabChildren.value || 'source_details' | |
| ) | |
| : ''; | |
| outputElement.innerHTML = | |
| menuItems + | |
| marked.parse( | |
| result.readme_content[selectedTab.value]?.content || | |
| result.readme_content[selectedTab.value]?.source_details?.content || | |
| '' | |
| ); | |
| estTimeStep1 = 0; | |
| estTimeStep2 = 0; | |
| isLoading.value = false; | |
| } catch (error) { | |
| outputElement.innerText = t('processingError'); | |
| isLoading.value = false; | |
| } | |
| } | |
| function handleHyperLinkImages(text) { | |
| return text.replace(/[\[\(]Image\s+([\d,\s]+)[\]\)]/g, (match, numbers) => { | |
| const list = numbers | |
| .split(',') | |
| .map((n) => n.trim()) | |
| .filter((n) => n !== ''); | |
| const result = list.map((n) => { | |
| return `[![Image ${n}]](./media/image-${n}.png)`; | |
| }); | |
| return result.join(''); | |
| }); | |
| } | |
| async function handleDownload(filename) { | |
| if (!data.value) { | |
| createTemplateModal(t('noDataToSave')); | |
| return; | |
| } | |
| const dataReport = | |
| handleHyperLinkImages(`${data.value.readme_content.case_summary.title}${data.value.readme_content.case_summary.content}${data.value.readme_content.content_classification.title}${data.value.readme_content.content_classification.content}${data.value.readme_content.verified_evidence.title} | |
| ${data.value.readme_content.verified_evidence.source_details.title}${data.value.readme_content.verified_evidence.source_details.content}${data.value.readme_content.verified_evidence.where.title}${data.value.readme_content.verified_evidence.where.content}${data.value.readme_content.verified_evidence.when.title}${data.value.readme_content.verified_evidence.when.content}${data.value.readme_content.verified_evidence.who.title}${data.value.readme_content.verified_evidence.who.content}${data.value.readme_content.verified_evidence.why.title}${data.value.readme_content.verified_evidence.why.content}${data.value.readme_content.forensic_analysis.title}${data.value.readme_content.forensic_analysis.content}${data.value.readme_content.other_evidence.title}${data.value.readme_content.other_evidence.content} | |
| `); | |
| const mediaItems = Array.isArray(data.value.readme_content.media) | |
| ? data.value.readme_content.media | |
| : []; | |
| if (mediaItems.length === 0) { | |
| const blob = new Blob([dataReport], { | |
| type: 'text/markdown', | |
| }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| return; | |
| } | |
| if (typeof JSZip === 'undefined') { | |
| createTemplateModal(t('downloadPrepareFailedNetwork')); | |
| return; | |
| } | |
| const zip = new JSZip(); | |
| zip.file(filename, dataReport); | |
| // Package each available media asset alongside the markdown report | |
| mediaItems.forEach((raw, index) => { | |
| if (typeof raw !== 'string' || !raw.trim()) { | |
| return; | |
| } | |
| const base64Data = raw.replace(/\s+/g, ''); | |
| zip.file(`media/image-${index + 1}.png`, base64Data, { base64: true }); | |
| }); | |
| try { | |
| const archiveBlob = await zip.generateAsync({ type: 'blob' }); | |
| const baseName = filename ? filename.replace(/\.[^/.]+$/, '') : 'report'; | |
| const archiveName = `${baseName || 'report'}-assets.zip`; | |
| const url = URL.createObjectURL(archiveBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = archiveName; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } catch (error) { | |
| console.error('Failed to generate download archive', error); | |
| createTemplateModal(t('downloadPrepareFailed')); | |
| } | |
| } | |
| window.goToEvidence = function () { | |
| if (selectedTab.value === 'other_evidence') return; | |
| document.querySelectorAll('.menu .item').forEach((item) => { | |
| item.classList.remove('active'); | |
| }); | |
| document | |
| .getElementById('other_evidence-1763023210526') | |
| .classList.add('active'); | |
| selectedTab.value = 'other_evidence'; | |
| }; | |
| window.previewImage = function (imageRaw) { | |
| const match = imageRaw.match(/\d+/); | |
| if (!match) return; | |
| const index = Number(match[0]) - 1; | |
| const image = data.value.readme_content.media[index]; | |
| if (!image) return; | |
| createTemplateModal( | |
| `<img src="data:image/jpeg;base64,${image}" alt="Image Preview" style="max-width: 100%; height: auto;" />`, | |
| `${t('imageLabel')} ${index + 1}` | |
| ); | |
| }; | |
| window.changeTabChildren = function (tab, element) { | |
| if (isLoading.value) return; | |
| document.querySelectorAll('.menu-children .item').forEach((item) => { | |
| item.classList.remove('active'); | |
| }); | |
| element.classList.add('active'); | |
| selectedTabChildren.value = tab; | |
| }; | |
| window.changeTab = function (tab, element) { | |
| if (isLoading.value) return; | |
| document.querySelectorAll('.menu .item').forEach((item) => { | |
| item.classList.remove('active'); | |
| }); | |
| element.classList.add('active'); | |
| selectedTab.value = tab; | |
| }; | |
| window.removeFile = function (event, fileId, fileType) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| if (isLoading.value) return; | |
| const [fileName, fileSize, fileLastModified] = fileId.split('|'); | |
| files.value = files.value.filter( | |
| (file) => | |
| !( | |
| file.name === fileName && | |
| file.size.toString() === fileSize && | |
| file.lastModified.toString() === fileLastModified | |
| ) | |
| ); | |
| }; | |
| function createTemplateModal(content, title = t('notificationTitle')) { | |
| const modal = `<div class="header-modal"> | |
| <span class="title">${title}</span> | |
| <div onclick="closeModal()" class="close">${closeIcon}</div> | |
| </div> | |
| <div class="content-modal scroll-box"> | |
| ${content} | |
| </div>`; | |
| openModal(modal); | |
| } | |
| window.previewFile = function (event, fileId) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const [fileName, fileSize, fileLastModified] = fileId.split('|'); | |
| const file = files.value.find( | |
| (f) => | |
| f.name === fileName && | |
| f.size.toString() === fileSize && | |
| f.lastModified.toString() === fileLastModified | |
| ); | |
| if (!file) return; | |
| const fileURL = URL.createObjectURL(file); | |
| currentPreviewURL = fileURL; | |
| let content = ''; | |
| if (file.type === 'video/quicktime') { | |
| content = t('movNotSupported'); | |
| } else if (file.type.startsWith('image/')) { | |
| content = `<img src="${fileURL}" alt="${file.name}" style="max-width: 100%; height: auto;" />`; | |
| } else if (file.type.startsWith('video/')) { | |
| content = `<video controls style="max-width: 100%; height: auto;"> | |
| <source src="${fileURL}" type="${file.type}"> | |
| ${t('videoTagNotSupported')} | |
| </video>`; | |
| } else { | |
| content = t('previewUnavailable'); | |
| } | |
| createTemplateModal(content, file.name); | |
| }; | |
| function openModal(content) { | |
| contentModalElement.innerHTML = content; | |
| wrapperModalElement.style.display = 'flex'; | |
| } | |
| window.closeModal = function () { | |
| wrapperModalElement.style.display = 'none'; | |
| const video = contentModalElement.querySelector('video'); | |
| if (video) { | |
| video.pause(); | |
| video.currentTime = 0; | |
| } | |
| if (currentPreviewURL) { | |
| URL.revokeObjectURL(currentPreviewURL); | |
| currentPreviewURL = null; | |
| } | |
| }; | |