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:
chihlasm
2026-03-24 04:21:41 +00:00
parent 65eb630254
commit 48f2b3faaf
7 changed files with 137 additions and 76 deletions

View File

@@ -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>

View File

@@ -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))
})
})
}, [])

View File

@@ -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))
})
})
}, [])

View File

@@ -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 }))
)

View 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
}),
)
}

View File

@@ -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))
})
})
}, [])

View File

@@ -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>) {