Skip to content

Commit 27592da

Browse files
authored
chore: Add unified error handling banner in all demo apps (#981)
## Description This PR adds unified error banner that shows the error details. ### Introduces a breaking change? - [ ] Yes - [x] No ### Type of change - [ ] Bug fix (change which fixes an issue) - [ ] New feature (change which adds functionality) - [ ] Documentation update (improves or adds clarity to existing documentation) - [x] Other (chores, tests, code style improvements etc.) ### Tested on - [x] iOS - [x] Android ### Testing instructions - [ ] Run demo apps and try to cause errors that propagates from our hooks / models (not some random errors). ### Screenshots Banners should look like this: <img width="1080" height="2048" alt="image" src="https://github.com/user-attachments/assets/9dd894ce-22d9-4705-8f88-919aebcc34f6" /> ### Related issues Closes #225 ### Checklist - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have updated the documentation accordingly - [x] My changes generate no new warnings ### Additional notes <!-- Include any additional information, assumptions, or context that reviewers might need to understand this PR. -->
1 parent 87c65bb commit 27592da

File tree

28 files changed

+554
-94
lines changed

28 files changed

+554
-94
lines changed

apps/bare_rn/App.tsx

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,64 @@ const spinnerStyles = StyleSheet.create({
8383
},
8484
});
8585

