11import React , { Component , Fragment , useEffect , useState } from "react" ;
22import init , { build_network } from "hivcluster_rs_web" ;
3+ import hivannotateInit , { annotate_network_json } from "hivannotate_rs_web" ;
34
45// Fallback initialization and methods in case npm package fails
56let hivclusterInit = init ;
67let hivclusterBuildNetwork = build_network ;
8+ let hivannotateAnnotateNetwork = annotate_network_json ;
79
810import {
11+ ATTRIBUTES_PATH ,
912 AVAILABLE_REFERENCES ,
1013 CAWLIGN_TEST_DATA_PATH ,
1114 CAWLIGN_VERSION ,
1215 CLEAR_LOG ,
1316 GET_TIME_WITH_MILLISECONDS ,
1417 HIVCLUSTER_RS_VERSION ,
1518 OUTPUT_ID ,
19+ SCHEMA_PATH ,
1620 SEATTLE_FASTA_PATH ,
1721 TN93_VERSION ,
1822} from "./constants" ;
@@ -42,13 +46,59 @@ export class App extends Component {
4246 inputData : null ,
4347 alignmentData : null ,
4448 pairwiseDistances : null ,
49+ annotatedNetworkData : null ,
50+ attributesData : null ,
51+ schemaData : null ,
52+ useAnnotation : false ,
53+ customAttributesFile : undefined ,
54+ customSchemaFile : undefined ,
4555 } ;
4656 }
4757
4858 componentDidMount ( ) {
4959 this . initHivclusterRS ( ) ;
60+ this . initHivannotate ( ) ;
5061 this . initBiowasm ( ) ;
5162 }
63+
64+ initHivannotate = async ( ) => {
65+ try {
66+ this . log ( "Initializing HIVAnnotate WASM..." ) ;
67+ await hivannotateInit ( ) ;
68+ this . log ( "HIVAnnotate WASM initialized successfully." ) ;
69+
70+ // Load default attributes and schema
71+ this . loadDefaultAnnotationData ( ) ;
72+ } catch ( error ) {
73+ this . log ( `Error initializing HIVAnnotate WASM: ${ error . message || error } ` ) ;
74+ console . error ( "HIVAnnotate init error:" , error ) ;
75+ }
76+ }
77+
78+ loadDefaultAnnotationData = async ( ) => {
79+ try {
80+ // Fetch default attributes and schema
81+ const attributesResponse = await fetch ( `${ import . meta. env . BASE_URL || "" } ${ ATTRIBUTES_PATH } ` ) ;
82+ const schemaResponse = await fetch ( `${ import . meta. env . BASE_URL || "" } ${ SCHEMA_PATH } ` ) ;
83+
84+ if ( ! attributesResponse . ok || ! schemaResponse . ok ) {
85+ throw new Error ( "Failed to fetch annotation data" ) ;
86+ }
87+
88+ const attributesData = await attributesResponse . json ( ) ;
89+ const schemaData = await schemaResponse . json ( ) ;
90+
91+ this . setState ( {
92+ attributesData : JSON . stringify ( attributesData ) ,
93+ schemaData : JSON . stringify ( schemaData )
94+ } ) ;
95+
96+ this . log ( "Default annotation data loaded successfully" ) ;
97+ } catch ( error ) {
98+ this . log ( `Error loading annotation data: ${ error . message || error } ` ) ;
99+ console . error ( "Annotation data loading error:" , error ) ;
100+ }
101+ }
52102
53103 setInputFile = ( event ) => {
54104 this . setState ( { inputFile : event . target . files [ 0 ] } ) ;
@@ -87,6 +137,24 @@ export class App extends Component {
87137 this . log ( `Custom reference file selected: ${ event . target . files [ 0 ] . name } ` ) ;
88138 }
89139 } ;
140+
141+ toggleAnnotation = ( event ) => {
142+ this . setState ( { useAnnotation : event . target . checked } ) ;
143+ } ;
144+
145+ setCustomAttributesFile = ( event ) => {
146+ if ( event . target . files && event . target . files [ 0 ] ) {
147+ this . setState ( { customAttributesFile : event . target . files [ 0 ] } ) ;
148+ this . log ( `Custom attributes file selected: ${ event . target . files [ 0 ] . name } ` ) ;
149+ }
150+ } ;
151+
152+ setCustomSchemaFile = ( event ) => {
153+ if ( event . target . files && event . target . files [ 0 ] ) {
154+ this . setState ( { customSchemaFile : event . target . files [ 0 ] } ) ;
155+ this . log ( `Custom schema file selected: ${ event . target . files [ 0 ] . name } ` ) ;
156+ }
157+ } ;
90158
91159 initHivclusterRS = async ( ) => {
92160 try {
@@ -267,7 +335,10 @@ export class App extends Component {
267335 ambiguityFraction : 0.015 ,
268336 removeDrams : "no" ,
269337 reference : "HXB2_pol" , // Default reference for the example data
270- customReferenceFile : undefined
338+ customReferenceFile : undefined ,
339+ useAnnotation : true , // Enable annotation for example data
340+ customAttributesFile : undefined ,
341+ customSchemaFile : undefined
271342 } , ( ) => {
272343 this . log ( "Example data and parameters loaded successfully." ) ;
273344 // Display the file name in the UI
@@ -586,6 +657,12 @@ export class App extends Component {
586657 }
587658
588659 this . log ( "HIVCluster-RS processing completed successfully" ) ;
660+
661+ // Run annotation if enabled
662+ if ( this . state . useAnnotation ) {
663+ await this . runAnnotation ( jsonOutput ) ;
664+ }
665+
589666 console . log ( "Network data:" , networkData ) ;
590667 } catch ( jsonError ) {
591668 this . log ( `Error parsing network JSON: ${ jsonError . message } ` ) ;
@@ -605,6 +682,72 @@ export class App extends Component {
605682 }
606683 } ;
607684
685+ runAnnotation = async ( networkJson ) => {
686+ this . log ( "Running HIVAnnotate on network data..." ) ;
687+
688+ try {
689+ // Get attributes and schema data - either from custom upload or default
690+ let attributesJson , schemaJson ;
691+
692+ if ( this . state . customAttributesFile ) {
693+ // Read from custom file
694+ this . log ( "Reading custom attributes file..." ) ;
695+ attributesJson = await this . state . customAttributesFile . text ( ) ;
696+ } else if ( this . state . attributesData ) {
697+ // Use pre-loaded data
698+ attributesJson = this . state . attributesData ;
699+ this . log ( "Using default attributes data" ) ;
700+ } else {
701+ throw new Error ( "No attributes data available" ) ;
702+ }
703+
704+ if ( this . state . customSchemaFile ) {
705+ // Read from custom file
706+ this . log ( "Reading custom schema file..." ) ;
707+ schemaJson = await this . state . customSchemaFile . text ( ) ;
708+ } else if ( this . state . schemaData ) {
709+ // Use pre-loaded data
710+ schemaJson = this . state . schemaData ;
711+ this . log ( "Using default schema data" ) ;
712+ } else {
713+ throw new Error ( "No schema data available" ) ;
714+ }
715+
716+ // Run annotation
717+ this . log ( "Annotating network..." ) ;
718+ const annotatedJson = hivannotateAnnotateNetwork ( networkJson , attributesJson , schemaJson ) ;
719+
720+ if ( ! annotatedJson ) {
721+ throw new Error ( "Annotation failed - no output returned" ) ;
722+ }
723+
724+ // Store annotated data
725+ this . setState ( { annotatedNetworkData : annotatedJson } ) ;
726+
727+ // Parse to get stats
728+ const annotatedData = JSON . parse ( annotatedJson ) ;
729+ const hasTraceResults = annotatedData . trace_results !== undefined ;
730+ const rootObj = hasTraceResults ? annotatedData . trace_results : annotatedData ;
731+
732+ // Count nodes with annotations
733+ if ( rootObj . Nodes ) {
734+ const totalNodes = rootObj . Nodes . length || 0 ;
735+ const nodesWithAttributes = rootObj . Nodes . filter ( n =>
736+ n . patient_attributes !== undefined ) . length || 0 ;
737+
738+ this . log ( `Annotation complete: ${ nodesWithAttributes } of ${ totalNodes } nodes annotated (${ Math . round ( nodesWithAttributes / totalNodes * 100 ) } %)` ) ;
739+ }
740+
741+ this . log ( "HIVAnnotate processing completed successfully" ) ;
742+ return annotatedJson ;
743+
744+ } catch ( error ) {
745+ this . log ( `Error in annotation process: ${ error . message || error } ` ) ;
746+ console . error ( "Annotation error:" , error ) ;
747+ return null ;
748+ }
749+ } ;
750+
608751 downloadData = ( filename , content ) => {
609752 const blob = new Blob ( [ content ] , { type : 'text/plain' } ) ;
610753 const url = URL . createObjectURL ( blob ) ;
@@ -791,6 +934,56 @@ export class App extends Component {
791934 Upload a custom reference sequence file for alignment. This will override the selection above.
792935 </ small >
793936 </ div >
937+
938+ < hr className = "mt-4 mb-4" />
939+
940+ < div className = "mb-3" >
941+ < div className = "form-check form-switch" >
942+ < input
943+ className = "form-check-input"
944+ type = "checkbox"
945+ id = "use-annotation"
946+ checked = { this . state . useAnnotation }
947+ onChange = { this . toggleAnnotation }
948+ />
949+ < label className = "form-check-label" htmlFor = "use-annotation" >
950+ Enable Network Annotation
951+ </ label >
952+ </ div >
953+ < small className = "text-muted d-block mt-1" >
954+ Annotate network nodes with additional metadata from attributes file
955+ </ small >
956+ </ div >
957+
958+ { this . state . useAnnotation && (
959+ < div className = "annotation-options" >
960+ < div className = "mb-3" >
961+ < label htmlFor = "custom-attributes" className = "form-label" > Custom Attributes JSON (Optional)</ label >
962+ < input
963+ className = "form-control"
964+ type = "file"
965+ id = "custom-attributes"
966+ onChange = { this . setCustomAttributesFile }
967+ />
968+ < small className = "text-muted d-block mt-1" >
969+ Upload a custom attributes file or use the default example data
970+ </ small >
971+ </ div >
972+
973+ < div className = "mb-3" >
974+ < label htmlFor = "custom-schema" className = "form-label" > Custom Schema JSON (Optional)</ label >
975+ < input
976+ className = "form-control"
977+ type = "file"
978+ id = "custom-schema"
979+ onChange = { this . setCustomSchemaFile }
980+ />
981+ < small className = "text-muted d-block mt-1" >
982+ Upload a custom schema file or use the default example data
983+ </ small >
984+ </ div >
985+ </ div >
986+ ) }
794987 </ div >
795988 < button
796989 className = "btn btn-warning mt-4 w-100"
@@ -852,12 +1045,21 @@ export class App extends Component {
8521045 < div className = "downloads mt-4" >
8531046 < h4 > Download Results</ h4 >
8541047 < div className = "download-buttons" >
855- < button
856- className = "btn btn-success me-2"
857- onClick = { ( ) => this . downloadData ( 'network_results.json' , JSON . stringify ( this . state . networkData , null , 2 ) ) }
858- >
859- < i className = "bi bi-download me-2" > </ i > Network Results (JSON)
860- </ button >
1048+ { this . state . annotatedNetworkData ? (
1049+ < button
1050+ className = "btn btn-success me-2"
1051+ onClick = { ( ) => this . downloadData ( 'annotated_network_results.json' , this . state . annotatedNetworkData ) }
1052+ >
1053+ < i className = "bi bi-download me-2" > </ i > Annotated Network Results (JSON)
1054+ </ button >
1055+ ) : (
1056+ < button
1057+ className = "btn btn-success me-2"
1058+ onClick = { ( ) => this . downloadData ( 'network_results.json' , JSON . stringify ( this . state . networkData , null , 2 ) ) }
1059+ >
1060+ < i className = "bi bi-download me-2" > </ i > Network Results (JSON)
1061+ </ button >
1062+ ) }
8611063
8621064 { this . state . inputData && (
8631065 < button
0 commit comments