11import { isFiniteNumber } from './numberUtils.js' ;
2+ import { getToolRegistry } from '/tools/toolRegistry.js' ;
23
34const METADATA_URL = '/samples/metadata/samples.index.metadata.json' ;
45const BACK_TO_SAMPLES_HREF = '/samples/index.html' ;
@@ -7,11 +8,38 @@ const TAGS_BLOCK_ID = 'sample-detail-tags';
78const NAV_BLOCK_ID = 'sample-detail-navigation' ;
89const RELATED_BLOCK_ID = 'sample-detail-related' ;
910const ENGINE_BLOCK_ID = 'sample-detail-engine-classes' ;
11+ const PRESET_BLOCK_ID = 'sample-detail-tool-presets' ;
1012
1113function asOptionalPath ( value ) {
1214 return typeof value === 'string' ? value . trim ( ) : '' ;
1315}
1416
17+ function normalizeToken ( value ) {
18+ return String ( value || '' ) . trim ( ) . toLowerCase ( ) ;
19+ }
20+
21+ function normalizePresetPath ( value ) {
22+ const normalized = String ( value || '' ) . trim ( ) . replace ( / \\ / g, '/' ) ;
23+ if ( ! normalized || normalized . includes ( '..' ) ) {
24+ return '' ;
25+ }
26+ if ( normalized . startsWith ( '/samples/' ) ) {
27+ return normalized ;
28+ }
29+ if ( normalized . startsWith ( './samples/' ) ) {
30+ return '/' + normalized . slice ( 2 ) ;
31+ }
32+ if ( normalized . startsWith ( 'samples/' ) ) {
33+ return '/' + normalized ;
34+ }
35+ return '' ;
36+ }
37+
38+ function toStandaloneToolHref ( entryPoint ) {
39+ const normalized = String ( entryPoint || '' ) . replace ( / ^ \. ? \/ * / , '' ) ;
40+ return normalized ? '/tools/' + encodeURI ( normalized ) : '' ;
41+ }
42+
1543function normalizeWhitespace ( value ) {
1644 return String ( value || '' ) . replace ( / \s + / g, ' ' ) . trim ( ) ;
1745}
@@ -59,7 +87,7 @@ function normalizeEngineClassRef(value) {
5987}
6088
6189function removePriorGeneratedBlocks ( main ) {
62- const staleIds = [ TAGS_BLOCK_ID , NAV_BLOCK_ID , RELATED_BLOCK_ID , ENGINE_BLOCK_ID ] ;
90+ const staleIds = [ TAGS_BLOCK_ID , NAV_BLOCK_ID , RELATED_BLOCK_ID , ENGINE_BLOCK_ID , PRESET_BLOCK_ID ] ;
6391 for ( const id of staleIds ) {
6492 const element = main . querySelector ( '#' + id ) ;
6593 if ( element ) {
@@ -126,7 +154,18 @@ export function normalizeMetadata(raw) {
126154 tags : normalizedTags ,
127155 engineClassesUsed : normalizedEngineClasses ,
128156 thumbnail : asOptionalPath ( entry . thumbnail ) ,
129- preview : asOptionalPath ( entry . preview )
157+ preview : asOptionalPath ( entry . preview ) ,
158+ toolHints : Array . isArray ( entry . toolHints )
159+ ? [ ...new Set ( entry . toolHints . map ( ( toolId ) => normalizeToken ( toolId ) ) . filter ( Boolean ) ) ]
160+ : [ ] ,
161+ roundtripToolPresets : Array . isArray ( entry . roundtripToolPresets )
162+ ? entry . roundtripToolPresets
163+ . map ( ( preset ) => ( {
164+ toolId : normalizeToken ( preset && preset . toolId ) ,
165+ presetPath : normalizePresetPath ( preset && preset . presetPath )
166+ } ) )
167+ . filter ( ( preset ) => preset . toolId && preset . presetPath )
168+ : [ ]
130169 } ;
131170 } ) ;
132171
@@ -321,6 +360,102 @@ function buildEngineClassesBlock(model) {
321360 return section ;
322361}
323362
363+ function buildRoundtripLinks ( currentSample , toolRegistryMap ) {
364+ const dedupedToolHints = [
365+ ...new Set ( ( Array . isArray ( currentSample . toolHints ) ? currentSample . toolHints : [ ] ) . map ( ( toolId ) => normalizeToken ( toolId ) ) . filter ( Boolean ) )
366+ ] . filter ( ( toolId ) => toolId !== 'workspace-manager' ) ;
367+
368+ const links = [ ] ;
369+ for ( const toolId of dedupedToolHints ) {
370+ const tool = toolRegistryMap . get ( toolId ) ;
371+ if ( ! tool ) {
372+ continue ;
373+ }
374+
375+ const baseHref = toStandaloneToolHref ( tool . entryPoint ) ;
376+ if ( ! baseHref ) {
377+ continue ;
378+ }
379+
380+ const mapping = ( Array . isArray ( currentSample . roundtripToolPresets ) ? currentSample . roundtripToolPresets : [ ] )
381+ . find ( ( entry ) => normalizeToken ( entry && entry . toolId ) === toolId ) ;
382+
383+ const presetPath = normalizePresetPath ( mapping && mapping . presetPath ) ;
384+ let href = baseHref ;
385+ if ( presetPath ) {
386+ href = `${ baseHref } ?sampleId=${ encodeURIComponent ( currentSample . id ) } &sampleTitle=${ encodeURIComponent ( currentSample . title || '' ) } &samplePresetPath=${ encodeURIComponent ( presetPath ) } ` ;
387+ }
388+
389+ links . push ( {
390+ toolId,
391+ label : tool . displayName || tool . name || toolId ,
392+ href,
393+ presetPath
394+ } ) ;
395+ }
396+ return links ;
397+ }
398+
399+ async function fetchPresetStatus ( presetPath ) {
400+ if ( ! presetPath ) {
401+ return { ok : false , detail : 'missing preset path' } ;
402+ }
403+ try {
404+ const response = await fetch ( presetPath , { cache : 'no-store' } ) ;
405+ if ( ! response . ok ) {
406+ return { ok : false , detail : `request failed (${ response . status } )` } ;
407+ }
408+ const parsed = await response . json ( ) ;
409+ if ( ! parsed || typeof parsed !== 'object' ) {
410+ return { ok : false , detail : 'invalid JSON object' } ;
411+ }
412+ return { ok : true , detail : 'loaded' } ;
413+ } catch ( error ) {
414+ return { ok : false , detail : error instanceof Error ? error . message : 'unknown error' } ;
415+ }
416+ }
417+
418+ async function buildToolPresetBlock ( model , toolRegistryMap ) {
419+ const section = document . createElement ( 'section' ) ;
420+ section . id = PRESET_BLOCK_ID ;
421+ section . className = 'sample-detail-row sample-tool-roundtrip' ;
422+
423+ const heading = document . createElement ( 'h3' ) ;
424+ heading . textContent = 'Tool Preset Sources' ;
425+ section . appendChild ( heading ) ;
426+
427+ const links = buildRoundtripLinks ( model . current , toolRegistryMap ) ;
428+ if ( ! links . length ) {
429+ const empty = document . createElement ( 'p' ) ;
430+ empty . className = 'sample-detail-muted' ;
431+ empty . textContent = 'No tool preset links available for this sample.' ;
432+ section . appendChild ( empty ) ;
433+ return section ;
434+ }
435+
436+ const statuses = await Promise . all ( links . map ( ( entry ) => fetchPresetStatus ( entry . presetPath ) ) ) ;
437+ const successful = statuses . filter ( ( status ) => status . ok ) . length ;
438+
439+ const summary = document . createElement ( 'p' ) ;
440+ summary . textContent = `Sample consumed ${ successful } /${ links . length } tool preset JSON files used by roundtrip launch links.` ;
441+ section . appendChild ( summary ) ;
442+
443+ const list = document . createElement ( 'ul' ) ;
444+ links . forEach ( ( entry , index ) => {
445+ const item = document . createElement ( 'li' ) ;
446+ const link = createLink ( entry . href , `Open ${ entry . label } ` ) ;
447+ item . appendChild ( link ) ;
448+
449+ const presetText = document . createElement ( 'span' ) ;
450+ const status = statuses [ index ] ;
451+ presetText . textContent = ` | Preset: ${ entry . presetPath || '(none)' } | Status: ${ status . ok ? 'loaded' : `failed (${ status . detail } )` } ` ;
452+ item . appendChild ( presetText ) ;
453+ list . appendChild ( item ) ;
454+ } ) ;
455+ section . appendChild ( list ) ;
456+ return section ;
457+ }
458+
324459function reorderMainChildren ( main , orderedNodes ) {
325460 const orderedSet = new Set ( orderedNodes . filter ( Boolean ) ) ;
326461 const extras = Array . from ( main . children ) . filter ( ( child ) => ! orderedSet . has ( child ) ) ;
@@ -358,6 +493,14 @@ export async function applySampleDetailEnhancement() {
358493 return ;
359494 }
360495
496+ const toolRegistry = getToolRegistry ( ) ;
497+ const toolRegistryMap = new Map (
498+ toolRegistry
499+ . filter ( ( tool ) => normalizeToken ( tool . id ) !== 'workspace-manager' )
500+ . map ( ( tool ) => [ normalizeToken ( tool . id ) , tool ] )
501+ . filter ( ( entry ) => entry [ 0 ] && entry [ 1 ] )
502+ ) ;
503+
361504 ensureStyles ( ) ;
362505 removePriorGeneratedBlocks ( main ) ;
363506
@@ -375,8 +518,10 @@ export async function applySampleDetailEnhancement() {
375518
376519 const relatedBlock = buildRelatedBlock ( model ) ;
377520 const engineClassesBlock = buildEngineClassesBlock ( model ) ;
521+ const toolPresetBlock = await buildToolPresetBlock ( model , toolRegistryMap ) ;
378522 canvas . insertAdjacentElement ( 'afterend' , relatedBlock ) ;
379523 relatedBlock . insertAdjacentElement ( 'afterend' , engineClassesBlock ) ;
524+ engineClassesBlock . insertAdjacentElement ( 'afterend' , toolPresetBlock ) ;
380525
381526 reorderMainChildren ( main , [
382527 titleAndDescription . h1 ,
@@ -385,7 +530,8 @@ export async function applySampleDetailEnhancement() {
385530 navBlock ,
386531 canvas ,
387532 relatedBlock ,
388- engineClassesBlock
533+ engineClassesBlock ,
534+ toolPresetBlock
389535 ] ) ;
390536
391537 if ( model . current . indexLabel ) {
0 commit comments