86+
function ErrorBanner({
87+
message,
88+
onDismiss,
89+
}: {
90+
message: string | null;
91+
onDismiss: () => void;
92+
}) {
93+
if (!message) return null;
94+
return (
95+
<View style={errorBannerStyles.container}>
96+
<Text style={errorBannerStyles.message} numberOfLines={3}>
97+
{message}
98+
</Text>
99+
<TouchableOpacity
100+
onPress={onDismiss}
101+
style={errorBannerStyles.closeButton}
102+
>
103+
<Text style={errorBannerStyles.closeText}></Text>
104+
</TouchableOpacity>
105+
</View>
106+
);
107+
}
108+
109+
const errorBannerStyles = StyleSheet.create({
110+
container: {
111+
backgroundColor: '#FEE2E2',
112+
borderLeftWidth: 4,
113+
borderLeftColor: '#EF4444',
114+
borderRadius: 8,
115+
marginHorizontal: 16,
116+
marginVertical: 8,
117+
paddingVertical: 10,
118+
paddingLeft: 12,
119+
paddingRight: 8,
120+
flexDirection: 'row',
121+
alignItems: 'center',
122+
},
123+
message: {
124+
flex: 1,
125+
color: '#991B1B',
126+
fontSize: 14,
127+
lineHeight: 20,
128+
},
129+
closeButton: {
130+
padding: 4,
131+
marginLeft: 8,
132+
},
133+
closeText: {
134+
color: '#991B1B',
135+
fontSize: 16,
136+
fontWeight: '600',
137+
},
138+
});
139+
86140
function App() {
87141
const [userInput, setUserInput] = useState('');
88142
const [isTextInputFocused, setIsTextInputFocused] = useState(false);
143+
const [error, setError] = useState<string | null>(null);
89144
const textInputRef = useRef<TextInput>(null);
90145
const scrollViewRef = useRef<ScrollView>(null);
91146

@@ -98,9 +153,7 @@ function App() {
98153
// } });
99154

100155
useEffect(() => {
101-
if (llm.error) {
102-
console.log('LLM error:', llm.error);
103-
}
156+
if (llm.error) setError(String(llm.error));
104157
}, [llm.error]);
105158

106159
const sendMessage = async () => {
@@ -111,7 +164,7 @@ function App() {
111164
try {
112165
await llm.sendMessage(userInput);
113166
} catch (e) {
114-
console.error(e);
167+
setError(e instanceof Error ? e.message : String(e));
115168
}
116169
};
117170

@@ -123,11 +176,12 @@ function App() {
123176
keyboardVerticalOffset={Platform.OS === 'ios' ? 100 : 0}
124177
>
125178
<Spinner
126-
visible={!llm.isReady}
179+
visible={!llm.isReady && !llm.error}
127180
textContent={`Loading model ${(llm.downloadProgress * 100).toFixed(0)}%`}
128181
/>
129182

130183
<SafeAreaView style={styles.content}>
184+
<ErrorBanner message={error} onDismiss={() => setError(null)} />
131185
{llm.messageHistory.length > 0 || llm.isGenerating ? (
132186
<ScrollView
133187
ref={scrollViewRef}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, { useContext, useEffect, useState } from 'react';
1010
import { GeneratingContext } from '../../context';
1111
import ScreenWrapper from '../../ScreenWrapper';
1212
import { StatsBar } from '../../components/StatsBar';
13+
import ErrorBanner from '../../components/ErrorBanner';
1314

1415
export default function ClassificationScreen() {
1516
const [results, setResults] = useState<{ label: string; score: number }[]>(
@@ -18,12 +19,19 @@ export default function ClassificationScreen() {
1819
const [imageUri, setImageUri] = useState('');
1920
const [inferenceTime, setInferenceTime] = useState<number | null>(null);
2021

22+
const [error, setError] = useState<string | null>(null);
23+
2124
const model = useClassification({ model: EFFICIENTNET_V2_S_QUANTIZED });
2225
const { setGlobalGenerating } = useContext(GeneratingContext);
26+
2327
useEffect(() => {
2428
setGlobalGenerating(model.isGenerating);
2529
}, [model.isGenerating, setGlobalGenerating]);
2630

31+
useEffect(() => {
32+
if (model.error) setError(String(model.error));
33+
}, [model.error]);
34+
2735
const handleCameraPress = async (isCamera: boolean) => {
2836
const image = await getImage(isCamera);
2937
const uri = image?.uri;
@@ -46,21 +54,24 @@ export default function ClassificationScreen() {
4654
.map(([label, score]) => ({ label, score: score as number }));
4755
setResults(top10);
4856
} catch (e) {
49-
console.error(e);
57+
setError(e instanceof Error ? e.message : String(e));
5058
}
5159
}
5260
};
5361

54-
if (!model.isReady) {
62+
if (!model.isReady && !model.error) {
5563
return (
5664
<Spinner
57-
visible={!model.isReady}
65+
visible={true}
5866
textContent={`Loading the model ${(model.downloadProgress * 100).toFixed(0)} %`}
5967
/>
6068
);
6169
}
70+
6271
return (
6372
<ScreenWrapper>
73+
<ErrorBanner message={error} onDismiss={() => setError(null)} />
74+
6475
<View style={styles.imageContainer}>
6576
<Image
6677
style={styles.image}

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/ocr_vertical/index.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,34 @@ import React, { useContext, useEffect, useState } from 'react';
88
import { GeneratingContext } from '../../context';
99
import ScreenWrapper from '../../ScreenWrapper';
1010
import { StatsBar } from '../../components/StatsBar';
11+
import ErrorBanner from '../../components/ErrorBanner';
1112

12-
export default function VerticalOCRScree() {
13+
export default function VerticalOCRScreen() {
1314
const [imageUri, setImageUri] = useState('');
1415
const [results, setResults] = useState<any[]>([]);
1516
const [imageDimensions, setImageDimensions] = useState<{
1617
width: number;
1718
height: number;
1819
}>();
1920
const [inferenceTime, setInferenceTime] = useState<number | null>(null);
21+
22+
const [error, setError] = useState<string | null>(null);
23+
2024
const model = useVerticalOCR({
2125
model: OCR_ENGLISH,
2226
independentCharacters: true,
2327
});
28+
2429
const { setGlobalGenerating } = useContext(GeneratingContext);
30+
2531
useEffect(() => {
2632
setGlobalGenerating(model.isGenerating);
2733
}, [model.isGenerating, setGlobalGenerating]);
2834

35+
useEffect(() => {
36+
if (model.error) setError(String(model.error));
37+
}, [model.error]);
38+
2939
const handleCameraPress = async (isCamera: boolean) => {
3040
const image = await getImage(isCamera);
3141
const width = image?.width;
@@ -46,14 +56,14 @@ export default function VerticalOCRScree() {
4656
setInferenceTime(Date.now() - start);
4757
setResults(output);
4858
} catch (e) {
49-
console.error(e);
59+
setError(e instanceof Error ? e.message : String(e));
5060
}
5161
};
5262

53-
if (!model.isReady) {
63+
if (!model.isReady && !model.error) {
5464
return (
5565
<Spinner
56-
visible={!model.isReady}
66+
visible={true}
5767
textContent={`Loading the model ${(model.downloadProgress * 100).toFixed(0)} %`}
5868
/>
5969
);
@@ -62,6 +72,8 @@ export default function VerticalOCRScree() {
6272
return (
6373
<ScreenWrapper>
6474
<View style={styles.container}>
75+
<ErrorBanner message={error} onDismiss={() => setError(null)} />
76+
6577
<View style={styles.imageContainer}>
6678
{imageUri && imageDimensions?.width && imageDimensions?.height ? (
6779
<ImageWithBboxes2
@@ -87,7 +99,7 @@ export default function VerticalOCRScree() {
8799
{results.map(({ text, score }, index) => (
88100
<View key={index} style={styles.resultRecord}>
89101
<Text style={styles.resultLabel}>{text}</Text>
90-
<Text>{score.toFixed(3)}</Text>
102+
<Text>{score?.toFixed(3)}</Text>
91103
</View>
92104
))}
93105
</ScrollView>

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

0 commit comments

Comments
 (0)