Spaces:
Running
Running
| (() => { | |
| "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, | |
| }; | |
| } | |
| })(); | |