fix(onboarding): 무진행 구간 UX + 중복 실행 가드
- verifying/manifest 상태(바이트 진행률 없음)에서 인디터미넌트 바 + 친화적 문구 — 9.6GB 검증 ~2분간 멈춘 것처럼 보이던 문제 해소 - OnboardingModal 중복 실행 가드 (runningRef) - LocalLLMService.pullModel: 동일 모델 동시 pull은 기존 promise 합류 - LocalSTTService.downloadModel: /download 409는 실패가 아닌 기존 진행 합류
This commit is contained in:
parent
64c1d3709b
commit
0b9e67afe9
6 changed files with 86 additions and 35 deletions
|
|
@ -87,7 +87,10 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
|||
const [status, setStatus] = useState('')
|
||||
const [detail, setDetail] = useState('')
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
// 진행률 없는 상태(무결성 검증 등) — 인디터미넌트 바로 "멈춘 것처럼" 보이는 문제 방지
|
||||
const [busyStatus, setBusyStatus] = useState(false)
|
||||
const activeStepRef = useRef<StepKind | null>(null)
|
||||
const runningRef = useRef(false)
|
||||
|
||||
const isVisible = open || internalOpen
|
||||
|
||||
|
|
@ -127,6 +130,9 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
|||
if (e.modelId !== DEFAULT_LLM_MODEL) return
|
||||
setStatus(e.status)
|
||||
if (e.percent > 0) setPercent(e.percent)
|
||||
// total 없는 상태 라인(pulling manifest / verifying / writing manifest)은
|
||||
// 바이트 진행률이 없음 — 인디터미넌트로 표시
|
||||
setBusyStatus(!e.total || e.total === 0)
|
||||
})
|
||||
return () => {
|
||||
unsub()
|
||||
|
|
@ -153,40 +159,47 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
|||
}, [t])
|
||||
|
||||
const handleDownload = useCallback(async (): Promise<void> => {
|
||||
// 중복 실행 가드 (연타/이중 트리거 방지)
|
||||
if (runningRef.current) return
|
||||
runningRef.current = true
|
||||
setPhase('downloading')
|
||||
setErrorMsg('')
|
||||
|
||||
// 재시도 시 이미 끝난 단계를 스킵하도록 매번 재계산
|
||||
const needed = await computeNeededSteps()
|
||||
setSttModelId(needed.sttModelId)
|
||||
setNeededSteps(needed.steps)
|
||||
try {
|
||||
// 재시도 시 이미 끝난 단계를 스킵하도록 매번 재계산
|
||||
const needed = await computeNeededSteps()
|
||||
setSttModelId(needed.sttModelId)
|
||||
setNeededSteps(needed.steps)
|
||||
|
||||
for (let i = 0; i < needed.steps.length; i++) {
|
||||
const step = needed.steps[i]
|
||||
setStepIndex(i)
|
||||
setPercent(0)
|
||||
setStatus('')
|
||||
setDetail('')
|
||||
activeStepRef.current = step
|
||||
for (let i = 0; i < needed.steps.length; i++) {
|
||||
const step = needed.steps[i]
|
||||
setStepIndex(i)
|
||||
setPercent(0)
|
||||
setStatus('')
|
||||
setDetail('')
|
||||
setBusyStatus(false)
|
||||
activeStepRef.current = step
|
||||
|
||||
const result =
|
||||
step === 'llm'
|
||||
? await window.electronAPI.llm.pullModel({ modelId: DEFAULT_LLM_MODEL })
|
||||
: await window.electronAPI.stt.downloadModel({ modelId: needed.sttModelId })
|
||||
const result =
|
||||
step === 'llm'
|
||||
? await window.electronAPI.llm.pullModel({ modelId: DEFAULT_LLM_MODEL })
|
||||
: await window.electronAPI.stt.downloadModel({ modelId: needed.sttModelId })
|
||||
|
||||
if (!result.success) {
|
||||
activeStepRef.current = null
|
||||
setPhase('failed')
|
||||
setErrorMsg(result.error?.message ?? 'unknown')
|
||||
return
|
||||
if (!result.success) {
|
||||
setPhase('failed')
|
||||
setErrorMsg(result.error?.message ?? 'unknown')
|
||||
return
|
||||
}
|
||||
setPercent(100)
|
||||
}
|
||||
setPercent(100)
|
||||
}
|
||||
|
||||
activeStepRef.current = null
|
||||
setPhase('success')
|
||||
// 온보딩 완료 플래그 저장
|
||||
window.electronAPI.config.set({ key: 'onboardingCompleted', value: true })
|
||||
setPhase('success')
|
||||
// 온보딩 완료 플래그 저장
|
||||
window.electronAPI.config.set({ key: 'onboardingCompleted', value: true })
|
||||
} finally {
|
||||
activeStepRef.current = null
|
||||
runningRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
|
|
@ -209,6 +222,15 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
|||
? (neededSteps[stepIndex] ?? null)
|
||||
: null
|
||||
|
||||
// Ollama 원시 status 문자열 → 친화적 문구 (알려진 것만 매핑, 그 외 원문)
|
||||
const formatStatus = (raw: string): string => {
|
||||
if (raw.startsWith('pulling manifest')) return t('onboarding.statusPreparing')
|
||||
if (raw.startsWith('verifying')) return t('onboarding.statusVerifying')
|
||||
if (raw.startsWith('writing manifest') || raw === 'success')
|
||||
return t('onboarding.statusFinalizing')
|
||||
return raw
|
||||
}
|
||||
|
||||
/** 단계별 안내 박스 렌더 */
|
||||
const renderStepInfo = (step: StepKind): React.ReactElement => (
|
||||
<Box
|
||||
|
|
@ -287,11 +309,11 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
|||
: t('onboarding.downloading')}
|
||||
</Typography>
|
||||
<Typography sx={{ ...typoSx('small'), color: colorAccent }}>
|
||||
{percent}%
|
||||
{busyStatus ? '…' : `${percent}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
variant={busyStatus ? 'indeterminate' : 'determinate'}
|
||||
value={percent}
|
||||
sx={{ height: 8, borderRadius: d3roRadius.xs }}
|
||||
/>
|
||||
|
|
@ -306,7 +328,7 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
|||
<Typography
|
||||
sx={{ ...typoSx('meta'), color: d3roPalette.text.secondary, mt: 1 }}
|
||||
>
|
||||
{t('onboarding.status', { status })}
|
||||
{t('onboarding.status', { status: formatStatus(status) })}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue