Skip to content

Commit 6d1399b

Browse files
committed
feat: materialize modality metadata
1 parent a63eed5 commit 6d1399b

5 files changed

Lines changed: 34 additions & 9 deletions

File tree

web/src/__tests__/subject-parsers.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ describe('parseAcquisition', () => {
142142
});
143143
expect(ev.type).toBe('Acquisition');
144144
expect(ev.event).toContain('Ephys');
145-
expect(ev.details).toContain('4.0');
145+
expect(Array.isArray(ev.modalities)).toBe(true);
146146
});
147147

148148
it('includes protocol in label when present', () => {

web/src/subject/details.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from './parsers.js';
1717
import { createBrainVizCanvas } from './brain-viz.js';
1818
import { createBrainViz3D } from './brain-viz-3d.js';
19-
import { buildQcLink, buildMetadataLink, buildCoLink } from '../assets/view.js';
19+
import { buildQcLink, buildMetadataLink, buildCoLink, buildS3ConsoleUrl } from '../assets/view.js';
2020

2121
// ---------------------------------------------------------------------------
2222
// Pure HTML-string builders (Node-testable)
@@ -60,13 +60,15 @@ export function buildBirthDetail(event) {
6060
* @returns {string}
6161
*/
6262
export function buildAcquisitionDetail(event) {
63-
const { start, end, event: label, details, data = {} } = event;
63+
const { start, end, event: label, modalities, data = {} } = event;
6464
const durationHrs = start && end ? ((end - start) / 3_600_000).toFixed(2) : 'N/A';
6565
const assetName = data._assetName ?? null;
6666
const qcHref = buildQcLink(assetName);
6767
const metaHref = buildMetadataLink(assetName);
6868
const coHref = buildCoLink(data._codeOcean ?? null);
69+
const s3Href = buildS3ConsoleUrl(data._location ?? null);
6970
const linkParts = [
71+
s3Href ? `<a href="${s3Href}" target="_blank" rel="noopener noreferrer">S3</a>` : '',
7072
coHref ? `<a href="${coHref}" target="_blank" rel="noopener noreferrer">Code Ocean</a>` : '',
7173
metaHref ? `<a href="${metaHref}" target="_blank" rel="noopener noreferrer">Metadata</a>` : '',
7274
qcHref ? `<a href="${qcHref}" target="_blank" rel="noopener noreferrer">QC Portal</a>` : '',
@@ -81,6 +83,7 @@ export function buildAcquisitionDetail(event) {
8183
data.reward_consumed_total != null
8284
? `<dt>Reward consumed</dt><dd>${data.reward_consumed_total} ${data.reward_consumed_unit ?? ''}</dd>`
8385
: '',
86+
modalities?.length ? `<dt>Modalities</dt><dd>${modalities.join(', ')}</dd>` : '',
8487
linkParts ? `<dt>Links</dt><dd>${linkParts}</dd>` : '',
8588
].join('');
8689
return `
@@ -90,7 +93,6 @@ export function buildAcquisitionDetail(event) {
9093
<dt>Start</dt><dd>${fmtDateTime(start)}</dd>
9194
<dt>End</dt><dd>${fmtDateTime(end)}</dd>
9295
<dt>Duration</dt><dd>${durationHrs} hours</dd>
93-
<dt>Details</dt><dd>${details ?? ''}</dd>
9496
${extra}
9597
</dl>
9698
</div>`;

web/src/subject/parsers.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,16 @@ export function parseAcquisition(acquisition) {
141141
const label = protocol ? `${acqType} (${protocol})` : acqType;
142142
const durationHrs = (end - start) / 3_600_000;
143143

144+
const modalities = (acquisition._modalities ?? [])
145+
.map((m) => m.abbreviation ?? m.name)
146+
.filter(Boolean);
147+
144148
return {
145149
start,
146150
end,
147151
event: label,
148152
type: 'Acquisition',
149-
details: `Duration: ${durationHrs.toFixed(1)} hours`,
153+
modalities,
150154
data: acquisition,
151155
};
152156
}

web/src/subject/timeline.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ export function createSubjectTimeline(events, opts = {}) {
220220
bubble.appendChild(typeEl);
221221
bubble.appendChild(dateEl);
222222

223+
if (ev.type === 'Acquisition' && ev.modalities?.length) {
224+
const modEl = document.createElement('span');
225+
modEl.className = 'tl-bubble-modalities';
226+
modEl.textContent = ev.modalities.join(', ');
227+
bubble.appendChild(modEl);
228+
}
229+
223230
bubble.addEventListener('click', () => {
224231
if (selectedBubble) selectedBubble.classList.remove('tl-bubble--selected');
225232
bubble.classList.add('tl-bubble--selected');

web/src/subject/view.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export function organizeSubjectData(records, subjectId) {
120120
rec.acquisition?.acquisition_start_time &&
121121
rec.data_description?.data_level !== 'derived'
122122
) {
123-
bundle.acquisitions.push({ ...rec.acquisition, _assetName: rec.name ?? '' });
123+
bundle.acquisitions.push({ ...rec.acquisition, _assetName: rec.name ?? '', _modalities: rec.data_description?.modalities ?? [] });
124124
}
125125
}
126126

@@ -294,7 +294,6 @@ async function _loadSubject(contentEl, subjectId, coordinator, signal) {
294294
assetsTableEl.querySelectorAll('tr[data-asset-name]').forEach((r) => {
295295
const isTarget = targetName && r.dataset.assetName === targetName;
296296
r.classList.toggle('asset-highlighted', isTarget);
297-
if (isTarget) r.scrollIntoView({ block: 'nearest' });
298297
});
299298
}
300299
},
@@ -310,8 +309,21 @@ async function _loadSubject(contentEl, subjectId, coordinator, signal) {
310309

311310
// Async: fetch DuckDB asset data (projects + grouped assets table)
312311
if (coordinator) {
313-
_fetchAndRenderAssets(coordinator, subjectId, infoEl, assetsSection, bundle.subject).then((tableEl) => {
312+
_fetchAndRenderAssets(coordinator, subjectId, infoEl, assetsSection, bundle.subject).then(({ tableEl, assets }) => {
314313
assetsTableEl = tableEl;
314+
// Enrich acquisition event data with S3 location and Code Ocean from DuckDB
315+
if (assets?.length) {
316+
const assetByName = new Map(assets.map((a) => [a.name, a]));
317+
for (const ev of events) {
318+
if (ev.type === 'Acquisition' && ev.data?._assetName) {
319+
const asset = assetByName.get(ev.data._assetName);
320+
if (asset) {
321+
ev.data._codeOcean = asset.code_ocean ?? null;
322+
ev.data._location = asset.location ?? null;
323+
}
324+
}
325+
}
326+
}
315327
}).catch((err) => {
316328
console.error('[SubjectView] Asset fetch failed:', err);
317329
assetsSection.innerHTML = `<h3>Assets</h3><p class="error-banner">Failed to load assets: ${err.message}</p>`;
@@ -388,7 +400,7 @@ async function _fetchAndRenderAssets(coordinator, subjectId, infoEl, assetsSecti
388400
assetsSection.innerHTML = '<h3>Assets</h3>';
389401
const tableEl = _buildAssetsTable(assets, sourceMap);
390402
assetsSection.appendChild(tableEl);
391-
return tableEl;
403+
return { tableEl, assets };
392404
}
393405

394406
function _buildAssetsTable(assets, sourceMap) {

0 commit comments

Comments
 (0)