fix: stale chunk auto-reload + image paste upload UX
- Add lazyWithRetry wrapper for all lazy-loaded routes to auto-reload on stale chunk errors after deploys (prevents ErrorBoundary flash) - Show toast notification when image paste/upload fails due to storage not configured (503), instead of silent tiny error thumbnails - Remove failed uploads from thumbnail strip on 503 (was showing confusing retry icon) - Pass completed upload IDs in navigation state from dashboard input - Suppress Sentry dialog for chunk load errors (deploy artifacts) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,14 @@ export function ErrorBoundary({ children, fallback }: Props) {
|
||||
if (fallback) return fallback as React.ReactElement
|
||||
return <DefaultFallback error={error as Error} resetError={resetError} />
|
||||
}}
|
||||
showDialog
|
||||
beforeCapture={(scope, error) => {
|
||||
// Don't report chunk load errors to Sentry — they're deploy artifacts, not bugs
|
||||
if (error && isChunkLoadError(error as Error)) {
|
||||
scope.setLevel('info')
|
||||
scope.setTag('chunk_load_error', 'true')
|
||||
}
|
||||
}}
|
||||
showDialog={false}
|
||||
>
|
||||
{children}
|
||||
</Sentry.ErrorBoundary>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { Send, Paperclip, Terminal, Loader2, X, RotateCcw, ImagePlus } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { uploadsApi } from '@/api/uploads'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { PendingUpload } from '@/types/upload'
|
||||
|
||||
const SUGGESTIONS = [
|
||||
@@ -44,6 +45,12 @@ export function StartSessionInput() {
|
||||
if (logContent.trim()) {
|
||||
state.logs = logContent.trim()
|
||||
}
|
||||
const completedUploadIds = pendingUploads
|
||||
.filter((u) => u.status === 'done' && u.result?.id)
|
||||
.map((u) => u.result!.id)
|
||||
if (completedUploadIds.length > 0) {
|
||||
state.uploadIds = completedUploadIds
|
||||
}
|
||||
navigate('/assistant', { state })
|
||||
}
|
||||
|
||||
@@ -81,12 +88,16 @@ export function StartSessionInput() {
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMsg = err?.response?.status === 503
|
||||
const is503 = err?.response?.status === 503
|
||||
const errorMsg = is503
|
||||
? 'File uploads not available'
|
||||
: err?.message || 'Upload failed'
|
||||
setPendingUploads((prev) =>
|
||||
prev.map((u) => u.id === upload.id ? { ...u, status: 'error' as const, error: errorMsg } : u)
|
||||
)
|
||||
if (is503) {
|
||||
toast.warning('Image attachments are not available yet — describe the issue in text instead')
|
||||
} else {
|
||||
toast.error(`Upload failed: ${errorMsg}`)
|
||||
}
|
||||
setPendingUploads((prev) => prev.filter((u) => u.id !== upload.id))
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import { Send, Paperclip, Terminal, Loader2, X, RotateCcw, ImagePlus } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { uploadsApi } from '@/api/uploads'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { StepResponseRequest } from '@/types/ai-session'
|
||||
import type { PendingUpload } from '@/types/upload'
|
||||
|
||||
@@ -81,12 +82,13 @@ export function FlowPilotMessageBar({ onRespond, disabled = false, isProcessing
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMsg = err?.response?.status === 503
|
||||
? 'File uploads not available'
|
||||
: err?.message || 'Upload failed'
|
||||
setPendingUploads((prev) =>
|
||||
prev.map((u) => u.id === upload.id ? { ...u, status: 'error' as const, error: errorMsg } : u)
|
||||
)
|
||||
const is503 = err?.response?.status === 503
|
||||
if (is503) {
|
||||
toast.warning('Image attachments are not available yet — describe the issue in text instead')
|
||||
} else {
|
||||
toast.error(`Upload failed: ${err?.message || 'Unknown error'}`)
|
||||
}
|
||||
setPendingUploads((prev) => prev.filter((u) => u.id !== upload.id))
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { Suspense } from 'react'
|
||||
import { lazyWithRetry } from '@/lib/lazyWithRetry'
|
||||
import { TreePreviewPanel } from '@/components/tree-preview/TreePreviewPanel'
|
||||
import { FlowCanvas } from './FlowCanvas'
|
||||
import { NodeEditorPanel } from './NodeEditorPanel'
|
||||
@@ -8,7 +9,7 @@ import { cn } from '@/lib/utils'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
|
||||
// Lazy load CodeModeEditor (Monaco is ~2MB)
|
||||
const CodeModeEditor = lazy(() =>
|
||||
const CodeModeEditor = lazyWithRetry(() =>
|
||||
import('./code-mode/CodeModeEditor').then(m => ({ default: m.CodeModeEditor }))
|
||||
)
|
||||
|
||||
|
||||
34
frontend/src/lib/lazyWithRetry.ts
Normal file
34
frontend/src/lib/lazyWithRetry.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { lazy } from 'react'
|
||||
|
||||
/**
|
||||
* Wraps React.lazy with retry logic for stale chunk errors after deployments.
|
||||
* On first failure, reloads the page once to get fresh asset manifest.
|
||||
*/
|
||||
export function lazyWithRetry<T extends React.ComponentType<unknown>>(
|
||||
importFn: () => Promise<{ default: T }>,
|
||||
) {
|
||||
return lazy(() =>
|
||||
importFn().catch((error: Error) => {
|
||||
const isChunkError =
|
||||
error.message.includes('dynamically imported module') ||
|
||||
error.message.includes('Loading chunk') ||
|
||||
error.message.includes('importing a module script failed') ||
|
||||
error.message.includes('loading css chunk')
|
||||
|
||||
if (!isChunkError) throw error
|
||||
|
||||
// Only auto-reload once per 10 seconds to prevent loops
|
||||
const key = 'rf_lazy_chunk_reload'
|
||||
const lastReload = sessionStorage.getItem(key)
|
||||
const now = Date.now()
|
||||
|
||||
if (!lastReload || now - Number(lastReload) > 10_000) {
|
||||
sessionStorage.setItem(key, String(now))
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// If we already reloaded recently and it still fails, let it propagate
|
||||
throw error
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -305,8 +305,13 @@ export default function AssistantChatPage() {
|
||||
setPendingUploads((prev) => prev.map((u) => u.id === upload.id ? { ...u, status: 'done' as const, result } : u))
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMsg = err?.response?.status === 503 ? 'File uploads not available' : err?.message || 'Upload failed'
|
||||
setPendingUploads((prev) => prev.map((u) => u.id === upload.id ? { ...u, status: 'error' as const, error: errorMsg } : u))
|
||||
const is503 = err?.response?.status === 503
|
||||
if (is503) {
|
||||
toast.warning('Image attachments are not available yet — describe the issue in text instead')
|
||||
} else {
|
||||
toast.error(`Upload failed: ${err?.message || 'Unknown error'}`)
|
||||
}
|
||||
setPendingUploads((prev) => prev.filter((u) => u.id !== upload.id))
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createBrowserRouter } from 'react-router-dom'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { Suspense } from 'react'
|
||||
import { AppLayout, ProtectedRoute } from '@/components/layout'
|
||||
import { RouteError } from '@/components/common/RouteError'
|
||||
import { ErrorBoundary } from '@/components/common/ErrorBoundary'
|
||||
import { PageLoader } from '@/components/common/PageLoader'
|
||||
import { lazyWithRetry } from '@/lib/lazyWithRetry'
|
||||
|
||||
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter)
|
||||
import {
|
||||
@@ -13,73 +14,73 @@ import {
|
||||
} from '@/pages'
|
||||
|
||||
// Public pages
|
||||
const LandingPage = lazy(() => import('@/pages/LandingPage'))
|
||||
const PublicTemplatesPage = lazy(() => import('@/pages/PublicTemplatesPage'))
|
||||
const SharedSessionPage = lazy(() => import('@/pages/SharedSessionPage'))
|
||||
const SurveyPage = lazy(() => import('@/pages/SurveyPage'))
|
||||
const SurveyThankYouPage = lazy(() => import('@/pages/SurveyThankYouPage'))
|
||||
const PrivacyPage = lazy(() => import('@/pages/PrivacyPage'))
|
||||
const TermsPage = lazy(() => import('@/pages/TermsPage'))
|
||||
const LandingPage = lazyWithRetry(() => import('@/pages/LandingPage'))
|
||||
const PublicTemplatesPage = lazyWithRetry(() => import('@/pages/PublicTemplatesPage'))
|
||||
const SharedSessionPage = lazyWithRetry(() => import('@/pages/SharedSessionPage'))
|
||||
const SurveyPage = lazyWithRetry(() => import('@/pages/SurveyPage'))
|
||||
const SurveyThankYouPage = lazyWithRetry(() => import('@/pages/SurveyThankYouPage'))
|
||||
const PrivacyPage = lazyWithRetry(() => import('@/pages/PrivacyPage'))
|
||||
const TermsPage = lazyWithRetry(() => import('@/pages/TermsPage'))
|
||||
|
||||
// Standalone auth pages
|
||||
const VerifyEmailPage = lazy(() => import('@/pages/VerifyEmailPage'))
|
||||
const ChangePasswordPage = lazy(() => import('@/pages/ChangePasswordPage'))
|
||||
const ForgotPasswordPage = lazy(() => import('@/pages/ForgotPasswordPage'))
|
||||
const ResetPasswordPage = lazy(() => import('@/pages/ResetPasswordPage'))
|
||||
const VerifyEmailPage = lazyWithRetry(() => import('@/pages/VerifyEmailPage'))
|
||||
const ChangePasswordPage = lazyWithRetry(() => import('@/pages/ChangePasswordPage'))
|
||||
const ForgotPasswordPage = lazyWithRetry(() => import('@/pages/ForgotPasswordPage'))
|
||||
const ResetPasswordPage = lazyWithRetry(() => import('@/pages/ResetPasswordPage'))
|
||||
|
||||
// Lazy load heavy pages for code splitting
|
||||
const QuickStartPage = lazy(() => import('@/pages/QuickStartPage'))
|
||||
const TreeLibraryPage = lazy(() => import('@/pages/TreeLibraryPage'))
|
||||
const MyTreesPage = lazy(() => import('@/pages/MyTreesPage'))
|
||||
const TreeNavigationPage = lazy(() => import('@/pages/TreeNavigationPage'))
|
||||
const TreeEditorPage = lazy(() => import('@/pages/TreeEditorPage'))
|
||||
const ProceduralEditorPage = lazy(() => import('@/pages/ProceduralEditorPage'))
|
||||
const ProceduralNavigationPage = lazy(() => import('@/pages/ProceduralNavigationPage'))
|
||||
const MaintenanceFlowDetailPage = lazy(() => import('@/pages/MaintenanceFlowDetailPage'))
|
||||
const BatchStatusPage = lazy(() => import('@/pages/BatchStatusPage'))
|
||||
const SessionHistoryPage = lazy(() => import('@/pages/SessionHistoryPage'))
|
||||
const SessionDetailPage = lazy(() => import('@/pages/SessionDetailPage'))
|
||||
const MySharesPage = lazy(() => import('@/pages/MySharesPage'))
|
||||
const TeamAnalyticsPage = lazy(() => import('@/pages/TeamAnalyticsPage'))
|
||||
const MyAnalyticsPage = lazy(() => import('@/pages/MyAnalyticsPage'))
|
||||
const FeedbackPage = lazy(() => import('@/pages/FeedbackPage'))
|
||||
const StepLibraryPage = lazy(() => import('@/pages/StepLibraryPage'))
|
||||
const ScriptLibraryPage = lazy(() => import('@/pages/ScriptLibraryPage'))
|
||||
const ScriptManagePage = lazy(() => import('@/pages/ScriptManagePage'))
|
||||
const AssistantChatPage = lazy(() => import('@/pages/AssistantChatPage'))
|
||||
const FlowAssistPage = lazy(() => import('@/pages/FlowAssistPage'))
|
||||
const FlowPilotSessionPage = lazy(() => import('@/pages/FlowPilotSessionPage'))
|
||||
const EscalationQueuePage = lazy(() => import('@/pages/EscalationQueuePage'))
|
||||
const ReviewQueuePage = lazy(() => import('@/pages/ReviewQueuePage'))
|
||||
const FlowPilotAnalyticsPage = lazy(() => import('@/pages/FlowPilotAnalyticsPage'))
|
||||
const ScriptBuilderPage = lazy(() => import('@/pages/ScriptBuilderPage'))
|
||||
const KBAcceleratorPage = lazy(() => import('@/pages/KBAcceleratorPage'))
|
||||
const GuidesHubPage = lazy(() => import('@/pages/GuidesHubPage'))
|
||||
const GuideDetailPage = lazy(() => import('@/pages/GuideDetailPage'))
|
||||
const AccountSettingsPage = lazy(() => import('@/pages/AccountSettingsPage'))
|
||||
const QuickStartPage = lazyWithRetry(() => import('@/pages/QuickStartPage'))
|
||||
const TreeLibraryPage = lazyWithRetry(() => import('@/pages/TreeLibraryPage'))
|
||||
const MyTreesPage = lazyWithRetry(() => import('@/pages/MyTreesPage'))
|
||||
const TreeNavigationPage = lazyWithRetry(() => import('@/pages/TreeNavigationPage'))
|
||||
const TreeEditorPage = lazyWithRetry(() => import('@/pages/TreeEditorPage'))
|
||||
const ProceduralEditorPage = lazyWithRetry(() => import('@/pages/ProceduralEditorPage'))
|
||||
const ProceduralNavigationPage = lazyWithRetry(() => import('@/pages/ProceduralNavigationPage'))
|
||||
const MaintenanceFlowDetailPage = lazyWithRetry(() => import('@/pages/MaintenanceFlowDetailPage'))
|
||||
const BatchStatusPage = lazyWithRetry(() => import('@/pages/BatchStatusPage'))
|
||||
const SessionHistoryPage = lazyWithRetry(() => import('@/pages/SessionHistoryPage'))
|
||||
const SessionDetailPage = lazyWithRetry(() => import('@/pages/SessionDetailPage'))
|
||||
const MySharesPage = lazyWithRetry(() => import('@/pages/MySharesPage'))
|
||||
const TeamAnalyticsPage = lazyWithRetry(() => import('@/pages/TeamAnalyticsPage'))
|
||||
const MyAnalyticsPage = lazyWithRetry(() => import('@/pages/MyAnalyticsPage'))
|
||||
const FeedbackPage = lazyWithRetry(() => import('@/pages/FeedbackPage'))
|
||||
const StepLibraryPage = lazyWithRetry(() => import('@/pages/StepLibraryPage'))
|
||||
const ScriptLibraryPage = lazyWithRetry(() => import('@/pages/ScriptLibraryPage'))
|
||||
const ScriptManagePage = lazyWithRetry(() => import('@/pages/ScriptManagePage'))
|
||||
const AssistantChatPage = lazyWithRetry(() => import('@/pages/AssistantChatPage'))
|
||||
const FlowAssistPage = lazyWithRetry(() => import('@/pages/FlowAssistPage'))
|
||||
const FlowPilotSessionPage = lazyWithRetry(() => import('@/pages/FlowPilotSessionPage'))
|
||||
const EscalationQueuePage = lazyWithRetry(() => import('@/pages/EscalationQueuePage'))
|
||||
const ReviewQueuePage = lazyWithRetry(() => import('@/pages/ReviewQueuePage'))
|
||||
const FlowPilotAnalyticsPage = lazyWithRetry(() => import('@/pages/FlowPilotAnalyticsPage'))
|
||||
const ScriptBuilderPage = lazyWithRetry(() => import('@/pages/ScriptBuilderPage'))
|
||||
const KBAcceleratorPage = lazyWithRetry(() => import('@/pages/KBAcceleratorPage'))
|
||||
const GuidesHubPage = lazyWithRetry(() => import('@/pages/GuidesHubPage'))
|
||||
const GuideDetailPage = lazyWithRetry(() => import('@/pages/GuideDetailPage'))
|
||||
const AccountSettingsPage = lazyWithRetry(() => import('@/pages/AccountSettingsPage'))
|
||||
// Admin pages
|
||||
const AdminLayout = lazy(() => import('@/components/admin/AdminLayout'))
|
||||
const AdminDashboardPage = lazy(() => import('@/pages/admin/DashboardPage'))
|
||||
const AdminUsersPage = lazy(() => import('@/pages/admin/UsersPage'))
|
||||
const AdminUserDetailPage = lazy(() => import('@/pages/admin/UserDetailPage'))
|
||||
const AdminInviteCodesPage = lazy(() => import('@/pages/admin/InviteCodesPage'))
|
||||
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AuditLogsPage'))
|
||||
const AdminPlanLimitsPage = lazy(() => import('@/pages/admin/PlanLimitsPage'))
|
||||
const AdminFeatureFlagsPage = lazy(() => import('@/pages/admin/FeatureFlagsPage'))
|
||||
const AdminSettingsPage = lazy(() => import('@/pages/admin/SettingsPage'))
|
||||
const AdminGlobalCategoriesPage = lazy(() => import('@/pages/admin/GlobalCategoriesPage'))
|
||||
const AdminSurveyInvitesPage = lazy(() => import('@/pages/admin/SurveyInvitesPage'))
|
||||
const AdminSurveyResponsesPage = lazy(() => import('@/pages/admin/SurveyResponsesPage'))
|
||||
const AdminGalleryManagementPage = lazy(() => import('@/pages/admin/GalleryManagementPage'))
|
||||
const AdminLayout = lazyWithRetry(() => import('@/components/admin/AdminLayout'))
|
||||
const AdminDashboardPage = lazyWithRetry(() => import('@/pages/admin/DashboardPage'))
|
||||
const AdminUsersPage = lazyWithRetry(() => import('@/pages/admin/UsersPage'))
|
||||
const AdminUserDetailPage = lazyWithRetry(() => import('@/pages/admin/UserDetailPage'))
|
||||
const AdminInviteCodesPage = lazyWithRetry(() => import('@/pages/admin/InviteCodesPage'))
|
||||
const AdminAuditLogsPage = lazyWithRetry(() => import('@/pages/admin/AuditLogsPage'))
|
||||
const AdminPlanLimitsPage = lazyWithRetry(() => import('@/pages/admin/PlanLimitsPage'))
|
||||
const AdminFeatureFlagsPage = lazyWithRetry(() => import('@/pages/admin/FeatureFlagsPage'))
|
||||
const AdminSettingsPage = lazyWithRetry(() => import('@/pages/admin/SettingsPage'))
|
||||
const AdminGlobalCategoriesPage = lazyWithRetry(() => import('@/pages/admin/GlobalCategoriesPage'))
|
||||
const AdminSurveyInvitesPage = lazyWithRetry(() => import('@/pages/admin/SurveyInvitesPage'))
|
||||
const AdminSurveyResponsesPage = lazyWithRetry(() => import('@/pages/admin/SurveyResponsesPage'))
|
||||
const AdminGalleryManagementPage = lazyWithRetry(() => import('@/pages/admin/GalleryManagementPage'))
|
||||
|
||||
// Account pages
|
||||
const AccountLayout = lazy(() => import('@/components/account/AccountLayout'))
|
||||
const ProfileSettingsPage = lazy(() => import('@/pages/account/ProfileSettingsPage'))
|
||||
const TeamCategoriesPage = lazy(() => import('@/pages/account/TeamCategoriesPage'))
|
||||
const TargetListsPage = lazy(() => import('@/pages/account/TargetListsPage'))
|
||||
const ChatRetentionSettingsPage = lazy(() => import('@/pages/account/ChatRetentionSettingsPage'))
|
||||
const IntegrationsPage = lazy(() => import('@/pages/account/IntegrationsPage'))
|
||||
const BrandingSettingsPage = lazy(() => import('@/pages/account/BrandingSettingsPage'))
|
||||
const AccountLayout = lazyWithRetry(() => import('@/components/account/AccountLayout'))
|
||||
const ProfileSettingsPage = lazyWithRetry(() => import('@/pages/account/ProfileSettingsPage'))
|
||||
const TeamCategoriesPage = lazyWithRetry(() => import('@/pages/account/TeamCategoriesPage'))
|
||||
const TargetListsPage = lazyWithRetry(() => import('@/pages/account/TargetListsPage'))
|
||||
const ChatRetentionSettingsPage = lazyWithRetry(() => import('@/pages/account/ChatRetentionSettingsPage'))
|
||||
const IntegrationsPage = lazyWithRetry(() => import('@/pages/account/IntegrationsPage'))
|
||||
const BrandingSettingsPage = lazyWithRetry(() => import('@/pages/account/BrandingSettingsPage'))
|
||||
|
||||
/** Wraps a lazy-loaded page with Suspense + ErrorBoundary */
|
||||
function page(Component: React.LazyExoticComponent<React.ComponentType>) {
|
||||
|
||||
Reference in New Issue
Block a user