Skip to content

Commit 0f6508a

Browse files
committed
feat(error-dialog): implement processing error dialog with traceback and context display
1 parent a44b1c5 commit 0f6508a

4 files changed

Lines changed: 223 additions & 11 deletions

File tree

‎src/App.tsx‎

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ import {
8686
} from "./utils/consoleLog";
8787
import { DialogBridge } from "./components/DialogBridge";
8888
import { t } from "./i18n/translate";
89-
import { useConfirm, useMessage, usePrompt } from "./components/ConfirmDialog";
89+
import {
90+
useConfirm,
91+
useMessage,
92+
useProcessingError,
93+
usePrompt,
94+
} from "./components/ConfirmDialog";
9095
import { EdgeSlowLoadHint } from "./components/EdgeSlowLoadHint";
9196
import { useProgress } from "./components/ProgressDialog";
9297
import {
@@ -333,6 +338,7 @@ export default function App() {
333338
const workspace = useWorkspace();
334339
const confirm = useConfirm();
335340
const notify = useMessage();
341+
const showProcessingError = useProcessingError();
336342
const prompt = usePrompt();
337343
const runWithProgress = useProgress();
338344
// Whether the metadata clipboard holds a payload "Paste metadata" can
@@ -1553,11 +1559,22 @@ export default function App() {
15531559
} else {
15541560
await refresh(lastId);
15551561
}
1562+
} catch (err) {
1563+
// A computation raising in Sigima/Pyodide (e.g. gamma correction on
1564+
// an image with negative values) is a *processing* problem, not a
1565+
// crash: surface the full traceback in a copy-pasteable modal
1566+
// (mirroring DataLab desktop) so the user understands their
1567+
// request could not be honoured. The traceback is also logged to
1568+
// the browser console by the runtime.
1569+
await showProcessingError({
1570+
context: t("Computing: {feature}", { feature: feature.label }),
1571+
traceback: err instanceof Error ? err.message : String(err),
1572+
});
15561573
} finally {
15571574
setBusy(false);
15581575
}
15591576
},
1560-
[runtime, refresh, refreshPanelKind],
1577+
[runtime, refresh, refreshPanelKind, showProcessingError],
15611578
);
15621579

15631580
const refreshPluginActions = useCallback(async () => {
@@ -1586,12 +1603,26 @@ export default function App() {
15861603
);
15871604
}
15881605
} catch (err) {
1589-
console.error("[plugins] action failed", err);
1606+
await showProcessingError({
1607+
context: t("Plugin action: {action}", {
1608+
action:
1609+
pluginActions.find((a) => a.action_id === actionId)?.title ??
1610+
actionId,
1611+
}),
1612+
traceback: err instanceof Error ? err.message : String(err),
1613+
});
15901614
} finally {
15911615
setBusy(false);
15921616
}
15931617
},
1594-
[runtime, refresh, refreshPanelKind, treeKind],
1618+
[
1619+
runtime,
1620+
refresh,
1621+
refreshPanelKind,
1622+
treeKind,
1623+
pluginActions,
1624+
showProcessingError,
1625+
],
15951626
);
15961627

15971628
const handleReloadPlugins = useCallback(async () => {
@@ -1804,16 +1835,27 @@ export default function App() {
18041835
}
18051836
}
18061837
} catch (err) {
1807-
const message = err instanceof Error ? err.message : String(err);
1808-
await notify({
1809-
kind: "error",
1810-
message: `Analysis failed: ${message}`,
1838+
const catalog =
1839+
treeKind === "image" ? imageAnalysisEntries : analysisEntries;
1840+
const label = catalog.find((e) => e.id === funcId)?.label ?? funcId;
1841+
await showProcessingError({
1842+
context: t("Computing: {feature}", { feature: label }),
1843+
traceback: err instanceof Error ? err.message : String(err),
18111844
});
18121845
} finally {
18131846
setBusy(false);
18141847
}
18151848
},
1816-
[runtime, refreshResults, treeKind, notify, currentId],
1849+
[
1850+
runtime,
1851+
refreshResults,
1852+
treeKind,
1853+
notify,
1854+
showProcessingError,
1855+
currentId,
1856+
analysisEntries,
1857+
imageAnalysisEntries,
1858+
],
18171859
);
18181860

