d3ro-voice/src/renderer/components/AppLayout.tsx
Yun Chan 4a5cf6c819 Phase 4 구현: Ollama LLM 연동 (다듬기, 번역, 스트리밍)
- LocalLLMService: Ollama REST API, 스트리밍 NDJSON 파싱, 5초 가용성 폴링
- 시스템 프롬프트: refine/translate/summarize/grammar/expand/custom 6개 액션
- VoiceModeService: 전사→LLM 후처리→텍스트 삽입, LLM 실패 시 원본 폴백
- LLM IPC 핸들러: status/models/process/cancel/serverUrl 8개
- StatusBar: Ollama 연결 상태 + 활성 모델 표시
- Preload: llm API 섹션 추가
- Bootstrap: llm-polling 초기화 단계 추가
2026-04-05 02:18:26 +09:00

117 lines
3.4 KiB
TypeScript

// src/renderer/components/AppLayout.tsx
import { useState } from 'react'
import {
Box,
Drawer,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Divider,
Typography,
Chip
} from '@mui/material'
import DashboardIcon from '@mui/icons-material/Dashboard'
import HistoryIcon from '@mui/icons-material/History'
import MenuBookIcon from '@mui/icons-material/MenuBook'
import SettingsIcon from '@mui/icons-material/Settings'
import { DashboardPage } from '../pages/DashboardPage'
import { SettingsModal } from './SettingsModal'
import { StatusBar } from './StatusBar'
type Route = 'dashboard' | 'history' | 'dictionary'
const DRAWER_WIDTH = 240
const NAV_ITEMS: Array<{ route: Route; label: string; icon: React.ReactElement }> = [
{ route: 'dashboard', label: 'Dashboard', icon: <DashboardIcon /> },
{ route: 'history', label: 'History', icon: <HistoryIcon /> },
{ route: 'dictionary', label: 'Dictionary', icon: <MenuBookIcon /> }
]
export function AppLayout(): React.ReactElement {
const [currentRoute, setCurrentRoute] = useState<Route>('dashboard')
const [settingsOpen, setSettingsOpen] = useState(false)
return (
<Box sx={{ display: 'flex', height: '100vh', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Sidebar Drawer */}
<Drawer
variant="permanent"
sx={{
width: DRAWER_WIDTH,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: DRAWER_WIDTH,
boxSizing: 'border-box'
}
}}
>
{/* Header */}
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6" noWrap sx={{ fontWeight: 700 }}>
D3RO Voice
</Typography>
<Chip label="v1.0" size="small" variant="outlined" />
</Box>
<Divider />
{/* Navigation */}
<List sx={{ flex: 1, pt: 1 }}>
{NAV_ITEMS.map((item) => (
<ListItemButton
key={item.route}
selected={currentRoute === item.route}
onClick={() => setCurrentRoute(item.route)}
sx={{ my: 0.5 }}
>
<ListItemIcon sx={{ minWidth: 40 }}>{item.icon}</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
))}
</List>
<Divider />
{/* Bottom */}
<List>
<ListItemButton sx={{ my: 0.5 }} onClick={() => setSettingsOpen(true)}>
<ListItemIcon sx={{ minWidth: 40 }}>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItemButton>
</List>
</Drawer>
{/* Content Area */}
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
overflow: 'auto',
bgcolor: 'background.default'
}}
>
{currentRoute === 'dashboard' && <DashboardPage />}
{currentRoute === 'history' && (
<Typography variant="h5" color="text.secondary">
History (Phase 5)
</Typography>
)}
{currentRoute === 'dictionary' && (
<Typography variant="h5" color="text.secondary">
Dictionary (Phase 5)
</Typography>
)}
</Box>
</Box>
<StatusBar />
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
</Box>
)
}