diff --git a/frontend/src/components/common/ErrorBoundary.tsx b/frontend/src/components/common/ErrorBoundary.tsx
index dea0c53b..da3d4f12 100644
--- a/frontend/src/components/common/ErrorBoundary.tsx
+++ b/frontend/src/components/common/ErrorBoundary.tsx
@@ -70,7 +70,14 @@ export function ErrorBoundary({ children, fallback }: Props) {
if (fallback) return fallback as React.ReactElement
return
}}
- 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}
diff --git a/frontend/src/components/dashboard/StartSessionInput.tsx b/frontend/src/components/dashboard/StartSessionInput.tsx
index dbb04622..c306073d 100644
--- a/frontend/src/components/dashboard/StartSessionInput.tsx
+++ b/frontend/src/components/dashboard/StartSessionInput.tsx
@@ -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))
})
})
}, [])
diff --git a/frontend/src/components/flowpilot/FlowPilotMessageBar.tsx b/frontend/src/components/flowpilot/FlowPilotMessageBar.tsx
index a30bf833..4532a3b4 100644
--- a/frontend/src/components/flowpilot/FlowPilotMessageBar.tsx
+++ b/frontend/src/components/flowpilot/FlowPilotMessageBar.tsx
@@ -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))
})
})
}, [])
diff --git a/frontend/src/components/tree-editor/TreeEditorLayout.tsx b/frontend/src/components/tree-editor/TreeEditorLayout.tsx
index c2e81d7b..f4baf96c 100644
--- a/frontend/src/components/tree-editor/TreeEditorLayout.tsx
+++ b/frontend/src/components/tree-editor/TreeEditorLayout.tsx
@@ -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 }))
)
diff --git a/frontend/src/lib/lazyWithRetry.ts b/frontend/src/lib/lazyWithRetry.ts
new file mode 100644
index 00000000..037209b7
--- /dev/null
+++ b/frontend/src/lib/lazyWithRetry.ts
@@ -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>(
+ 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
+ }),
+ )
+}
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index a1f69b3c..cb0a7723 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -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))
})
})
}, [])
diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx
index 22ef345d..57d14ee4 100644
--- a/frontend/src/router.tsx
+++ b/frontend/src/router.tsx
@@ -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) {