18191861
const handleFreeMemory = useCallback(async () => {

‎src/components/ConfirmDialog.tsx‎

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,96 @@ export function MessageDialog(props: MessageDialogProps) {
143143
);
144144
}
145145

146+
// ---------------------------------------------------------------------
147+
// Processing error (rich, copy-pasteable traceback + tip)
148+
// ---------------------------------------------------------------------
149+
150+
export interface ProcessingErrorOptions {
151+
/** Context line, e.g. ``"Computing: Gamma correction"``. */
152+
context: string;
153+
/** Full Python traceback, shown verbatim and copy-pasteable. */
154+
traceback: string;
155+
/** Optional override of the default computation tip. */
156+
tip?: string;
157+
/** Optional dialog heading; defaults to ``"Error"``. */
158+
title?: string;
159+
}
160+
161+
interface PendingProcessingError extends ProcessingErrorOptions {
162+
resolve: () => void;
163+
}
164+
165+
interface ProcessingErrorDialogProps {
166+
options: ProcessingErrorOptions;
167+
onClose: () => void;
168+
}
169+
170+
/** Pure presentational error dialog mirroring DataLab desktop's
171+
* ``WarningErrorMessageBox``: a context line, the full traceback in a
172+
* selectable / copyable block, and a tip explaining that computation
173+
* errors are usually caused by the data or parameters. */
174+
export function ProcessingErrorDialog(props: ProcessingErrorDialogProps) {
175+
const { options, onClose } = props;
176+
const { context, traceback, tip, title } = options;
177+
const [copied, setCopied] = useState(false);
178+
useEffect(() => {
179+
const handler = (e: KeyboardEvent) => {
180+
if (e.key === "Escape" || e.key === "Enter") onClose();
181+
};
182+
window.addEventListener("keydown", handler);
183+
return () => window.removeEventListener("keydown", handler);
184+
}, [onClose]);
185+
const handleCopy = async () => {
186+
try {
187+
await navigator.clipboard.writeText(traceback);
188+
setCopied(true);
189+
window.setTimeout(() => setCopied(false), 1500);
190+
} catch {
191+
// ignore clipboard failures (permission denied, etc.)
192+
}
193+
};
194+
const tipText =
195+
tip ??
196+
t(
197+
"DataLab relies on various libraries to perform the computation. During the computation, errors may occur because of the data (e.g. division by zero, unexpected data type, etc.) or because of the libraries (e.g. memory error, etc.). If you encounter an error, before reporting it, please ensure that the computation is correct, by checking the data and the parameters.",
198+
);
199+
return (
200+
<div className="overlay" role="dialog" aria-modal="true" onClick={onClose}>
201+
<div
202+
className="card processing-error-card"
203+
onClick={(e) => e.stopPropagation()}
204+
>
205+
<h2>{title || t("Error")}</h2>
206+
<div className="processing-error-context">
207+
<p>{t("An error has occurred during the following context:")}</p>
208+
<p>
209+
<strong>{context}</strong>
210+
</p>
211+
</div>
212+
<div className="processing-error-traceback">
213+
<div className="processing-error-traceback-head">
214+
<span>
215+
{t("The following traceback may help to understand the problem:")}
216+
</span>
217+
<button type="button" onClick={handleCopy}>
218+
{copied ? t("Copied") : t("Copy")}
219+
</button>
220+
</div>
221+
<pre>{traceback}</pre>
222+
</div>
223+
<div className="processing-error-tip">
224+
<p>{tipText}</p>
225+
</div>
226+
<div className="actions">
227+
<button onClick={onClose} autoFocus>
228+
{t("OK")}
229+
</button>
230+
</div>
231+
</div>
232+
</div>
233+
);
234+
}
235+
146236
// ---------------------------------------------------------------------
147237
// Prompt (single-line text input)
148238
// ---------------------------------------------------------------------
@@ -225,18 +315,21 @@ export function PromptDialog(props: PromptDialogProps) {
225315
type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;
226316
type MessageFn = (options: MessageOptions) => Promise<void>;
227317
type PromptFn = (options: PromptOptions) => Promise<string | null>;
318+
type ProcessingErrorFn = (options: ProcessingErrorOptions) => Promise<void>;
228319

229320
interface DialogContextValue {
230321
confirm: ConfirmFn;
231322
message: MessageFn;
232323
prompt: PromptFn;
324+
processingError: ProcessingErrorFn;
233325
}
234326

235327
const DialogContext = createContext<DialogContextValue | null>(null);
236328

237329
type AnyPending =
238330
| ({ _kind: "confirm" } & PendingConfirm)
239331
| ({ _kind: "message" } & PendingMessage)
332+
| ({ _kind: "processingError" } & PendingProcessingError)
240333
| ({ _kind: "prompt" } & PendingPrompt);
241334

242335
/**
@@ -261,15 +354,23 @@ export function DialogProvider(props: { children: ReactNode }) {
261354
setQueue((q) => [...q, { _kind: "message", ...options, resolve }]);
262355
});
263356
}, []);
357+
const processingError = useCallback<ProcessingErrorFn>((options) => {
358+
return new Promise<void>((resolve) => {
359+
setQueue((q) => [
360+
...q,
361+
{ _kind: "processingError", ...options, resolve },
362+
]);
363+
});
364+
}, []);
264365
const prompt = useCallback<PromptFn>((options) => {
265366
return new Promise<string | null>((resolve) => {
266367
setQueue((q) => [...q, { _kind: "prompt", ...options, resolve }]);
267368
});
268369
}, []);
269370

270371
const value = useMemo<DialogContextValue>(
271-
() => ({ confirm, message, prompt }),
272-
[confirm, message, prompt],
372+
() => ({ confirm, message, prompt, processingError }),
373+
[confirm, message, prompt, processingError],
273374
);
274375

275376
return (
@@ -293,6 +394,15 @@ export function DialogProvider(props: { children: ReactNode }) {
293394
}}
294395
/>
295396
)}
397+
{current?._kind === "processingError" && (
398+
<ProcessingErrorDialog
399+
options={current}
400+
onClose={() => {
401+
current.resolve();
402+
popQueue();
403+
}}
404+
/>
405+
)}
296406
{current?._kind === "prompt" && (
297407
<PromptDialog
298408
options={current}
@@ -322,6 +432,10 @@ export function useMessage(): MessageFn {
322432
return useDialogs().message;
323433
}
324434

435+
export function useProcessingError(): ProcessingErrorFn {
436+
return useDialogs().processingError;
437+
}
438+
325439
export function usePrompt(): PromptFn {
326440
return useDialogs().prompt;
327441
}

‎src/locales/fr.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"Allan variance": "Variance d'Allan",
9090
"Allan variance…": "Variance d'Allan…",
9191
"Already open — click to focus tab (last seen {when})": "Déjà ouvert — cliquez pour activer l'onglet (vu {when})",
92+
"An error has occurred during the following context:": "Une erreur s'est produite dans le contexte suivant :",
9293
"Analysis": "Analyse",
9394
"and": "et",
9495
"API key": "Clé API",
@@ -201,6 +202,7 @@
201202
"Completion: {count}": "Complétion : {count}",
202203
"Computation engine (Sigima)": "Moteur de calcul (Sigima)",
203204
"Computation engine running in your browser (Python/WebAssembly)": "Moteur de calcul exécuté dans votre navigateur (Python/WebAssembly)",
205+
"Computing: {feature}": "Calcul : {feature}",
204206
"Configure base URL and model in Settings before sending a message.": "Configurez l'URL de base et le modèle dans les paramètres avant d'envoyer un message.",
205207
"Confirm": "Confirmer",
206208
"Confirm rename of the edited item": "Confirmer le renommage de l'élément édité",
@@ -221,6 +223,7 @@
221223
"Convert to polar coordinates": "Convertir en coordonnées polaires",
222224
"Convert to polar coordinates…": "Convertir en coordonnées polaires…",
223225
"Convolution": "Convolution",
226+
"Copied": "Copié",
224227
"Copied!": "Copié !",
225228
"Copy": "Copier",
226229
"Copy as TSV": "Copier au format TSV",
@@ -254,6 +257,7 @@
254257
"Data preview": "Aperçu des données",
255258
"Data storage": "Stockage des données",
256259
"DataLab project website": "Site web du projet DataLab",
260+
"DataLab relies on various libraries to perform the computation. During the computation, errors may occur because of the data (e.g. division by zero, unexpected data type, etc.) or because of the libraries (e.g. memory error, etc.). If you encounter an error, before reporting it, please ensure that the computation is correct, by checking the data and the parameters.": "DataLab s'appuie sur diverses bibliothèques pour effectuer le calcul. Pendant le calcul, des erreurs peuvent survenir à cause des données (par exemple, division par zéro, type de données inattendu, etc.) ou à cause des bibliothèques (par exemple, erreur de mémoire, etc.). Si vous rencontrez une erreur, avant de la signaler, veuillez vous assurer que le calcul est correct, en vérifiant les données et les paramètres.",
257261
"DataLab-Web is about to execute the following Python plugin. Plugins run with full access to the in-browser Python runtime (Pyodide). Only proceed if you trust the source.": "DataLab-Web est sur le point d'exécuter l'extension Python suivante. Les extensions s'exécutent avec un accès complet à l'environnement Python du navigateur (Pyodide). Ne continuez que si vous faites confiance à la source.",
258262
"DataLab-Web is currently in beta — click for details": "DataLab-Web est actuellement en version bêta — cliquez pour plus de détails",
259263
"DataLab-Web is under active development and some features may still be incomplete or behave unexpectedly. If you run into an issue, please report it on the": "DataLab-Web est en développement actif et certaines fonctionnalités peuvent encore être incomplètes ou se comporter de manière inattendue. Si vous rencontrez un problème, veuillez le signaler sur le",
@@ -669,6 +673,7 @@
669673
"Plot results": "Tracer les résultats",
670674
"Plot results failed: {message}": "Échec du tracé des résultats : {message}",
671675
"Plot results obtained from previous analyses.": "Tracer les résultats obtenus lors des analyses précédentes.",
676+
"Plugin action: {action}": "Action du plugin : {action}",
672677
"Plugin error": "Erreur d'extension",
673678
"Plugins": "Extensions",
674679
"point": "point",
@@ -894,6 +899,7 @@
894899
"That's the end of the tour. The demo objects we created will be cleared as soon as you close this dialog.": "C'est la fin de la visite. Les objets de démonstration que nous avons créés seront supprimés dès que vous fermerez cette boîte de dialogue.",
895900
"The": "Le",
896901
"The assistant wants to run the following mutating tool. Review the arguments and approve or reject.": "L'assistant souhaite exécuter l'outil suivant modifiant les données. Examinez les arguments et approuvez ou rejetez.",
902+
"The following traceback may help to understand the problem:": "La trace d'appels suivante peut aider à comprendre le problème :",
897903
"The full user guide opens in a side drawer, and you can replay this tour any time from Help → Welcome.": "Le guide utilisateur complet s'ouvre dans un panneau latéral, et vous pouvez rejouer cette visite à tout moment depuis Aide → Bienvenue.",
898904
"The mock provider replays a scripted set of responses — used by tests and demos, never reaches out to the network.": "Le fournisseur fictif rejoue un ensemble de réponses scriptées — utilisé par les tests et les démonstrations, ne contacte jamais le réseau.",
899905
"Thinking…": "Réflexion…",

‎src/styles.css‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2268,6 +2268,56 @@ input {
22682268
font-size: 12px;
22692269
}
22702270

2271+
/* Processing error dialog (desktop WarningErrorMessageBox parity) */
2272+
.processing-error-card {
2273+
width: 760px;
2274+
max-width: 92vw;
2275+
}
2276+
2277+
.processing-error-context p,
2278+
.processing-error-tip p {
2279+
margin: 4px 0;
2280+
font-size: 12px;
2281+
}
2282+
2283+
.processing-error-traceback {
2284+
margin: 12px 0;
2285+
}
2286+
2287+
.processing-error-traceback-head {
2288+
display: flex;
2289+
align-items: center;
2290+
justify-content: space-between;
2291+
gap: 8px;
2292+
margin-bottom: 6px;
2293+
font-size: 12px;
2294+
}
2295+
2296+
.processing-error-traceback pre {
2297+
margin: 0;
2298+
max-height: 260px;
2299+
overflow: auto;
2300+
padding: 10px 12px;
2301+
background: var(--bg);
2302+
border: 1px solid var(--border);
2303+
border-radius: 4px;
2304+
font-family: "Cascadia Code", Consolas, "Courier New", monospace;
2305+
font-size: 12px;
2306+
line-height: 1.45;
2307+
white-space: pre;
2308+
user-select: text;
2309+
color: #f48771;
2310+
}
2311+
2312+
.processing-error-tip {
2313+
display: flex;
2314+
gap: 8px;
2315+
padding: 8px 10px;
2316+
background: rgba(127, 127, 127, 0.1);
2317+
border-left: 3px solid var(--accent);
2318+
border-radius: 4px;
2319+
}
2320+
22712321
/* Progress dialog */
22722322
.progress-dialog {
22732323
min-width: 420px;

0 commit comments

Comments
 (0)