fe / main.js
3v324v23's picture
Merge branch 'develop' of https://huggingface.co/spaces/tokyotechlab/fe
bc32157
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>&#91;${listImage.join(', ')}&#93;</span>`;
}
const listUrl = (insideSquare || insideSource)
.split(',')
.map(
(item) =>
`<span class="${
isReplacedBlock ? 'image-bg' : ''
}" onClick=goToEvidence()>${item}</span>`
);
return `${beforeWord ? beforeWord + ' ' : ''} \n<span>&#91;${listUrl.join(
', '
)}&#93;</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;
}
};