// src/renderer/components/AppLayout.tsx // v2 "Midnight Glass": 보더리스 셸 — 와이드 사이드바(그라디언트 액티브 필) + // 앰비언트 배경 + 오버레이 스크롤(레이아웃 폭 불변) + 커스텀 타이틀바 import { useState, useEffect } from 'react' import { Alert, Box, Snackbar, Typography } from '@mui/material' import { LayoutDashboard, History as HistoryIcon, BookOpen, SquareTerminal, MessagesSquare, Library, Users, Settings, } from 'lucide-react' import { OverlayScrollbarsComponent } from 'overlayscrollbars-react' import { Led } from '@d3ro/ui/components/ds' import { DashboardPage } from '../pages/DashboardPage' import { HistoryPage } from '../pages/HistoryPage' import { DictionaryPage } from '../pages/DictionaryPage' import { CommandsPage } from '../pages/CommandsPage' import { VoiceConversationPage } from '../pages/VoiceConversationPage' import { KnowledgeBasePage } from '../pages/KnowledgeBasePage' import { MeetingModePage } from '../pages/MeetingModePage' import { SettingsModal } from './SettingsModal' import { LicenseModal } from './LicenseModal' import { OnboardingModal } from './OnboardingModal' import { StatusBar } from './StatusBar' import { TitleBar } from './TitleBar' import { d3roPalette, d3roFontSans, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' import type { TranslationKey } from '@d3ro/i18n' import type { LicenseTier } from '@d3ro/core/types' type Route = 'dashboard' | 'history' | 'dictionary' | 'commands' | 'conversation' | 'knowledge' | 'meeting' interface NavItem { route: Route labelKey: TranslationKey icon: React.ReactElement } const ICON_SIZE = 18 const NAV_ITEMS: NavItem[] = [ { route: 'dashboard', labelKey: 'nav.dashboard', icon: }, { route: 'history', labelKey: 'nav.history', icon: }, { route: 'dictionary', labelKey: 'nav.dictionary', icon: }, { route: 'commands', labelKey: 'nav.commands', icon: }, { route: 'conversation', labelKey: 'nav.conversation', icon: }, { route: 'knowledge', labelKey: 'nav.knowledge', icon: }, { route: 'meeting', labelKey: 'nav.meeting', icon: }, ] function tierToLedColor(tier: LicenseTier): 'amber' | 'green' { switch (tier) { case 'free': return 'amber' case 'pro': return 'green' case 'pro_plus': return 'green' } } export function AppLayout(): React.ReactElement { const { t } = useI18n() const [currentRoute, setCurrentRoute] = useState('dashboard') const [settingsOpen, setSettingsOpen] = useState(false) const [onboardingOpen, setOnboardingOpen] = useState(false) const [licenseModalOpen, setLicenseModalOpen] = useState(false) const [currentTier, setCurrentTier] = useState('free') // Phase 3.2: Premium LLM fallback 배너 (상단 중앙, 8초, warning filled) const [fallbackMsg, setFallbackMsg] = useState(null) // 음성 세션 에러/경고 배너 (모델 미설치, 엔진 실패, LLM 스킵 등) const [voiceAlert, setVoiceAlert] = useState<{ message: string; severity: 'error' | 'warning' } | null>(null) // 첫 실행 감지 — 로컬 모드 entry point에서 온보딩 자동 표시 useEffect(() => { window.electronAPI.config.getAll().then((r) => { if (r.success && !r.data.onboardingCompleted) { setOnboardingOpen(true) } }) }, []) // License: load tier + subscribe to changes + listen for open-modal events useEffect(() => { window.electronAPI.license.getInfo().then((r) => { if (r.success) setCurrentTier(r.data.tier) }) const unsubTier = window.electronAPI.license.onTierChanged((info) => { setCurrentTier(info.tier) }) const unsubUpgrade = window.electronAPI.license.onUpgradePrompt(() => { setLicenseModalOpen(true) }) const handleOpenLicenseModal = () => setLicenseModalOpen(true) window.addEventListener('d3ro:open-license-modal', handleOpenLicenseModal) // 대시보드 CTA(키 설정/시스템 설정)에서 설정 모달 열기 const handleOpenSettings = () => setSettingsOpen(true) window.addEventListener('d3ro:open-settings', handleOpenSettings) // 음성 세션 에러/경고 — 단축키 녹음 실패를 메인 UI에서도 명확히 알림. // 모델 미설치(101)는 온보딩 모달을 함께 연다. const unsubVoiceError = window.electronAPI.voice.onError((e) => { const severity = e.severity ?? 'error' let message = e.message if (e.errorCode === 101) { message = t('voice.error.modelMissing') setOnboardingOpen(true) } else if (e.errorCode === 103 || (e.errorCode >= 130 && e.errorCode <= 132)) { message = t('voice.error.engine') } else if (severity === 'warning' && e.errorCode === 300) { message = t('voice.warning.llmSkipped') } setVoiceAlert({ message, severity }) }) // Phase 3.2: Premium LLM fallback/upgrade 이벤트 구독 const unsubFallback = window.electronAPI.llm.premium.onFallback((e) => { setFallbackMsg(e.reason) }) const unsubUpgradeReq = window.electronAPI.llm.premium.onUpgradeRequired(() => { setLicenseModalOpen(true) }) return () => { unsubTier() unsubUpgrade() unsubFallback() unsubUpgradeReq() unsubVoiceError() window.removeEventListener('d3ro:open-license-modal', handleOpenLicenseModal) window.removeEventListener('d3ro:open-settings', handleOpenSettings) } }, [t]) return ( {/* ── 사이드바: 와이드 메뉴 (레퍼런스 패턴) ─── */} {/* 로고 — 그라디언트 브랜드 텍스트 */} D3RO Voice {/* 네비게이션 */} {NAV_ITEMS.map((item) => { const isActive = currentRoute === item.route return ( setCurrentRoute(item.route)} sx={{ display: 'flex', alignItems: 'center', gap: 1.25, px: 1.5, py: 1.25, borderRadius: d3roRadius.inner, cursor: 'pointer', color: isActive ? d3roPalette.text.primary : d3roPalette.text.inactive, // 액티브: 글래스 필 + 그라디언트 헤어라인 보더 + 소프트 글로우 border: '1px solid transparent', background: isActive ? `linear-gradient(${d3roPalette.glass.raised}, ${d3roPalette.glass.raised}) padding-box, ${d3roPalette.gradient.accent} border-box` : 'transparent', boxShadow: isActive ? d3roShadow.glowSoft : 'none', transition: 'color 0.15s ease, background-color 0.15s ease', '&:hover': { color: d3roPalette.text.primary, bgcolor: isActive ? undefined : d3roPalette.glass.raised, }, '& svg': { color: isActive ? d3roPalette.accent.light : 'inherit', flexShrink: 0, }, }} > {item.icon} {t(item.labelKey)} ) })} {/* 스페이서 */} {/* Settings */} setSettingsOpen(true)} sx={{ display: 'flex', alignItems: 'center', gap: 1.25, px: 1.5, py: 1.25, borderRadius: d3roRadius.inner, cursor: 'pointer', color: d3roPalette.text.inactive, transition: 'color 0.15s ease, background-color 0.15s ease', '&:hover': { color: d3roPalette.text.primary, bgcolor: d3roPalette.glass.raised }, }} > {t('nav.settings')} {/* ── 콘텐츠 영역: 앰비언트 배경 + 오버레이 스크롤 ── */} {currentRoute === 'dashboard' && } {currentRoute === 'history' && } {currentRoute === 'dictionary' && } {currentRoute === 'commands' && } {currentRoute === 'conversation' && } {currentRoute === 'knowledge' && } {currentRoute === 'meeting' && } setSettingsOpen(false)} /> setLicenseModalOpen(false)} /> setOnboardingOpen(false)} /> {/* Phase 3.2: Premium LLM fallback 배너 — 상단 중앙, 8초, warning filled */} setFallbackMsg(null)} anchorOrigin={{ vertical: 'top', horizontal: 'center' }} > setFallbackMsg(null)} sx={{ width: '100%' }}> {fallbackMsg} {/* 음성 세션 에러/경고 배너 — 모델 미설치·엔진 실패·LLM 스킵 알림 */} setVoiceAlert(null)} anchorOrigin={{ vertical: 'top', horizontal: 'center' }} > setVoiceAlert(null)} sx={{ width: '100%' }} > {voiceAlert?.message ?? ''} ) }