Files
UmbraTrans/web/assets/workspace-project.js
umbra audio 3e0568b9d1 Generate backend endpoints for language-project workflow
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
2026-10-25 11:04:53 -07:00

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();
}
})();