Keep a page error or a busy engine from blanking the app

A user on Upload got a white page while the engine's proxy answered
many requests with nginx's rate-limit 503.

- Pages render inside an error boundary, so a crash shows a message
  with Try again and Reload instead of unmounting the whole app.
- A 503 or 429 without the engine's JSON reads "The engine is busy",
  and queries wait 5s, then 10s, before asking again instead of 1s.
- Upload asks for knowledge details two at a time and reuses them
  until the document's knowledge status changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-29 18:38:18 +07:00
parent a47fe0f845
commit dfb3275afe
8 changed files with 97 additions and 5 deletions

View File

@@ -6,6 +6,7 @@ import { LANGS, locale, setLang, t, useLang, type Key } from '../i18n'
import { workspaceCredits } from '../api/engine'
import { Brand, Icon, type IconName } from './Icon'
import { Toaster } from './Toast'
import { ErrorBoundary } from './ErrorBoundary'
// Learning Studio (learning-lab/frontend); both apps share one Clerk instance, so the user stays signed in.
const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL
@@ -208,7 +209,7 @@ export function AppLayout() {
</div>
</header>
{/* Keyed by route so each page plays the fade-in from theme.css. */}
<main tabIndex={-1} key={pathname}><Outlet /></main>
<main tabIndex={-1} key={pathname}><ErrorBoundary><Outlet /></ErrorBoundary></main>
<footer className="app-footer">Learning Lab Express <span>by BAWANA</span><span>{t('layout.footer')}</span></footer>
</div>
{help && <HelpDialog onClose={() => setHelp(false)} />}

View File

@@ -0,0 +1,35 @@
import { Component, type ReactNode } from 'react'
import { t } from '../i18n'
import { Empty } from './Page'
// Without this, one component throwing while rendering unmounts the whole app and leaves a white
// page. The layout keys it by route, so moving to another page clears the error.
export class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
state = { error: null as Error | null }
static getDerivedStateFromError(error: Error) {
return { error }
}
componentDidCatch(error: Error) {
console.error('Page crashed', error)
}
render() {
const { error } = this.state
if (!error) return this.props.children
return (
<div className="card">
<Empty icon="info" title={t('err.crashTitle')}>
{t('err.crashCopy')}
<br />
<small>{error.message}</small>
</Empty>
<div style={{ display: 'flex', justifyContent: 'center', gap: 8, paddingBottom: 24 }}>
<button className="btn small" onClick={() => this.setState({ error: null })}>{t('common.retry')}</button>
<button className="btn small primary" onClick={() => window.location.reload()}>{t('err.reload')}</button>
</div>
</div>
)
}
}