(() => { "use strict"; const DATASETS = { japan: { name: "Japan Math Philosophy Prompts", url: "https://huggingface.co/datasets/guicybercode/japan-math-philosophy-prompts/resolve/0b472efffdcd5b0b6a166cab21ca318108711629/data/train.jsonl", source: "https://huggingface.co/datasets/guicybercode/japan-math-philosophy-prompts", languages: ["pt-BR", "en", "ja"], }, iceland: { name: "Iceland Tech & Christian Ethics Prompts", url: "https://huggingface.co/datasets/guicybercode/iceland-tech-christian-ethics-prompts/resolve/99a46bd5df9d6af3d7226e4836cb28d4d754f3b7/data/train.jsonl", source: "https://huggingface.co/datasets/guicybercode/iceland-tech-christian-ethics-prompts", languages: ["pt-BR", "en"], }, }; const MAX_DATASET_BYTES = 1_000_000; const MAX_RECORDS = 500; const REQUEST_TIMEOUT_MS = 15_000; const LANGUAGE_NAMES = { "pt-BR": "Português (Brasil)", en: "English", ja: "日本語", }; const COPY = { "pt-BR": { pageTitle: "Culture Compass — exploração editorial multilíngue", metaDescription: "Explore atividades multilíngues experimentais sobre matemática, filosofia, tecnologia e ética.", skipLink: "Ir para o conteúdo principal", interfaceLanguage: "Idioma da interface", eyebrow: "Laboratório editorial multilíngue", heroTitle: "Ideias para matemática, tecnologia e ética", heroIntro: "Explore atividades para reflexão com filtros claros de idioma e tema.", draftTitle: "Aviso de uso", draftText: "Rascunhos experimentais gerados por IA. Revisão humana é necessária antes de uso educacional, em modelos ou como material curado.", explorerTitle: "Escolha uma direção", explorerIntro: "A interface e o conteúdo têm seletores independentes.", controlsHelp: "Alterar um filtro atualiza a atividade exibida.", datasetLabel: "Dataset", contentLanguageLabel: "Idioma do conteúdo", topicLabel: "Tema ou tópico", allTopics: "Todos os temas", randomButton: "Sortear atividade", activityHeading: "Atividade selecionada", resultIntro: "O texto abaixo vem diretamente do dataset-fonte.", loadingTitle: "Carregando atividades", loadingMessage: "Buscando o arquivo público no Hugging Face.", emptyTitle: "Nenhuma atividade encontrada", emptyMessage: "Escolha outro idioma ou tópico para continuar.", errorTitle: "Não foi possível carregar", errorMessage: "Confira sua conexão e tente buscar o dataset novamente.", retryButton: "Tentar novamente", aiDraftBadge: "Rascunho gerado por IA", reviewBadge: "Revisão humana necessária", statusListLabel: "Status editorial", sourceDisclosure: "Confira contexto, limitações e licença no repositório original.", sourceLink: "Abrir dataset-fonte no Hugging Face", footerNote: "Interface estática, sem rastreamento próprio.", footerNavLabel: "Datasets-fonte", prompt: "Proposta de atividade", objectives: "Objetivos de aprendizagem", answer: "Resposta matemática curta", reflection: "Critérios para reflexão", facilitation: "Nota de facilitação", culturalBoundary: "Limite cultural", setting: "Cenário ficcional", question: "Pergunta para discussão", perspectives: "Perspectivas para debate", perspective: "Perspectiva {id}", followups: "Perguntas de aprofundamento", concepts: "Conceitos éticos sugeridos", pluralism: "Compromisso com o pluralismo", fictionality: "Aviso de ficcionalidade", recordPosition: "{current} de {total}", loaded: "{count} atividades disponíveis.", selected: "Atividade pronta para leitura.", mathFacet: "Matemática", philosophyFacet: "Filosofia", technologyFacet: "Tecnologia", tagFacet: "Tópico", untitled: "Atividade sem título", }, en: { pageTitle: "Culture Compass — multilingual editorial explorer", metaDescription: "Explore experimental multilingual activities about mathematics, philosophy, technology, and ethics.", skipLink: "Skip to main content", interfaceLanguage: "Interface language", eyebrow: "Multilingual editorial laboratory", heroTitle: "Ideas for mathematics, technology, and ethics", heroIntro: "Explore reflective activities with clear language and topic filters.", draftTitle: "Use notice", draftText: "Experimental AI-generated drafts. Human review is required before educational use, model use, or treatment as curated material.", explorerTitle: "Choose a direction", explorerIntro: "Interface and content languages have independent selectors.", controlsHelp: "Changing a filter updates the displayed activity.", datasetLabel: "Dataset", contentLanguageLabel: "Content language", topicLabel: "Theme or topic", allTopics: "All topics", randomButton: "Choose a random activity", activityHeading: "Selected activity", resultIntro: "The text below comes directly from the source dataset.", loadingTitle: "Loading activities", loadingMessage: "Fetching the public file from Hugging Face.", emptyTitle: "No activities found", emptyMessage: "Choose another language or topic to continue.", errorTitle: "Unable to load", errorMessage: "Check your connection and try fetching the dataset again.", retryButton: "Try again", aiDraftBadge: "AI-generated draft", reviewBadge: "Human review required", statusListLabel: "Editorial status", sourceDisclosure: "Read the context, limitations, and license in the original repository.", sourceLink: "Open source dataset on Hugging Face", footerNote: "Static interface, no app-level tracking.", footerNavLabel: "Source datasets", prompt: "Activity prompt", objectives: "Learning objectives", answer: "Short mathematical answer", reflection: "Reflection criteria", facilitation: "Facilitation note", culturalBoundary: "Cultural boundary", setting: "Fictional setting", question: "Discussion question", perspectives: "Perspectives for debate", perspective: "Perspective {id}", followups: "Follow-up questions", concepts: "Suggested ethical concepts", pluralism: "Pluralism commitment", fictionality: "Fictionality notice", recordPosition: "{current} of {total}", loaded: "{count} activities available.", selected: "Activity ready to read.", mathFacet: "Mathematics", philosophyFacet: "Philosophy", technologyFacet: "Technology", tagFacet: "Topic", untitled: "Untitled activity", }, "zh-CN": { pageTitle: "Culture Compass — 多语言编辑探索工具", metaDescription: "探索关于数学、哲学、技术与伦理的多语言实验活动。", skipLink: "跳到主要内容", interfaceLanguage: "界面语言", eyebrow: "多语言编辑实验室", heroTitle: "数学、技术与伦理的思考素材", heroIntro: "通过清晰的语言和主题筛选,探索反思活动。", draftTitle: "使用提示", draftText: "这些是由人工智能生成的实验性草稿。用于教学、模型或作为精选材料之前,必须经过人工审核。", explorerTitle: "选择探索方向", explorerIntro: "界面语言与内容语言可独立选择。", controlsHelp: "更改筛选条件会更新显示的活动。", datasetLabel: "数据集", contentLanguageLabel: "内容语言", topicLabel: "主题或话题", allTopics: "所有主题", randomButton: "随机选择活动", activityHeading: "已选活动", resultIntro: "下方文本直接来自源数据集。", loadingTitle: "正在加载活动", loadingMessage: "正在从 Hugging Face 获取公开文件。", emptyTitle: "未找到活动", emptyMessage: "请选择其他语言或主题后继续。", errorTitle: "无法加载", errorMessage: "请检查网络连接,然后重试获取数据集。", retryButton: "重试", aiDraftBadge: "AI 生成的草稿", reviewBadge: "需要人工审核", statusListLabel: "编辑状态", sourceDisclosure: "请在原始仓库中查看背景、限制与许可证。", sourceLink: "在 Hugging Face 打开源数据集", footerNote: "静态界面,不添加应用级跟踪。", footerNavLabel: "源数据集", prompt: "活动任务", objectives: "学习目标", answer: "数学简答", reflection: "反思标准", facilitation: "引导说明", culturalBoundary: "文化边界说明", setting: "虚构场景", question: "讨论问题", perspectives: "讨论视角", perspective: "视角 {id}", followups: "延伸问题", concepts: "建议的伦理概念", pluralism: "多元立场承诺", fictionality: "虚构性说明", recordPosition: "第 {current} 项,共 {total} 项", loaded: "共有 {count} 项活动。", selected: "活动已可阅读。", mathFacet: "数学", philosophyFacet: "哲学", technologyFacet: "技术", tagFacet: "话题", untitled: "无标题活动", }, }; const state = { locale: "pt-BR", dataset: "japan", records: [], visible: [], selectedId: null, cache: new Map(), controller: null, view: "loading", }; let elements; function text(key, values = {}) { const template = COPY[state.locale][key] || COPY.en[key] || key; return template.replace(/\{(\w+)\}/g, (_, name) => String(values[name] ?? "")); } function parseJsonl(source) { const records = []; source.split(/\r?\n/).forEach((rawLine, index) => { const line = rawLine.trim(); if (!line) return; try { const record = JSON.parse(line); if (!record || Array.isArray(record) || typeof record !== "object") { throw new Error("record must be an object"); } records.push(record); } catch (error) { throw new Error(`Invalid JSONL record at line ${index + 1}`, { cause: error }); } }); return records; } async function readLimitedText(response, maxBytes = MAX_DATASET_BYTES) { const reader = response.body?.getReader?.(); if (!reader) { const source = await response.text(); if (new TextEncoder().encode(source).byteLength > maxBytes) { throw new Error("Dataset response exceeds size limit"); } return source; } const decoder = new TextDecoder(); let receivedBytes = 0; let source = ""; try { while (true) { const { done, value } = await reader.read(); if (done) break; receivedBytes += value.byteLength; if (receivedBytes > maxBytes) { await reader.cancel("Dataset response exceeds size limit"); throw new Error("Dataset response exceeds size limit"); } source += decoder.decode(value, { stream: true }); } return source + decoder.decode(); } catch (error) { try { await reader.cancel(); } catch { // The stream may already be closed, cancelled, or aborted. } throw error; } finally { reader.releaseLock?.(); } } function groupId(record) { return record.localization_group_id || record.pair_id || null; } function validateRecords(records) { const ids = new Set(); records.forEach((record) => { if (typeof record.id !== "string" || typeof record.language !== "string" || typeof groupId(record) !== "string") { throw new Error("Dataset record is missing a stable identity or language"); } if (ids.has(record.id)) throw new Error("Dataset contains a duplicate record id"); ids.add(record.id); }); return records; } function arrayOfText(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string") : []; } function slugLabel(value) { return String(value).replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); } function topicFacets(records, datasetKey = state.dataset) { const facets = new Map(); const add = (kind, value) => { if (typeof value !== "string" || !value.trim()) return; facets.set(`${kind}:${value}`, { kind, value }); }; records.forEach((record) => { if (datasetKey === "japan") { add("math", record.math_topic); add("philosophy", record.philosophical_focus); } else { add("technology", record.technology_domain); arrayOfText(record.topic_tags).forEach((tag) => add("tag", tag)); } }); return [...facets.values()].sort((a, b) => a.value.localeCompare(b.value)); } function normalizeJapanRecord(record) { return { id: record.id, groupId: groupId(record), language: record.language, kind: "japan", title: record.title, facets: topicFacets([record], "japan"), raw: record, }; } function normalizeIcelandRecord(record) { return { id: record.id, groupId: groupId(record), language: record.language, kind: "iceland", title: record.scenario_title, facets: topicFacets([record], "iceland"), raw: record, }; } function buildActivities(records, datasetKey) { const normalize = datasetKey === "iceland" ? normalizeIcelandRecord : normalizeJapanRecord; return records.map(normalize); } function filterActivities(records, language, topic = "all") { return records.filter((record) => record.language === language && matchesTopic(record, topic)); } function selectRandom(records, currentId = null, random = Math.random) { if (!records.length) return null; const pool = records.length > 1 ? records.filter((record) => record.id !== currentId) : records; return pool[Math.floor(random() * pool.length)] || pool[0] || null; } function getAvailableLanguages(records, datasetKey) { const present = new Set(records.map((record) => record.language)); const config = DATASETS[datasetKey]; return config ? config.languages.filter((language) => present.has(language)) : [...present]; } function getAvailableTopics(records, datasetKey, language) { const localized = language ? records.filter((record) => record.language === language) : records; return topicFacets(localized, datasetKey); } function facetLabel(facet) { const prefixes = { math: text("mathFacet"), philosophy: text("philosophyFacet"), technology: text("technologyFacet"), tag: text("tagFacet"), }; return `${prefixes[facet.kind]} — ${slugLabel(facet.value)}`; } function matchesTopic(record, selection) { if (selection === "all") return true; const separator = selection.indexOf(":"); const kind = selection.slice(0, separator); const value = selection.slice(separator + 1); if (kind === "math") return record.math_topic === value; if (kind === "philosophy") return record.philosophical_focus === value; if (kind === "technology") return record.technology_domain === value; if (kind === "tag") return arrayOfText(record.topic_tags).includes(value); return false; } function make(tag, className, content) { const node = document.createElement(tag); if (className) node.className = className; if (content !== undefined && content !== null) node.textContent = String(content); return node; } function remoteText(tag, className, content, language) { const node = make(tag, className, content); if (language) node.lang = language; return node; } function appendList(parent, items, className = "", language = null) { const list = make("ul", className); items.forEach((item) => list.append(remoteText("li", "", item, language))); parent.append(list); } function section(parent, heading, className = "") { const wrapper = make("section", `activity-section ${className}`.trim()); wrapper.append(make("h4", "", heading)); parent.append(wrapper); return wrapper; } function renderJapan(record, body) { const prompt = section(body, text("prompt")); prompt.append(remoteText("p", "prompt-block", record.prompt, record.language)); const objectives = arrayOfText(record.learning_objectives); if (objectives.length) appendList(section(body, text("objectives")), objectives, "", record.language); if (record.expected_response && typeof record.expected_response.short_answer === "string") { section(body, text("answer")).append( remoteText("p", "", record.expected_response.short_answer, record.language), ); } const criteria = record.expected_response ? arrayOfText(record.expected_response.reflection_criteria) : []; if (criteria.length) appendList(section(body, text("reflection")), criteria, "", record.language); if (typeof record.facilitation_note === "string") { section(body, text("facilitation")).append( remoteText("p", "", record.facilitation_note, record.language), ); } const boundary = record.japan_reference && record.japan_reference.boundary_note; if (typeof boundary === "string") { section(body, text("culturalBoundary"), "callout").append( remoteText("p", "", boundary, record.language), ); } } function renderIceland(record, body) { if (typeof record.setting === "string") { section(body, text("setting")).append( remoteText("p", "prompt-block", record.setting, record.language), ); } if (typeof record.discussion_question === "string") { section(body, text("question")).append( remoteText("p", "question-block", record.discussion_question, record.language), ); } if (Array.isArray(record.perspectives) && record.perspectives.length) { const wrapper = section(body, text("perspectives")); const grid = make("div", "perspectives-grid"); record.perspectives.forEach((perspective, index) => { const card = make("section", "perspective-card"); const id = typeof perspective.id === "string" ? perspective.id.toUpperCase() : String(index + 1); card.append(make("p", "perspective-label", text("perspective", { id }))); card.append(remoteText("h5", "", perspective.title, record.language)); card.append(remoteText("p", "", perspective.argument, record.language)); const questions = arrayOfText(perspective.questions); if (questions.length) { card.append(make("h6", "", text("followups"))); appendList(card, questions, "", record.language); } grid.append(card); }); wrapper.append(grid); } const concepts = arrayOfText(record.christian_ethics_concepts); if (concepts.length) { appendList(section(body, text("concepts")), concepts.map(slugLabel), "tag-list", "en"); } const safeguard = record.pluralism && record.pluralism.safeguard; if (typeof safeguard === "string") { section(body, text("pluralism"), "callout").append( remoteText("p", "", safeguard, record.language), ); } const notice = record.fictionality && record.fictionality.notice; if (typeof notice === "string") { section(body, text("fictionality"), "callout").append( remoteText("p", "", notice, record.language), ); } } function setPanel(view) { state.view = view; elements.card.hidden = true; elements.panel.hidden = false; elements.panel.dataset.state = view; const titles = { loading: "loadingTitle", empty: "emptyTitle", error: "errorTitle" }; const messages = { loading: "loadingMessage", empty: "emptyMessage", error: "errorMessage" }; elements.stateTitle.textContent = text(titles[view]); elements.stateMessage.textContent = text(messages[view]); elements.retry.hidden = view !== "error"; elements.region.setAttribute("aria-busy", String(view === "loading")); elements.live.textContent = `${text(titles[view])}. ${text(messages[view])}`; } function renderRecord(record) { state.view = "record"; elements.panel.hidden = true; elements.card.hidden = false; elements.region.setAttribute("aria-busy", "false"); elements.card.lang = state.locale; elements.title.textContent = record.title || record.scenario_title || text("untitled"); elements.title.lang = record.language; elements.context.textContent = DATASETS[state.dataset].name; elements.context.lang = "en"; const current = state.visible.findIndex((item) => item.id === record.id) + 1; elements.position.textContent = text("recordPosition", { current, total: state.visible.length }); elements.body.replaceChildren(); if (state.dataset === "japan") renderJapan(record, elements.body); else renderIceland(record, elements.body); elements.source.href = DATASETS[state.dataset].source; state.selectedId = record.id; elements.live.textContent = text("selected"); } function refreshResult(random = false) { const language = elements.contentLanguage.value; const topic = elements.topic.value; state.visible = filterActivities(state.records, language, topic); if (!state.visible.length) { state.selectedId = null; setPanel("empty"); return; } if (random) { renderRecord(selectRandom(state.visible, state.selectedId)); return; } renderRecord(state.visible[0]); } function populateTopics() { const previous = elements.topic.value; const languageRecords = state.records.filter((record) => record.language === elements.contentLanguage.value); const facets = getAvailableTopics(languageRecords, state.dataset); elements.topic.replaceChildren(); const all = make("option", "", text("allTopics")); all.value = "all"; all.lang = state.locale; elements.topic.append(all); facets.forEach((facet) => { const option = make("option", "", facetLabel(facet)); option.value = `${facet.kind}:${facet.value}`; option.lang = elements.contentLanguage.value; elements.topic.append(option); }); const valid = [...elements.topic.options].some((option) => option.value === previous); elements.topic.value = valid ? previous : "all"; } function populateLanguages(available) { const preferred = elements.contentLanguage.value; elements.contentLanguage.replaceChildren(); available.forEach((language) => { const option = make("option", "", LANGUAGE_NAMES[language] || language); option.value = language; option.lang = language; elements.contentLanguage.append(option); }); elements.contentLanguage.value = available.includes(preferred) ? preferred : (available[0] || ""); } function setControlsLoading(isLoading) { elements.contentLanguage.disabled = isLoading; elements.topic.disabled = isLoading; elements.random.disabled = isLoading; } async function loadDataset(force = false, manageFocus = false) { const datasetKey = state.dataset; const config = DATASETS[datasetKey]; if (state.controller) state.controller.abort(); state.controller = new AbortController(); const controller = state.controller; const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); setPanel("loading"); if (manageFocus) elements.stateTitle.focus?.(); setControlsLoading(true); populateLanguages(config.languages); try { let records = !force ? state.cache.get(datasetKey) : null; if (!records) { const response = await fetch(config.url, { credentials: "omit", headers: { Accept: "application/json, text/plain;q=0.9" }, referrerPolicy: "no-referrer", signal: controller.signal, }); if (!response.ok) throw new Error(`Dataset request failed with status ${response.status}`); const declaredSize = Number(response.headers?.get?.("Content-Length")); if (Number.isFinite(declaredSize) && declaredSize > MAX_DATASET_BYTES) { throw new Error("Dataset response exceeds size limit"); } const source = await readLimitedText(response); records = validateRecords(parseJsonl(source)); if (records.length > MAX_RECORDS) { throw new Error("Dataset record count exceeds limit"); } if (controller.signal.aborted || datasetKey !== state.dataset) return; state.cache.set(datasetKey, records); } if (controller.signal.aborted || datasetKey !== state.dataset) return; state.records = records; populateLanguages(getAvailableLanguages(records, datasetKey)); populateTopics(); setControlsLoading(false); if (!records.length || !elements.contentLanguage.value) setPanel("empty"); else { elements.live.textContent = text("loaded", { count: records.length }); refreshResult(); if (manageFocus) elements.title.focus?.(); } } catch (error) { if (error && error.name === "AbortError" && controller !== state.controller) return; setControlsLoading(false); setPanel("error"); } finally { clearTimeout(timeoutId); if (state.controller === controller) state.controller = null; } } function applyLocale() { document.documentElement.lang = state.locale; document.title = text("pageTitle"); document.querySelectorAll("[data-i18n]").forEach((node) => { node.textContent = text(node.dataset.i18n); }); document.querySelectorAll("[data-i18n-aria]").forEach((node) => { node.setAttribute("aria-label", text(node.dataset.i18nAria)); }); document.querySelectorAll("[data-i18n-content]").forEach((node) => { node.setAttribute("content", text(node.dataset.i18nContent)); }); if (state.records.length) { populateTopics(); const selected = state.records.find((record) => record.id === state.selectedId); if (selected) renderRecord(selected); else if (state.view === "record") refreshResult(); else setPanel(state.view); } else { setPanel(state.view); } } function init() { elements = { uiLanguage: document.getElementById("ui-language"), dataset: document.getElementById("dataset-select"), contentLanguage: document.getElementById("content-language"), topic: document.getElementById("topic-select"), random: document.getElementById("random-button"), retry: document.getElementById("retry-button"), panel: document.getElementById("state-panel"), stateTitle: document.getElementById("state-title"), stateMessage: document.getElementById("state-message"), region: document.getElementById("result-region"), live: document.getElementById("live-status"), card: document.getElementById("activity-card"), title: document.getElementById("activity-title"), context: document.getElementById("activity-context"), position: document.getElementById("record-position"), body: document.getElementById("activity-body"), source: document.getElementById("source-link"), }; elements.uiLanguage.addEventListener("change", () => { state.locale = elements.uiLanguage.value; applyLocale(); }); elements.dataset.addEventListener("change", () => { state.dataset = elements.dataset.value; state.records = []; state.selectedId = null; loadDataset(); }); elements.contentLanguage.addEventListener("change", () => { populateTopics(); refreshResult(); }); elements.topic.addEventListener("change", () => refreshResult()); elements.random.addEventListener("click", () => refreshResult(true)); elements.retry.addEventListener("click", () => loadDataset(true, true)); applyLocale(); loadDataset(); } if (typeof document !== "undefined") { document.addEventListener("DOMContentLoaded", init, { once: true }); } if (typeof module !== "undefined" && module.exports) { module.exports = { parseJsonl, readLimitedText, groupId, validateRecords, buildActivities, filterActivities, selectRandom, getAvailableLanguages, getAvailableTopics, normalizeJapanRecord, normalizeIcelandRecord, TRANSLATIONS: COPY, DATASETS, }; } })();