Skip to content

Commit 2a48f66

Browse files
committed
chore: add unified error handling banner in all demo apps
1 parent 380e7d9 commit 2a48f66

File tree

16 files changed

+387
-65
lines changed

16 files changed

+387
-65
lines changed

apps/computer-vision/app/object_detection/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ const MODELS: ModelOption<ObjectDetectionModelSources>[] = [
2020
{ label: 'RF-DeTR Nano', value: RF_DETR_NANO },
2121
{ label: 'SSDLite MobileNet', value: SSDLITE_320_MOBILENET_V3_LARGE },
2222
];
23+
import ErrorBanner from '../../components/ErrorBanner';
2324

2425
export default function ObjectDetectionScreen() {
2526
const [imageUri, setImageUri] = useState('');
2627
const [results, setResults] = useState<Detection[]>([]);
28+
const [error, setError] = useState<string | null>(null);
2729
const [imageDimensions, setImageDimensions] = useState<{
2830
width: number;
2931
height: number;
@@ -38,6 +40,10 @@ export default function ObjectDetectionScreen() {
3840
setGlobalGenerating(model.isGenerating);
3941
}, [model.isGenerating, setGlobalGenerating]);
4042

43+
useEffect(() => {
44+
if (model.error) setError(String(model.error));
45+
}, [model.error]);
46+
4147
const handleCameraPress = async (isCamera: boolean) => {
4248
const image = await getImage(isCamera);
4349
const uri = image?.uri;
@@ -60,7 +66,7 @@ export default function ObjectDetectionScreen() {
6066
setInferenceTime(Date.now() - start);
6167
setResults(output);
6268
} catch (e) {
63-
console.error(e);
69+
setError(e instanceof Error ? e.message : String(e));
6470
}
6571
}
6672
};
@@ -76,6 +82,7 @@ export default function ObjectDetectionScreen() {
7682

7783
return (
7884
<ScreenWrapper>
85+
<ErrorBanner message={error} onDismiss={() => setError(null)} />
7986
<View style={styles.imageContainer}>
8087
<View style={styles.image}>
8188
{imageUri && imageDimensions?.width && imageDimensions?.height ? (

apps/computer-vision/app/ocr/index.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ const MODELS: ModelOption<OCRModelSources>[] = [
3131
{ label: 'Japanese', value: OCR_JAPANESE },
3232
{ label: 'Korean', value: OCR_KOREAN },
3333
];
34+
import ErrorBanner from '../../components/ErrorBanner';
3435

3536
export default function OCRScreen() {
3637
const [imageUri, setImageUri] = useState('');
3738
const [results, setResults] = useState<any[]>([]);
39+
const [error, setError] = useState<string | null>(null);
3840
const [imageDimensions, setImageDimensions] = useState<{
3941
width: number;
4042
height: number;
@@ -51,6 +53,10 @@ export default function OCRScreen() {
5153
setGlobalGenerating(model.isGenerating);
5254
}, [model.isGenerating, setGlobalGenerating]);
5355

56+
useEffect(() => {
57+
if (model.error) setError(String(model.error));
58+
}, [model.error]);
59+
5460
const handleCameraPress = async (isCamera: boolean) => {
5561
const image = await getImage(isCamera);
5662
const width = image?.width;
@@ -71,21 +77,22 @@ export default function OCRScreen() {
7177
setInferenceTime(Date.now() - start);
7278
setResults(output);
7379
} catch (e) {
74-
console.error(e);
80+
setError(e instanceof Error ? e.message : String(e));
7581
}
7682
};
7783

78-
if (!model.isReady) {
84+
if (!model.isReady && !model.error) {
7985
return (
8086
<Spinner
81-
visible={!model.isReady}
87+
visible={true}
8288
textContent={`Loading the model ${(model.downloadProgress * 100).toFixed(0)} %`}
8389
/>
8490
);
8591
}
8692

8793
return (
8894
<ScreenWrapper>
95+
<ErrorBanner message={error} onDismiss={() => setError(null)} />
8996
<View style={styles.container}>
9097
<View style={styles.imageContainer}>
9198
{imageUri && imageDimensions?.width && imageDimensions?.height ? (

apps/computer-vision/app/semantic_segmentation/index.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import React, { useContext, useEffect, useState } from 'react';
2525
import { GeneratingContext } from '../../context';
2626
import ScreenWrapper from '../../ScreenWrapper';
2727
import { StatsBar } from '../../components/StatsBar';
28+
import ErrorBanner from '../../components/ErrorBanner';
2829

2930
const numberToColor: number[][] = [
3031
[255, 87, 51], // 0 Red
@@ -69,19 +70,24 @@ export default function SemanticSegmentationScreen() {
6970
DEEPLAB_V3_MOBILENET_V3_LARGE_QUANTIZED
7071
);
7172

72-
const { isReady, isGenerating, downloadProgress, forward } =
73+
const { isReady, isGenerating, downloadProgress, forward, error: modelError } =
7374
useSemanticSegmentation({ model: selectedModel });
7475

7576
const [imageUri, setImageUri] = useState('');
7677
const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
7778
const [segImage, setSegImage] = useState<SkImage | null>(null);
7879
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
7980
const [inferenceTime, setInferenceTime] = useState<number | null>(null);
81+
const [error, setError] = useState<string | null>(null);
8082

8183
useEffect(() => {
8284
setGlobalGenerating(isGenerating);
8385
}, [isGenerating, setGlobalGenerating]);
8486

87+
useEffect(() => {
88+
if (modelError) setError(String(modelError));
89+
}, [modelError]);
90+
8591
const handleCameraPress = async (isCamera: boolean) => {
8692
const image = await getImage(isCamera);
8793
if (!image?.uri) return;
@@ -125,21 +131,22 @@ export default function SemanticSegmentationScreen() {
125131
setSegImage(img);
126132
setInferenceTime(Date.now() - start);
127133
} catch (e) {
128-
console.error(e);
134+
setError(e instanceof Error ? e.message : String(e));
129135
}
130136
};
131137

132-
if (!isReady) {
138+
if (!isReady && !modelError) {
133139
return (
134140
<Spinner
135-
visible={!isReady}
141+
visible={true}
136142
textContent={`Loading the model ${(downloadProgress * 100).toFixed(0)} %`}
137143
/>
138144
);
139145
}
140146

141147
return (
142148
<ScreenWrapper>
149+
<ErrorBanner message={error} onDismiss={() => setError(null)} />
143150
<View style={styles.imageCanvasContainer}>
144151
<View style={styles.imageContainer}>
145152
<Image

apps/computer-vision/app/style_transfer/index.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const MODELS: ModelOption<StyleTransferModelSources>[] = [
2929
{ label: 'Rain Princess', value: STYLE_TRANSFER_RAIN_PRINCESS_QUANTIZED },
3030
{ label: 'Udnie', value: STYLE_TRANSFER_UDNIE_QUANTIZED },
3131
];
32+
import ErrorBanner from '../../components/ErrorBanner';
3233

3334
export default function StyleTransferScreen() {
3435
const [selectedModel, setSelectedModel] = useState<StyleTransferModelSources>(
@@ -41,9 +42,14 @@ export default function StyleTransferScreen() {
4142
setGlobalGenerating(model.isGenerating);
4243
}, [model.isGenerating, setGlobalGenerating]);
4344

45+
useEffect(() => {
46+
if (model.error) setError(String(model.error));
47+
}, [model.error]);
48+
4449
const [imageUri, setImageUri] = useState('');
4550
const [styledUri, setStyledUri] = useState('');
4651
const [inferenceTime, setInferenceTime] = useState<number | null>(null);
52+
const [error, setError] = useState<string | null>(null);
4753

4854
const handleCameraPress = async (isCamera: boolean) => {
4955
const image = await getImage(isCamera);
@@ -63,22 +69,23 @@ export default function StyleTransferScreen() {
6369
setInferenceTime(Date.now() - start);
6470
setStyledUri(uri);
6571
} catch (e) {
66-
console.error(e);
72+
setError(e instanceof Error ? e.message : String(e));
6773
}
6874
}
6975
};
7076

71-
if (!model.isReady) {
77+
if (!model.isReady && !model.error) {
7278
return (
7379
<Spinner
74-
visible={!model.isReady}
80+
visible={true}
7581
textContent={`Loading the model ${(model.downloadProgress * 100).toFixed(0)} %`}
7682
/>
7783
);
7884
}
7985

8086
return (
8187
<ScreenWrapper>
88+
<ErrorBanner message={error} onDismiss={() => setError(null)} />
8289
<View style={styles.imageContainer}>
8390
<Image
8491
style={styles.image}

apps/computer-vision/app/text_to_image/index.tsx

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import ColorPalette from '../../colors';
2424
import ProgressBar from '../../components/ProgressBar';
2525
import { Ionicons } from '@expo/vector-icons';
2626
import { StatsBar } from '../../components/StatsBar';
27+
import ErrorBanner from '../../components/ErrorBanner';
2728

2829
type TextToImageModelSources = TextToImageProps['model'];
2930

@@ -36,12 +37,18 @@ export default function TextToImageScreen() {
3637
const [inferenceStepIdx, setInferenceStepIdx] = useState<number>(0);
3738
const [image, setImage] = useState<string | null>(null);
3839
const [steps, setSteps] = useState<number>(40);
40+
3941
const [input, setInput] = useState('');
4042
const [selectedModel, setSelectedModel] = useState<TextToImageModelSources>(
4143
BK_SDM_TINY_VPRED_256
4244
);
4345
const [generationTime, setGenerationTime] = useState<number | null>(null);
4446

47+
const [showTextInput, setShowTextInput] = useState(false);
48+
const [keyboardVisible, setKeyboardVisible] = useState(false);
49+
const [error, setError] = useState<string | null>(null);
50+
const [imageTitle, setImageTitle] = useState<string | null>(null);
51+
4552
const imageSize = 224;
4653
const model = useTextToImage({
4754
model: selectedModel,
@@ -54,8 +61,28 @@ export default function TextToImageScreen() {
5461
setGlobalGenerating(model.isGenerating);
5562
}, [model.isGenerating, setGlobalGenerating]);
5663

64+
useEffect(() => {
65+
if (model.error) setError(String(model.error));
66+
}, [model.error]);
67+
68+
useEffect(() => {
69+
const showSub = Keyboard.addListener('keyboardDidShow', () => {
70+
setKeyboardVisible(true);
71+
});
72+
const hideSub = Keyboard.addListener('keyboardDidHide', () => {
73+
setKeyboardVisible(false);
74+
});
75+
return () => {
76+
showSub.remove();
77+
hideSub.remove();
78+
};
79+
}, []);
80+
5781
const runForward = async () => {
5882
if (!input.trim()) return;
83+
84+
setImageTitle(input);
85+
5986
try {
6087
const start = Date.now();
6188
const output = await model.generate(input, imageSize, steps);
@@ -65,27 +92,41 @@ export default function TextToImageScreen() {
6592
setGenerationTime(Date.now() - start);
6693
}
6794
} catch (e) {
68-
console.error(e);
95+
setError(e instanceof Error ? e.message : String(e));
96+
setImageTitle(null);
6997
} finally {
7098
setInferenceStepIdx(0);
7199
}
72100
};
73101

74-
if (!model.isReady) {
102+
if (!model.isReady && !model.error) {
75103
return (
76104
<Spinner
77-
visible={!model.isReady}
105+
visible={true}
78106
textContent={`Loading the model ${(model.downloadProgress * 100).toFixed(0)} %`}
79107
/>
80108
);
81109
}
82110

83111
return (
84-
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
112+
<TouchableWithoutFeedback
113+
onPress={() => {
114+
Keyboard.dismiss();
115+
setShowTextInput(false);
116+
}}
117+
>
85118
<KeyboardAvoidingView
86119
style={styles.container}
87120
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
88121
>
122+
{keyboardVisible && <View style={styles.overlay} />}
123+
124+
<ErrorBanner message={error} onDismiss={() => setError(null)} />
125+
126+
<View style={styles.titleContainer}>
127+
{imageTitle && <Text style={styles.titleText}>{imageTitle}</Text>}
128+
</View>
129+
89130
<View style={styles.imageContainer}>
90131
{model.isGenerating ? (
91132
<View style={styles.progressContainer}>
@@ -171,11 +212,28 @@ const styles = StyleSheet.create({
171212
flex: 1,
172213
width: '100%',
173214
},
215+
overlay: {
216+
...StyleSheet.absoluteFillObject,
217+
backgroundColor: 'rgba(0,0,0,0.1)',
218+
zIndex: 1,
219+
},
220+
titleContainer: {
221+
paddingHorizontal: 16,
222+
paddingTop: 8,
223+
alignItems: 'center',
224+
},
225+
titleText: {
226+
fontSize: 18,
227+
fontWeight: '600',
228+
color: '#333',
229+
textAlign: 'center',
230+
},
174231
imageContainer: {
175232
flex: 1,
176233
alignItems: 'center',
177234
justifyContent: 'center',
178235
padding: 16,
236+
zIndex: 0,
179237
},
180238
image: {
181239
width: 256,
@@ -238,4 +296,4 @@ const styles = StyleSheet.create({
238296
alignItems: 'center',
239297
justifyContent: 'center',
240298
},
241-
});
299+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
3+
4+
interface ErrorBannerProps {
5+
message: string | null;
6+
onDismiss: () => void;
7+
}
8+
9+
export default function ErrorBanner({ message, onDismiss }: ErrorBannerProps) {
10+
if (!message) return null;
11+
12+
return (
13+
<View style={styles.container}>
14+
<Text style={styles.message} numberOfLines={3}>
15+
{message}
16+
</Text>
17+
<TouchableOpacity onPress={onDismiss} style={styles.closeButton}>
18+
<Text style={styles.closeText}></Text>
19+
</TouchableOpacity>
20+
</View>
21+
);
22+
}
23+
24+
const styles = StyleSheet.create({
25+
container: {
26+
backgroundColor: '#FEE2E2',
27+
borderLeftWidth: 4,
28+
borderLeftColor: '#EF4444',
29+
borderRadius: 8,
30+
marginHorizontal: 16,
31+
marginVertical: 8,
32+
paddingVertical: 10,
33+
paddingLeft: 12,
34+
paddingRight: 8,
35+
flexDirection: 'row',
36+
alignItems: 'center',
37+
},
38+
message: {
39+
flex: 1,
40+
color: '#991B1B',
41+
fontSize: 14,
42+
lineHeight: 20,
43+
},
44+
closeButton: {
45+
padding: 4,
46+
marginLeft: 8,
47+
},
48+
closeText: {
49+
color: '#991B1B',
50+
fontSize: 16,
51+
fontWeight: '600',
52+
},
53+
});

0 commit comments

Comments
 (0)