import { h, ref, watch, onMounted } from 'vue'; import { fetchAstLog, fetchScoreText, putScoreText, fetchAudioWidget, URLS } from '../api.js'; import { parseAstLog, buildModel } from '../ast-parser.js'; import { patchScore } from '../exporter.js'; import { shortView } from '../node-views.js'; import { StatusPoller } from './StatusPoller.js'; export const PaneCP = { props: ['store', 'importOnLoad', 'onFocusFO'], setup(props) { const importing = ref(false); const importError = ref(''); const exporting = ref(false); const exportError = ref(''); const audioWidgetHtml = ref(''); watch( () => props.store.synthesisStatus, async (s) => { if (!s?.frozen) { audioWidgetHtml.value = ''; return; } audioWidgetHtml.value = await fetchAudioWidget( s.file_accomplished ? URLS.result : null, s.errors ?? null, ); }, ); async function doImport() { if (props.store.isDirty) return; importError.value = ''; importing.value = true; try { const text = await fetchAstLog(); props.store.scoreModel = buildModel(parseAstLog(text)); props.store.exportLog = []; } catch (e) { importError.value = e.message; } finally { importing.value = false; } } async function doExport() { exportError.value = ''; exporting.value = true; try { const raw = await fetchScoreText(); props.store.rawScoreText = raw; const model = props.store.scoreModel; const { text: patched, log } = patchScore( raw, model.instruments, model.bars, model.info, model.articles ?? [], { articlesModified: !!model.articlesModified }, ); model.articlesModified = false; props.store.exportLog = log; await putScoreText(patched); props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 }; } catch (e) { exportError.value = e.message; } finally { exporting.value = false; } } onMounted(() => { if (props.importOnLoad) doImport(); }); return () => { const store = props.store; const model = store.scoreModel; const fp = store.focusPath; // Build vertical path list: score root + each focus node. const pathItems = model ? [ { node: model, idx: -1 }, ...fp.map((node, idx) => ({ node, idx })), ] : []; return h('div', null, [ // Header h('div', { class: 'se-cp-header' }, [ h('button', { class: 'se-btn', disabled: store.isDirty || importing.value, title: store.isDirty ? 'Save or discard edits before re-importing' : 'Import from server', onClick: doImport, }, importing.value ? 'Importing…' : '→ Import'), h('span', { class: 'se-cp-title' }, model ? (model.info?.title ?? 'Untitled score') : 'No score loaded'), model ? h('button', { class: 'se-btn se-btn-primary', disabled: !store.isDirty || exporting.value, onClick: doExport, }, exporting.value ? 'Exporting…' : 'Export ↑') : null, ]), // Vertical focus path — short views, clickable to navigate up pathItems.length ? h('ul', { class: 'se-object-list se-cp-path' }, pathItems.map(({ node, idx }) => { const { typeTag, label, meta } = shortView(node); const isCurrent = idx === fp.length - 1 || (idx === -1 && fp.length === 0); return h('li', { class: ['se-object-item', isCurrent ? 'focused' : null], onClick: () => { if (idx === -1) store.setFocus([]); else store.setFocus(fp.slice(0, idx + 1)); props.onFocusFO?.(); }, }, [ h('span', { class: 'se-object-type' }, typeTag), h('span', { class: 'se-object-label' }, [ h('strong', null, label), ...meta.map(({ key, value }) => h('span', { style: 'color:#888;margin-left:0.5rem;font-size:0.8em' }, `${key}=${value}`) ), ]), ]); }) ) : null, // Import / export errors importError.value ? h('div', { class: 'se-error' }, importError.value) : null, exportError.value ? h('div', { class: 'se-error' }, exportError.value) : null, // Export log (shown after export, cleared on next import) store.exportLog?.length ? h('div', { class: 'se-export-log' }, store.exportLog.map((entry, i) => h('div', { key: i, class: entry.level === 'changed' ? 'se-export-changed' : 'se-export-info', }, entry.path ? `→ ${entry.path}` : entry.message) ) ) : null, // Status poller (while running) store.synthesisStatus && !store.synthesisStatus.frozen ? h(StatusPoller, { store }) : null, // Audio widget (rendered server-side from audiowidget.tmpl) audioWidgetHtml.value ? h('div', { innerHTML: audioWidgetHtml.value, style: 'margin-top:0.5rem' }) : null, ]); }; }, };