fix: 首次启动模型配置步骤白屏
- ModelCheckStep 的 onDone 引用不稳定导致 useEffect 反复执行 checkModels - configured 时 return null 导致父容器可见但内容为空(白屏) - 添加 AppErrorBoundary 防止未捕获错误导致白屏
This commit is contained in:
1 parent
44aaa1fef0
commit
dcde9f8f71
2 files changed
+74
-11
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Component, useCallback, useEffect, useState } from 'react'
|
||||
import type { ErrorInfo, ReactNode } from 'react'
|
||||
import { useApp } from '@/store/app-context'
|
||||
import { AppProvider } from '@/store/app-context'
|
||||
import { LanguageProvider } from '@/i18n'
|
||||
@@ -55,9 +56,13 @@ function AppRoot() {
|
||||
if (state.gatewayConnected && !everConnected) setEverConnected(true)
|
||||
}, [state.gatewayConnected, everConnected])
|
||||
|
||||
// 稳定引用,避免 gateway 事件导致 AppRoot re-render 时传给 OnboardingFlow 的 onDone 引用变化,
|
||||
// 进而触发 ModelCheckStep 内 useEffect 反复执行 checkModels
|
||||
const handleOnboardingDone = useCallback(() => setOnboardingDone(true), [])
|
||||
|
||||
// 新用户:走完整 onboarding(gateway loading → profile setup)
|
||||
if (!onboardingDone) {
|
||||
return <OnboardingFlow onDone={() => setOnboardingDone(true)} />
|
||||
return <OnboardingFlow onDone={handleOnboardingDone} />
|
||||
}
|
||||
|
||||
// 老用户:未连接过 → 显示启动封面(品牌 Logo + 连接进度 + 日志)
|
||||
@@ -68,6 +73,48 @@ function AppRoot() {
|
||||
return <MainContent />
|
||||
}
|
||||
|
||||
// ── ErrorBoundary: 防止未捕获的 React 错误导致白屏 ──────────────────────────
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
class AppErrorBoundary extends Component<{ children: ReactNode }, ErrorBoundaryState> {
|
||||
state: ErrorBoundaryState = { error: null }
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo): void {
|
||||
console.error('[AppErrorBoundary]', error, info.componentStack)
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-background p-8">
|
||||
<div className="max-w-md text-center space-y-4">
|
||||
<h2 className="text-lg font-semibold text-destructive">Something went wrong</h2>
|
||||
<p className="text-sm text-muted-foreground break-all">{this.state.error.message}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
this.setState({ error: null })
|
||||
window.location.reload()
|
||||
}}
|
||||
className="px-4 py-2 rounded-md bg-primary text-primary-foreground text-sm hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
useAppUpdate()
|
||||
|
||||
@@ -75,7 +122,9 @@ export default function App() {
|
||||
<LanguageProvider initialLocale="zh-CN" initialPreference="system">
|
||||
<TooltipProvider>
|
||||
<AppProvider>
|
||||
<AppRoot />
|
||||
<AppErrorBoundary>
|
||||
<AppRoot />
|
||||
</AppErrorBoundary>
|
||||
<PortConflictDialog />
|
||||
</AppProvider>
|
||||
<Toaster />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
|
||||
// 剥离 ANSI 颜色/控制转义码,避免日志乱码
|
||||
const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g
|
||||
@@ -519,6 +519,10 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen
|
||||
const [checking, setChecking] = useState(true)
|
||||
const [configured, setConfigured] = useState(false)
|
||||
|
||||
// 用 ref 持有最新的 onDone,避免 onDone 引用变化触发 useEffect 重复执行
|
||||
const onDoneRef = useRef(onDone)
|
||||
onDoneRef.current = onDone
|
||||
|
||||
const checkModels = useCallback(async () => {
|
||||
setChecking(true)
|
||||
try {
|
||||
@@ -529,10 +533,13 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen
|
||||
const hasPrimary = !!result.defaults?.primary
|
||||
if (hasProviders && hasPrimary) {
|
||||
setConfigured(true)
|
||||
setChecking(false)
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (err) {
|
||||
console.warn('[ModelCheckStep] checkModels failed:', err)
|
||||
}
|
||||
setConfigured(false)
|
||||
setChecking(false)
|
||||
return false
|
||||
@@ -540,16 +547,16 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen
|
||||
|
||||
useEffect(() => {
|
||||
checkModels().then((ok) => {
|
||||
if (ok) onDone()
|
||||
if (ok) onDoneRef.current()
|
||||
})
|
||||
}, [checkModels, onDone])
|
||||
}, [checkModels])
|
||||
|
||||
// Re-check when settings dialog closes
|
||||
const recheck = useCallback(() => {
|
||||
checkModels().then((ok) => {
|
||||
if (ok) onDone()
|
||||
if (ok) onDoneRef.current()
|
||||
})
|
||||
}, [checkModels, onDone])
|
||||
}, [checkModels])
|
||||
|
||||
// Expose recheck for parent to call after settings close
|
||||
useEffect(() => {
|
||||
@@ -557,7 +564,7 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen
|
||||
return () => { delete (window as unknown as Record<string, unknown>).__modelCheckRecheck }
|
||||
}, [recheck])
|
||||
|
||||
if (checking && !configured) {
|
||||
if (checking) {
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 text-primary animate-spin" />
|
||||
@@ -565,7 +572,14 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen
|
||||
)
|
||||
}
|
||||
|
||||
if (configured) return null
|
||||
// 模型已配置时也显示 spinner(onDone 会很快切走整个 onboarding)
|
||||
if (configured) {
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 text-primary animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-6 text-center">
|
||||
|
||||
Reference in new issue
Block a user