// src/renderer/components/StatusBar.tsx // 인스트루먼트 섀시 하단 — 각인 스타일 상태 표시 + Ollama 오프라인 넛징 // 타이포 토큰 적용 (d3roTypo SSOT) import { useState, useEffect } from 'react' import { Box, Typography, Fade, IconButton } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import { Led } from './ds' import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '../theme' import { OllamaGuideModal } from './OllamaGuideModal' import { useI18n } from '../i18n' import type { LLMStatus } from '@shared/types' export function StatusBar(): React.ReactElement { const { t } = useI18n() const [llmStatus, setLlmStatus] = useState(null) const [showNudge, setShowNudge] = useState(false) const [guideOpen, setGuideOpen] = useState(false) const [captionActive, setCaptionActive] = useState(false) // 자막 상태 구독 useEffect(() => { window.electronAPI.caption.getState().then((r) => { if (r.success) setCaptionActive(r.data === 'active') }) const unsub = window.electronAPI.caption.onStateChanged((data) => { setCaptionActive(data.state === 'active') }) return unsub }, []) useEffect(() => { // 초기 상태 로드 + 이벤트 구독 (폴링 불필요 — onStatusChanged로 실시간 갱신) window.electronAPI.llm.getStatus().then((r) => { if (r.success) setLlmStatus(r.data) }) const unsub = window.electronAPI.llm.onStatusChanged((e) => setLlmStatus(e.status)) return unsub }, []) const connected = llmStatus?.connectionState === 'connected' useEffect(() => { if (!connected) { const showTimer = setTimeout(() => setShowNudge(true), 3000) const hideTimer = setTimeout(() => setShowNudge(false), 13000) return () => { clearTimeout(showTimer); clearTimeout(hideTimer) } } setShowNudge(false) return undefined }, [connected]) return ( <> {/* 넛징 버블 */} {t('status.nudge.title')} setShowNudge(false)} sx={{ color: d3roPalette.text.inactive, p: 0, ml: 1 }}> {t('status.nudge.desc')} { setGuideOpen(true); setShowNudge(false) }} sx={{ fontSize: d3roTypo.meta.size, color: d3roPalette.accent.amber, fontWeight: d3roTypo.label.weight, cursor: 'pointer', '&:hover': { textDecoration: 'underline' }, }} > {t('status.nudge.guide')} !connected && setGuideOpen(true)} sx={{ fontFamily: d3roFontMono, fontSize: d3roTypo.engrave.size, color: connected ? d3roPalette.text.inactive : d3roPalette.tag.red, letterSpacing: d3roTypo.engrave.spacing, fontWeight: d3roTypo.engrave.weight, cursor: connected ? 'default' : 'pointer', '&:hover': connected ? {} : { textDecoration: 'underline' }, }} > {connected ? t('status.ollama') : t('status.offline')} {llmStatus?.activeModel && ( {llmStatus.activeModel.toUpperCase()} )} {/* 자막 상태 표시 */} {captionActive && ( CAPTION )} PRECISION DATA LINK setGuideOpen(false)} /> ) }