diff --git a/apps/desktop/src/renderer/components/OllamaGuideModal.tsx b/apps/desktop/src/renderer/components/OllamaGuideModal.tsx index 49e5c52..e7d03a1 100644 --- a/apps/desktop/src/renderer/components/OllamaGuideModal.tsx +++ b/apps/desktop/src/renderer/components/OllamaGuideModal.tsx @@ -43,6 +43,7 @@ import { import { Led } from '@d3ro/ui/components/ds' import { useI18n } from '@d3ro/i18n' import type { LLMModel, LLMStatus } from '@d3ro/core/types' +import { useOllamaModelPull } from '../hooks/useOllamaModelPull' export interface OllamaGuideModalProps { open: boolean @@ -151,15 +152,6 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac const [starting, setStarting] = useState(false) const [startMessage, setStartMessage] = useState(null) - // Download state - const [pullingModelId, setPullingModelId] = useState(null) - const [pullProgress, setPullProgress] = useState<{ - status: string - percent: number - completed: number - total: number - }>({ status: '', percent: 0, completed: 0, total: 0 }) - // Test prompt state const [testPrompt, setTestPrompt] = useState('안녕하세요 오늘의 날씨를 알려줘') const [testResult, setTestResult] = useState(null) @@ -185,32 +177,24 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac } }, []) + // 모델 받기 — 받기만 하고 선택은 별도 버튼(handleSelectModel) + const { + pullingModelId, + progress: pullProgress, + error: pullError, + pull, + select, + } = useOllamaModelPull({ enabled: open, onPulled: refreshStatus }) + useEffect(() => { if (!open) return refreshStatus() - const unsubPull = window.electronAPI.llm.onPullProgress((e) => { - setPullingModelId(e.modelId) - setPullProgress({ - status: e.status, - percent: e.percent, - completed: e.completed, - total: e.total, - }) - if (e.percent >= 100 || e.status === 'success') { - setTimeout(() => { - setPullingModelId(null) - refreshStatus() - }, 1200) - } - }) - const unsubStatus = window.electronAPI.llm.onStatusChanged((e) => { setLlmStatus(e.status) }) return () => { - unsubPull() unsubStatus() } }, [open, refreshStatus]) @@ -242,21 +226,13 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac }, [refreshStatus]) const handlePullModel = useCallback(async (modelId: string) => { - setPullingModelId(modelId) - setPullProgress({ status: '다운로드 시작 중...', percent: 0, completed: 0, total: 0 }) - try { - await window.electronAPI.llm.pullModel({ modelId }) - } catch { - setPullingModelId(null) - } - }, []) + await pull(modelId) + }, [pull]) const handleSelectModel = useCallback(async (modelId: string) => { - await window.electronAPI.llm.setModel({ modelId }) - await window.electronAPI.config.set({ key: 'llmModelId', value: modelId }) - setActiveModel(modelId) + if (await select(modelId)) setActiveModel(modelId) await refreshStatus() - }, [refreshStatus]) + }, [select, refreshStatus]) const handleRunTest = useCallback(async () => { if (!testPrompt.trim()) return @@ -270,7 +246,7 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac if (res.success && res.data) { setTestResult(res.data.processedText) } else { - setTestResult(`오류: ${res.error?.message ?? '응답 실패'}`) + setTestResult(`오류: ${res.success ? '응답 실패' : res.error.message}`) } } catch (err) { setTestResult(`실패: ${err instanceof Error ? err.message : String(err)}`) @@ -476,6 +452,14 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac 음성 인식 후 텍스트 다듬기, 번역, 요약에 가장 안정적인 경량 AI 모델들입니다. + {pullError && ( + + {pullError.stage === 'pull' + ? `${pullError.modelId} — ${t('onboarding.failed', { message: pullError.message })}` + : `${pullError.modelId} — ${pullError.message}`} + + )} + {/* 모델 카드 그리드 */} {RECOMMENDED_MODELS.map((model) => { @@ -520,7 +504,7 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac - {pullProgress.status || '다운로드 중...'} + {pullProgress.status || t('onboarding.statusPreparing')} {pullProgress.percent}% diff --git a/apps/desktop/src/renderer/components/OnboardingModal.tsx b/apps/desktop/src/renderer/components/OnboardingModal.tsx index 71c87c9..ffa0014 100644 --- a/apps/desktop/src/renderer/components/OnboardingModal.tsx +++ b/apps/desktop/src/renderer/components/OnboardingModal.tsx @@ -33,7 +33,9 @@ import { } from 'lucide-react' import { d3roPalette, d3roRadius, d3roShadow, typoSx, d3roFontMono, d3roFontSans } from '@d3ro/ui/theme' import { Led } from '@d3ro/ui/components/ds' +import { useI18n } from '@d3ro/i18n' import type { LLMModel, LLMStatus } from '@d3ro/core/types' +import { useOllamaModelPull } from '../hooks/useOllamaModelPull' type Phase = 'select_mode' | 'local_ollama_setup' | 'online_auth' | 'success' | 'failed' @@ -43,6 +45,7 @@ interface OnboardingModalProps { } export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.ReactElement { + const { t } = useI18n() const [internalOpen, setInternalOpen] = useState(false) const [phase, setPhase] = useState('select_mode') @@ -54,10 +57,6 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React. const [startingOllama, setStartingOllama] = useState(false) const [ollamaMsg, setOllamaMsg] = useState(null) - // Model download state - const [pullingModel, setPullingModel] = useState(null) - const [pullProgress, setPullProgress] = useState({ status: '', percent: 0 }) - // Auth State const [authTab, setAuthTab] = useState<'login' | 'register'>('login') const [email, setEmail] = useState('') @@ -95,29 +94,25 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React. }) }, []) + // 모델 받기 — 받기에 성공한 경우에만 활성 모델로 저장 (autoSelect) + const { + pullingModelId: pullingModel, + progress: pullProgress, + error: pullError, + pull, + } = useOllamaModelPull({ enabled: isVisible, autoSelect: true, onPulled: checkOllama }) + useEffect(() => { if (!isVisible) return - const unsubPull = window.electronAPI.llm.onPullProgress((e) => { - setPullingModel(e.modelId) - setPullProgress({ status: e.status, percent: e.percent }) - if (e.percent >= 100 || e.status === 'success') { - setTimeout(() => { - setPullingModel(null) - checkOllama() - }, 1200) - } - }) - const unsubStatus = window.electronAPI.llm.onStatusChanged((e) => { setOllamaStatus(e.status) }) return () => { - unsubPull() unsubStatus() } - }, [isVisible, checkOllama]) + }, [isVisible]) const handleSelectLocalMode = async () => { setPhase('local_ollama_setup') @@ -153,16 +148,8 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React. } const handlePullModel = async (modelId: string) => { - setPullingModel(modelId) - setPullProgress({ status: '다운로드 시작 중...', percent: 0 }) - try { - await window.electronAPI.llm.pullModel({ modelId }) - await window.electronAPI.llm.setModel({ modelId }) - await window.electronAPI.config.set({ key: 'llmModelId', value: modelId }) - setActiveModel(modelId) - } catch { - setPullingModel(null) - } + const outcome = await pull(modelId) + if (outcome === 'selected') setActiveModel(modelId) } const handleCompleteLocalSetup = async () => { @@ -485,7 +472,7 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React. - {pullProgress.status || '모델 다운로드 중...'} + {pullProgress.status || t('onboarding.statusPreparing')} {pullProgress.percent}% @@ -494,6 +481,14 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React. ) : ( + + {pullError && ( + + {pullError.stage === 'pull' + ? t('onboarding.failed', { message: pullError.message }) + : pullError.message} + + )}