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 초기화 단계 추가
This commit is contained in:
Yun Chan 2026-04-05 02:18:26 +09:00
parent 517210af2f
commit 4a5cf6c819
10 changed files with 663 additions and 9 deletions

View file

@ -18,6 +18,7 @@ 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'
@ -34,7 +35,8 @@ export function AppLayout(): React.ReactElement {
const [settingsOpen, setSettingsOpen] = useState(false)
return (
<Box sx={{ display: 'flex', height: '100vh' }}>
<Box sx={{ display: 'flex', height: '100vh', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Sidebar Drawer */}
<Drawer
variant="permanent"
@ -107,7 +109,8 @@ export function AppLayout(): React.ReactElement {
</Typography>
)}
</Box>
</Box>
<StatusBar />
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
</Box>
)

View file

@ -0,0 +1,69 @@
// src/renderer/components/StatusBar.tsx
// 하단 상태 표시: Ollama 연결 상태
import { useState, useEffect } from 'react'
import { Box, Chip } from '@mui/material'
import CircleIcon from '@mui/icons-material/Circle'
import type { LLMStatus } from '@shared/types'
export function StatusBar(): React.ReactElement {
const [llmStatus, setLlmStatus] = useState<LLMStatus | null>(null)
useEffect(() => {
// 초기 상태 조회
window.electronAPI.llm.getStatus().then((result) => {
if (result.success) setLlmStatus(result.data)
})
// 상태 변경 구독
const unsub = window.electronAPI.llm.onStatusChanged((event) => {
setLlmStatus(event.status)
})
// 5초마다 폴링 (main에서 이벤트를 보내지 않을 수 있으므로)
const interval = setInterval(() => {
window.electronAPI.llm.getStatus().then((result) => {
if (result.success) setLlmStatus(result.data)
})
}, 5000)
return () => {
unsub()
clearInterval(interval)
}
}, [])
const connected = llmStatus?.connectionState === 'connected'
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
px: 2,
py: 0.5,
borderTop: 1,
borderColor: 'divider',
bgcolor: 'background.paper'
}}
>
<Chip
icon={<CircleIcon sx={{ fontSize: 8 }} />}
label={connected ? 'Ollama Connected' : 'Ollama Offline'}
size="small"
variant="outlined"
color={connected ? 'success' : 'default'}
sx={{ height: 22, '& .MuiChip-label': { fontSize: 11 } }}
/>
{llmStatus?.activeModel && (
<Chip
label={llmStatus.activeModel}
size="small"
variant="outlined"
sx={{ height: 22, '& .MuiChip-label': { fontSize: 11 } }}
/>
)}
</Box>
)
}