11import { useRef , useState , useCallback , useEffect , DragEvent , ChangeEvent } from "react" ;
22
3+ const API_BASE = "https://harshithreddy01-polyp-detection.hf.space" ;
4+
35const ALLOWED_TYPES = [ "image/jpeg" , "image/jpg" , "image/png" ] ;
46const MAX_MB = 10 ;
57
8+ type ModelName = "Kvasir-Seg" | "BKAI-IGH" ;
9+
610type State =
711 | { stage : "idle" }
812 | { stage : "loading" }
913 | { stage : "error" ; message : string }
10- | { stage : "result" ; originalURL : string ; maskURL : string } ;
14+ | { stage : "result" ; originalURL : string ; maskURL : string ; model : ModelName } ;
15+
16+ const MODEL_INFO : Record < ModelName , { title : string ; description : string } > = {
17+ "Kvasir-Seg" : {
18+ title : "Kvasir-SEG" ,
19+ description :
20+ "Trained on 1,000 annotated colonoscopy images covering a wide variety of polyp shapes, sizes, and textures. Best choice for general-purpose polyp detection in standard colonoscopy footage." ,
21+ } ,
22+ "BKAI-IGH" : {
23+ title : "BKAI-IGH" ,
24+ description :
25+ "Trained on a clinically diverse dataset that distinguishes between neoplastic and non-neoplastic polyp categories. Recommended when finer discrimination between polyp types is needed." ,
26+ } ,
27+ } ;
1128
1229function validate ( file : File ) : string | null {
1330 if ( ! ALLOWED_TYPES . includes ( file . type ) )
@@ -43,21 +60,16 @@ function Overlay({ originalURL, maskURL }: { originalURL: string; maskURL: strin
4360 const tryDraw = ( ) => {
4461 loaded ++ ;
4562 if ( loaded < 2 ) return ;
46-
4763 canvas . width = 256 ;
4864 canvas . height = 256 ;
49-
5065 ctx . drawImage ( orig , 0 , 0 , 256 , 256 ) ;
51-
5266 const off = document . createElement ( "canvas" ) ;
5367 off . width = 256 ;
5468 off . height = 256 ;
5569 const octx = off . getContext ( "2d" ) ! ;
5670 octx . drawImage ( mask , 0 , 0 , 256 , 256 ) ;
57-
5871 const maskPx = octx . getImageData ( 0 , 0 , 256 , 256 ) ;
5972 const base = ctx . getImageData ( 0 , 0 , 256 , 256 ) ;
60-
6173 for ( let i = 0 ; i < maskPx . data . length ; i += 4 ) {
6274 if ( maskPx . data [ i ] > 128 ) {
6375 base . data [ i ] = Math . min ( 255 , base . data [ i ] + 90 ) ;
@@ -85,9 +97,10 @@ function Overlay({ originalURL, maskURL }: { originalURL: string; maskURL: strin
8597export default function App ( ) {
8698 const [ state , setState ] = useState < State > ( { stage : "idle" } ) ;
8799 const [ dragOver , setDragOver ] = useState ( false ) ;
100+ const [ selectedModel , setSelectedModel ] = useState < ModelName > ( "Kvasir-Seg" ) ;
88101 const inputRef = useRef < HTMLInputElement > ( null ) ;
89102
90- const run = useCallback ( async ( file : File ) => {
103+ const run = useCallback ( async ( file : File , model : ModelName ) => {
91104 const err = validate ( file ) ;
92105 if ( err ) { setState ( { stage : "error" , message : err } ) ; return ; }
93106
@@ -98,29 +111,32 @@ export default function App() {
98111 form . append ( "file" , file ) ;
99112
100113 try {
101- const resp = await fetch ( "/predict" , { method : "POST" , body : form } ) ;
114+ const resp = await fetch ( `${ API_BASE } /predict?model=${ encodeURIComponent ( model ) } ` , {
115+ method : "POST" ,
116+ body : form ,
117+ } ) ;
102118 if ( ! resp . ok ) {
103119 const body = await resp . json ( ) . catch ( ( ) => ( { } ) ) as { detail ?: string } ;
104120 throw new Error ( body . detail ?? `Server error (${ resp . status } )` ) ;
105121 }
106122 const data = await resp . json ( ) as { mask : string } ;
107123 const maskURL = "data:image/png;base64," + data . mask ;
108- setState ( { stage : "result" , originalURL, maskURL } ) ;
124+ setState ( { stage : "result" , originalURL, maskURL, model } ) ;
109125 } catch ( e ) {
110126 setState ( { stage : "error" , message : ( e as Error ) . message } ) ;
111127 }
112128 } , [ ] ) ;
113129
114130 const onFileChange = ( e : ChangeEvent < HTMLInputElement > ) => {
115131 const file = e . target . files ?. [ 0 ] ;
116- if ( file ) run ( file ) ;
132+ if ( file ) run ( file , selectedModel ) ;
117133 } ;
118134
119135 const onDrop = ( e : DragEvent < HTMLDivElement > ) => {
120136 e . preventDefault ( ) ;
121137 setDragOver ( false ) ;
122138 const file = e . dataTransfer . files [ 0 ] ;
123- if ( file ) run ( file ) ;
139+ if ( file ) run ( file , selectedModel ) ;
124140 } ;
125141
126142 const reset = ( ) => {
@@ -131,7 +147,6 @@ export default function App() {
131147 return (
132148 < div style = { { display : "flex" , flexDirection : "column" , minHeight : "100vh" } } >
133149
134- { /* Header */ }
135150 < header style = { {
136151 background : "#fff" ,
137152 borderBottom : "1px solid var(--border)" ,
@@ -166,10 +181,8 @@ export default function App() {
166181 </ div >
167182 </ header >
168183
169- { /* Main */ }
170184 < main style = { { flex : 1 , maxWidth : 960 , width : "100%" , margin : "0 auto" , padding : "40px 24px" , display : "flex" , flexDirection : "column" , gap : 24 } } >
171185
172- { /* Notice */ }
173186 < div style = { {
174187 background : "var(--accent-light)" ,
175188 border : "1px solid #bfdbfe" ,
@@ -179,9 +192,7 @@ export default function App() {
179192 color : "#1e40af" ,
180193 lineHeight : 1.65 ,
181194 } } >
182- < strong style = { { display : "block" , marginBottom : 6 , fontSize : "0.9rem" } } >
183- What images to upload
184- </ strong >
195+ < strong style = { { display : "block" , marginBottom : 6 , fontSize : "0.9rem" } } > What images to upload</ strong >
185196 This model is trained on < strong > colonoscopy and endoscopy images</ strong > for detecting colorectal polyps.
186197 Upload only images captured by a colonoscope showing the inner colon wall.
187198 < ul style = { { paddingLeft : 18 , marginTop : 8 , display : "flex" , flexDirection : "column" , gap : 4 } } >
@@ -192,7 +203,52 @@ export default function App() {
192203 </ ul >
193204 </ div >
194205
195- { /* Upload zone — hidden when result is shown */ }
206+ < div >
207+ < div style = { { fontWeight : 700 , fontSize : "0.9rem" , marginBottom : 12 } } > Select Model</ div >
208+ < div style = { { display : "grid" , gridTemplateColumns : "1fr 1fr" , gap : 12 } } >
209+ { ( Object . keys ( MODEL_INFO ) as ModelName [ ] ) . map ( ( key ) => {
210+ const active = selectedModel === key ;
211+ return (
212+ < button
213+ key = { key }
214+ onClick = { ( ) => setSelectedModel ( key ) }
215+ style = { {
216+ background : active ? "var(--accent-light)" : "#fff" ,
217+ border : `2px solid ${ active ? "var(--accent)" : "var(--border)" } ` ,
218+ borderRadius : "var(--radius)" ,
219+ padding : "14px 16px" ,
220+ textAlign : "left" ,
221+ cursor : "pointer" ,
222+ transition : "border-color 0.15s, background 0.15s" ,
223+ } }
224+ >
225+ < div style = { {
226+ fontWeight : 700 ,
227+ fontSize : "0.9rem" ,
228+ color : active ? "var(--accent)" : "var(--text)" ,
229+ marginBottom : 6 ,
230+ display : "flex" ,
231+ alignItems : "center" ,
232+ gap : 8 ,
233+ } } >
234+ < span style = { {
235+ width : 14 , height : 14 , borderRadius : "50%" ,
236+ border : `2px solid ${ active ? "var(--accent)" : "var(--border)" } ` ,
237+ background : active ? "var(--accent)" : "transparent" ,
238+ display : "inline-block" ,
239+ flexShrink : 0 ,
240+ } } />
241+ { MODEL_INFO [ key ] . title }
242+ </ div >
243+ < div style = { { fontSize : "0.8rem" , color : "var(--muted)" , lineHeight : 1.55 } } >
244+ { MODEL_INFO [ key ] . description }
245+ </ div >
246+ </ button >
247+ ) ;
248+ } ) }
249+ </ div >
250+ </ div >
251+
196252 { state . stage !== "result" && (
197253 < div
198254 onClick = { ( ) => inputRef . current ?. click ( ) }
@@ -244,7 +300,6 @@ export default function App() {
244300 </ div >
245301 ) }
246302
247- { /* Error */ }
248303 { state . stage === "error" && (
249304 < div style = { {
250305 background : "var(--red-light)" ,
@@ -259,7 +314,6 @@ export default function App() {
259314 </ div >
260315 ) }
261316
262- { /* Loader */ }
263317 { state . stage === "loading" && (
264318 < div style = { {
265319 display : "flex" , alignItems : "center" , justifyContent : "center" ,
@@ -275,15 +329,19 @@ export default function App() {
275329 borderRadius : "50%" ,
276330 animation : "spin 0.7s linear infinite" ,
277331 } } />
278- Running polyp segmentation…
332+ Running segmentation with { MODEL_INFO [ selectedModel ] . title } …
279333 </ div >
280334 ) }
281335
282- { /* Results */ }
283336 { state . stage === "result" && (
284337 < div style = { { display : "flex" , flexDirection : "column" , gap : 16 } } >
285- < div style = { { display : "flex" , alignItems : "center" , justifyContent : "space-between" } } >
286- < div style = { { fontWeight : 700 , fontSize : "1.05rem" } } > Segmentation Results</ div >
338+ < div style = { { display : "flex" , alignItems : "center" , justifyContent : "space-between" , flexWrap : "wrap" , gap : 10 } } >
339+ < div >
340+ < div style = { { fontWeight : 700 , fontSize : "1.05rem" } } > Segmentation Results</ div >
341+ < div style = { { fontSize : "0.78rem" , color : "var(--muted)" , marginTop : 2 } } >
342+ Model: { MODEL_INFO [ state . model ] . title }
343+ </ div >
344+ </ div >
287345 < button
288346 onClick = { reset }
289347 style = { {
0 commit comments