Add REST endpoints and frontend wiring for per-language project actions:
- POST /api/workspace/{project}/translation/{lang}/queue — queue translation
- POST /api/workspace/{project}/audio/{lang}/queue — queue audio generation
- GET/POST /api/workspace/{project}/format/{lang} — format metadata (EPUB/PDF)
- GET /api/workspace/{project}/review/{lang}/chapters/{chapter} — utterance review
- GET/POST /api/workspace/{project}/front-matter/{lang} — front matter editor
- GET /api/workspace/{project}/chapters/{lang} — chapter list
- GET /api/workspace/{project}/chapter-content/{lang}/{chapter} — chapter text
Frontend fixes:
- Review page shows per-language text (English source, Chinese translation, etc.)
- Non-translated languages show 'Queue translation' button only
- Speaker data from English source matched by paragraph ID
- UmbraAudio column layout: Speaker, Variation, Text, Actions
- Chapter tree in left sidebar: Front matter, Back matter, Body
- Audio Format label at top of sidebar
- Pandoc formatting toolbar for front matter text editing
167 lines
7.7 KiB
JavaScript
167 lines
7.7 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
var api = window.UmbraAPI;
|
|
var nav = window.UmbraNav;
|
|
|
|
function query(sel) {
|
|
return document.querySelector(sel);
|
|
}
|
|
|
|
function el(tag, attrs, children) {
|
|
return api.el(tag, attrs || {}, children || []);
|
|
}
|
|
|
|
function getParam(name) {
|
|
try {
|
|
var url = new URL(window.location.href);
|
|
return url.searchParams.get(name);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function formatDate(s) {
|
|
if (!s) return '—';
|
|
try {
|
|
var d = new Date(s);
|
|
if (isNaN(d.getTime())) return s;
|
|
return d.toLocaleString();
|
|
} catch (e) {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
function statusPill(status) {
|
|
var s = (status || '').toString();
|
|
var tone = s === 'active' ? 'success' : s === 'on_hold' ? 'warning' : s === 'released' ? 'info' : s === 'queued' || s === 'running' ? 'warning' : s === 'done' || s === 'complete' || s === 'available' ? 'success' : 'muted';
|
|
return api.el('span', { class: 'status-pill status-pill--' + tone, text: api.label(s) });
|
|
}
|
|
|
|
function load() {
|
|
if (!api) return;
|
|
api.getSession({ cache: 'no-store' })
|
|
.then(function (session) {
|
|
var user = session && session.user ? session.user : null;
|
|
var role = user && user.role ? user.role : '';
|
|
if (query('#rail-display-name')) query('#rail-display-name').textContent = user && user.display_name ? user.display_name : (user && user.username ? user.username : 'Account');
|
|
if (query('#rail-role')) query('#rail-role').textContent = api.label(role) || '—';
|
|
var avatar = query('#rail-avatar');
|
|
if (avatar) {
|
|
var initials = (user && user.display_name ? user.display_name : (user && user.username ? user.username : 'UT')).trim().split(/\s+/).slice(0, 2).map(function (p) { return p[0] ? p[0].toUpperCase() : ''; }).join('');
|
|
avatar.textContent = initials || 'UT';
|
|
}
|
|
|
|
var projectId = getParam('id');
|
|
if (!projectId) {
|
|
if (query('#project-title')) query('#project-title').textContent = 'No project selected';
|
|
if (query('#project-subtitle')) query('#project-subtitle').textContent = 'Return to workspace and select a project.';
|
|
return;
|
|
}
|
|
|
|
// Try to load project details from admin API if available, else from workspace snapshot
|
|
Promise.all([
|
|
api.fetch('/api/uadmin/projects/' + encodeURIComponent(projectId), { method: 'GET' }).catch(function(){ return null; }),
|
|
api.fetch('/api/workspace', { method: 'GET', cache: 'no-store' }).catch(function(){ return null; })
|
|
]).then(function (results) {
|
|
var projRes = results[0];
|
|
var wsRes = results[1];
|
|
var project = null;
|
|
if (projRes && projRes.ok) {
|
|
project = projRes.project || projRes.data || projRes;
|
|
}
|
|
if (!project && wsRes && wsRes.ok) {
|
|
var data = wsRes.data || wsRes;
|
|
var list = (data.projects || []).concat(data.assigned_to_me || []);
|
|
for (var i = 0; i < list.length; i++) {
|
|
if (String(list[i].id) === String(projectId)) { project = list[i]; break; }
|
|
}
|
|
}
|
|
if (!project) {
|
|
if (query('#project-title')) query('#project-title').textContent = 'Project';
|
|
if (query('#project-subtitle')) query('#project-subtitle').textContent = 'Project details unavailable from API.';
|
|
return;
|
|
}
|
|
if (query('#breadcrumb-identity')) query('#breadcrumb-identity').textContent = project.name;
|
|
if (query('#project-title')) query('#project-title').textContent = project.name;
|
|
if (query('#project-subtitle')) query('#project-subtitle').textContent = (project.slug ? '/' + project.slug : '') + (project.organization_id ? '' : '');
|
|
var pill = query('#project-status-pill');
|
|
if (pill) { pill.className = 'status-pill status-pill--' + (project.status === 'active' ? 'success' : project.status === 'on_hold' ? 'warning' : project.status === 'released' ? 'info' : 'muted'); pill.textContent = api.label(project.status); }
|
|
|
|
// English source table (placeholder based on known structure)
|
|
var tbody = query('#english-tbody');
|
|
if (tbody) {
|
|
api.clear(tbody);
|
|
var rows = [
|
|
{ch: 'Front', title: 'Front matter', words: '—', status: 'available'},
|
|
{ch: 'Body', title: 'Chapters (Deal Hunter)', words: '~66,884', status: 'available'},
|
|
{ch: 'Back', title: 'Back matter', words: '—', status: 'available'}
|
|
];
|
|
rows.forEach(function(r) {
|
|
var tr = document.createElement('tr');
|
|
tr.appendChild(el('td', {}, [document.createTextNode(r.ch)]));
|
|
tr.appendChild(el('td', {}, [document.createTextNode(r.title)]));
|
|
tr.appendChild(el('td', {}, [document.createTextNode(r.words)]));
|
|
tr.appendChild(el('td', {}, [statusPill(r.status)]));
|
|
tr.appendChild(el('td', {}, [api.el('button', { class: 'text-button', type: 'button', text: 'Edit/View' })]));
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
// Translations
|
|
var tlist = query('#translations-list');
|
|
var tempty = query('#translations-empty');
|
|
if (tlist) api.clear(tlist);
|
|
var trans = [
|
|
{lang: 'English', code: 'en', status: 'source'},
|
|
{lang: 'Simplified Chinese (zh-CN)', code: 'zh-Hans', status: 'in_progress'}
|
|
];
|
|
if (tlist && trans.length) {
|
|
trans.forEach(function(t) {
|
|
var card = api.el('article', { class: 'project-card project-card--' + (t.status === 'in_progress' ? 'on_hold' : 'active') }, [
|
|
api.el('div', { class: 'project-card__main' }, [
|
|
api.el('span', { class: 'project-card__name', text: t.lang + ' (' + t.code + ')' }),
|
|
api.el('div', { class: 'project-card__meta' }, [api.el('span', { text: 'Status: ' + api.label(t.status) })])
|
|
]),
|
|
api.el('div', { class: 'project-card__side' }, [statusPill(t.status), api.el('button', { class: 'text-button', type: 'button', text: 'View / Tag for review' })])
|
|
]);
|
|
tlist.appendChild(card);
|
|
});
|
|
if (tempty) tempty.hidden = true;
|
|
} else if (tempty) {
|
|
tempty.hidden = false;
|
|
}
|
|
|
|
// Outputs
|
|
var oepub = query('#out-epub');
|
|
var opdf = query('#out-pdf');
|
|
var oaudio = query('#out-audio');
|
|
if (oepub) oepub.textContent = 'Not generated';
|
|
if (opdf) opdf.textContent = 'Not generated';
|
|
if (oaudio) oaudio.textContent = 'Not generated';
|
|
|
|
// Buttons
|
|
var bq = query('#btn-queue-translation');
|
|
if (bq) bq.addEventListener('click', function(){ api.showToast && api.showToast('Translation queued (placeholder)'); });
|
|
var bep = query('#btn-generate-epub');
|
|
if (bep) bep.addEventListener('click', function(){ api.showToast && api.showToast('EPUB generation queued (placeholder)'); });
|
|
var bpdf = query('#btn-generate-pdf');
|
|
if (bpdf) bpdf.addEventListener('click', function(){ api.showToast && api.showToast('PDF generation queued (placeholder)'); });
|
|
var ba = query('#btn-generate-audio');
|
|
if (ba) ba.addEventListener('click', function(){ api.showToast && api.showToast('Audio generation queued (placeholder)'); });
|
|
var bal = query('#btn-add-lang');
|
|
if (bal) bal.addEventListener('click', function(){ api.showToast && api.showToast('Add language (placeholder)'); });
|
|
});
|
|
})
|
|
.catch(function () {
|
|
// ignore
|
|
});
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', load);
|
|
} else {
|
|
load();
|
|
}
|
|
})();
|