feat: show annotations from job to user#172
Conversation
FilipVrubel
commented
May 19, 2026
- After a job completes, its output annotations are fetched and drawn on a Canvas2D overlay on the slide (read-only, non-interactive)
- _captureAnnotation now posts directly to MDS via scope.annotations.create() instead of polling for an ID
- string inputs render as textarea
- Known limitation: overlay is read-only, not part of the OSDAnnotations system.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a JobResultsOverlay class to render job results on the OpenSeadragon viewer and updates the analyze-dev plugin to fetch, decode, and display these results upon job completion. It also refactors the annotation capture logic to create annotations directly in the MDS and adds support for multi-line string inputs. The review feedback suggests optimizing performance by parallelizing annotation fetching, improving code readability with declarative array methods, and refactoring the shape-drawing logic into a strategy pattern for better extensibility.
| const allShapes = []; | ||
| for (const key of annotationKeys) { | ||
| const collectionId = job.outputs[key]; | ||
| if (!collectionId) { | ||
| console.log('[analyze] no collection ID for output key', key); | ||
| continue; | ||
| } | ||
| try { | ||
| const result = await scope.collections.queryItems(collectionId, {}); | ||
| if (!result?.items?.length) { | ||
| console.log('[analyze] empty collection for key', key); | ||
| continue; | ||
| } | ||
| console.log('[analyze] fetched', result.items.length, 'annotations for key', key); | ||
| const decoded = await this._empaiaConvertor.decode({ items: result.items, presets: [] }); | ||
| if (decoded?.objects) allShapes.push(...decoded.objects.filter(Boolean)); | ||
| } catch (e) { | ||
| console.warn('[analyze] failed to fetch/decode annotations for key', key, e); | ||
| } | ||
| } |
There was a problem hiding this comment.
The loop for fetching and decoding annotations runs sequentially for each annotation key. Since these are independent network requests, they can be parallelized using Promise.all to improve performance, especially when there are multiple annotation outputs.
const promises = annotationKeys.map(async (key) => {
const collectionId = job.outputs[key];
if (!collectionId) {
console.log('[analyze] no collection ID for output key', key);
return [];
}
try {
const result = await scope.collections.queryItems(collectionId, {});
if (!result?.items?.length) {
console.log('[analyze] empty collection for key', key);
return [];
}
console.log('[analyze] fetched', result.items.length, 'annotations for key', key);
const decoded = await this._empaiaConvertor.decode({ items: result.items, presets: [] });
return decoded?.objects?.filter(Boolean) || [];
} catch (e) {
console.warn('[analyze] failed to fetch/decode annotations for key', key, e);
return [];
}
});
const allShapes = (await Promise.all(promises)).flat();| state.objects = []; | ||
| for (const [, job] of this._jobStore) { | ||
| if (String(job.viewerId) !== String(viewerId)) continue; | ||
| for (const obj of job.fabricObjects) state.objects.push(obj); | ||
| } |
There was a problem hiding this comment.
| return tiledImage.imageToViewerElementCoordinates(new OpenSeadragon.Point(x, y)); | ||
| } | ||
|
|
||
| _drawShape(ctx, decoded, color, tiledImage) { |
There was a problem hiding this comment.
The _drawShape method uses a long if/else if chain to handle different shape types. For better maintainability and extensibility, consider refactoring this into a strategy pattern, for example by using a map of factoryID to drawing functions. Each drawing function could be a separate private method on the class.
|
The annotation integration should rely either on annotations module api, or use the flex renderer tile sources to render annotations as overlays. Not implementing new custom approach. |
| pluginReady() { | ||
| this._overlay = new JobResultsOverlay(); | ||
| this._empaiaConvertor = null; | ||
| UTILITIES.loadPlugin('gui_annotations'); |
There was a problem hiding this comment.
In general we should call integrateWithPlugin conditionally instead and deal with the case when the plugin is not available. this line force-loads the plugin, which:
- is problematic when the plugin already exists in the system
- is problematic when the plugin was disabled in the system or
- the system does not want the plugin available to a user even if the plugin is available
So you could either try to disable actions that require annotations and work in a limited way (e.g. allow other thatn ROI-based executions and do not render annotations in interactive way, for example using the visualization overlay tiled image), or simply say 'I cannot work without annotations' in the dialogs.
| continue; | ||
| } | ||
| try { | ||
| const result = await scope.collections.queryItems(collectionId, {}); |
There was a problem hiding this comment.
Especially here you could potentially query more of them at once within one HTTP request I think. at least empaia API usually has query accepting multiple entries
| if (String(job.viewerId) !== String(viewerId)) continue; | ||
| for (const obj of job.fabricObjects) state.objects.push(obj); | ||
| const fabric = annot.getFabric(viewer); | ||
| if (!fabric) { |
There was a problem hiding this comment.
Did you test with multiple viewers? This code looks good, but working with multiple viewers is tricky and should be tested too, later on. Just keep this in mind for a later stage, we can not just work wihtout testing it until we have stuff we need.
|
|
||
| try { | ||
| // createLayer() returns Promise<undefined>; retrieve via getLayer() after awaiting. | ||
| await fabric.createLayer(layerId); |