- 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 초기화 단계 추가
117 lines
3.4 KiB
TypeScript
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>
|
|
)
|
|
}
|