Ollama 설치 안내 UI + 온보딩 Ollama 단계
- StatusBar: 오프라인 3초 후 넛징 버블 표시 ("설치 안내 보기→" 링크)
- OllamaGuideModal: 3단계 설치 안내 (다운로드→모델 pull→자동 연결)
- OnboardingModal: Step 3에 Ollama 설치 안내 단계 추가
- Settings 정보 탭: "초기 설정 안내 다시 보기" 버튼
- system.openExternal IPC 추가 (외부 URL 열기)
This commit is contained in:
parent
ea48168b1e
commit
dc06a326b5
6 changed files with 341 additions and 35 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
// src/main/ipc/window-handlers.ts
|
// src/main/ipc/window-handlers.ts
|
||||||
|
|
||||||
import { ipcMain } from 'electron'
|
import { ipcMain, shell } from 'electron'
|
||||||
import { IPC_CHANNELS } from '@shared/ipc-channels'
|
import { IPC_CHANNELS } from '@shared/ipc-channels'
|
||||||
import { ipcSuccess } from '@shared/errors'
|
import { ipcSuccess } from '@shared/errors'
|
||||||
import { getMainWindow, hideResultPopup, hideRecordingTip } from '../windows/WindowManager'
|
import { getMainWindow, hideResultPopup, hideRecordingTip } from '../windows/WindowManager'
|
||||||
|
|
@ -37,4 +37,10 @@ export function registerWindowHandlers(): void {
|
||||||
ipcMain.on('window:hideRecordingTip', () => {
|
ipcMain.on('window:hideRecordingTip', () => {
|
||||||
hideRecordingTip()
|
hideRecordingTip()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 외부 URL 열기
|
||||||
|
ipcMain.handle(IPC_CHANNELS.SYSTEM.OPEN_EXTERNAL, async (_event, params: { url: string }) => {
|
||||||
|
await shell.openExternal(params.url)
|
||||||
|
return ipcSuccess(undefined)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,9 @@ const electronAPI = {
|
||||||
getPlatform: () => invoke<NodeJS.Platform>(IPC_CHANNELS.SYSTEM.GET_PLATFORM),
|
getPlatform: () => invoke<NodeJS.Platform>(IPC_CHANNELS.SYSTEM.GET_PLATFORM),
|
||||||
getVersion: () => invoke<string>(IPC_CHANNELS.SYSTEM.GET_VERSION),
|
getVersion: () => invoke<string>(IPC_CHANNELS.SYSTEM.GET_VERSION),
|
||||||
checkMicPermission: () =>
|
checkMicPermission: () =>
|
||||||
invoke<PermissionStatus>(IPC_CHANNELS.SYSTEM.CHECK_MIC_PERMISSION)
|
invoke<PermissionStatus>(IPC_CHANNELS.SYSTEM.CHECK_MIC_PERMISSION),
|
||||||
|
openExternal: (params: { url: string }) =>
|
||||||
|
invoke<void>(IPC_CHANNELS.SYSTEM.OPEN_EXTERNAL, params),
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Instruction (커스텀 명령어) ────────────────────────
|
// ── Instruction (커스텀 명령어) ────────────────────────
|
||||||
|
|
|
||||||
161
src/renderer/components/OllamaGuideModal.tsx
Normal file
161
src/renderer/components/OllamaGuideModal.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
// src/renderer/components/OllamaGuideModal.tsx
|
||||||
|
// Ollama 설치/설정 안내 모달
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/material'
|
||||||
|
import CloseIcon from '@mui/icons-material/Close'
|
||||||
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||||
|
import { d3roPalette, d3roFontMono } from '../theme'
|
||||||
|
import { Led } from './ds'
|
||||||
|
|
||||||
|
interface OllamaGuideModalProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeBlock({ children }: { children: string }): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: d3roPalette.bg.inset,
|
||||||
|
borderRadius: '8px',
|
||||||
|
p: 1.5,
|
||||||
|
fontFamily: d3roFontMono,
|
||||||
|
fontSize: '12px',
|
||||||
|
color: d3roPalette.accent.amber,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxShadow: 'inset 0 1px 4px rgba(0,0,0,0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{children}</span>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => navigator.clipboard.writeText(children)}
|
||||||
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
|
>
|
||||||
|
<ContentCopyIcon sx={{ fontSize: 14 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): React.ReactElement {
|
||||||
|
const handleOpenLink = (url: string) => {
|
||||||
|
window.electronAPI.system.openExternal({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
bgcolor: d3roPalette.bg.chassis,
|
||||||
|
backgroundImage: 'none',
|
||||||
|
border: `1px solid ${d3roPalette.border.subtle}`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontFamily: d3roFontMono,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: '14px',
|
||||||
|
letterSpacing: '1px',
|
||||||
|
color: d3roPalette.accent.amber,
|
||||||
|
py: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
OLLAMA SETUP GUIDE
|
||||||
|
<IconButton onClick={onClose} size="small" sx={{ color: d3roPalette.text.inactive }}>
|
||||||
|
<CloseIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||||
|
<DialogContent sx={{ bgcolor: d3roPalette.bg.app }}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, py: 1 }}>
|
||||||
|
{/* Step 1 */}
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||||
|
<Led color="amber" size={8} />
|
||||||
|
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '12px', fontWeight: 700 }}>
|
||||||
|
STEP 1 — Ollama 설치
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 1.5 }}>
|
||||||
|
Ollama는 로컬에서 LLM을 실행하는 무료 도구입니다.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
endIcon={<OpenInNewIcon sx={{ fontSize: 14 }} />}
|
||||||
|
onClick={() => handleOpenLink('https://ollama.com/download')}
|
||||||
|
sx={{ fontFamily: d3roFontMono, fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
ollama.com/download
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||||
|
|
||||||
|
{/* Step 2 */}
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||||
|
<Led color="amber" size={8} />
|
||||||
|
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '12px', fontWeight: 700 }}>
|
||||||
|
STEP 2 — 모델 다운로드
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 1.5 }}>
|
||||||
|
터미널에서 원하는 모델을 pull하세요. 한국어에 추천:
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
<CodeBlock>ollama pull qwen3:4b</CodeBlock>
|
||||||
|
<Typography variant="caption" sx={{ color: d3roPalette.text.inactive }}>
|
||||||
|
또는 더 큰 모델: ollama pull qwen3:8b (더 정확, 더 느림)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||||
|
|
||||||
|
{/* Step 3 */}
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||||
|
<Led color="green" size={8} />
|
||||||
|
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '12px', fontWeight: 700 }}>
|
||||||
|
STEP 3 — 자동 연결
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary }}>
|
||||||
|
Ollama가 실행되면 D3RO-VOICE가 자동으로 감지합니다.
|
||||||
|
하단 상태 바의 LED가 빨간색에서 초록색으로 바뀌면 준비 완료!
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2, bgcolor: d3roPalette.bg.app }}>
|
||||||
|
<Button onClick={onClose} variant="contained">
|
||||||
|
확인
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
import MicIcon from '@mui/icons-material/Mic'
|
import MicIcon from '@mui/icons-material/Mic'
|
||||||
import KeyboardIcon from '@mui/icons-material/Keyboard'
|
import KeyboardIcon from '@mui/icons-material/Keyboard'
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||||
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
import { d3roPalette, d3roFontMono } from '../theme'
|
import { d3roPalette, d3roFontMono } from '../theme'
|
||||||
import { Led } from './ds'
|
import { Led } from './ds'
|
||||||
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
||||||
|
|
@ -25,7 +26,7 @@ interface OnboardingModalProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.ReactElement {
|
export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.ReactElement {
|
||||||
const [step, setStep] = useState(0) // 0: 환영, 1: 마이크, 2: 핫키, 3: 완료
|
const [step, setStep] = useState(0) // 0: 환영, 1: 마이크, 2: 핫키, 3: Ollama, 4: 완료
|
||||||
const [devices, setDevices] = useState<AudioDevice[]>([])
|
const [devices, setDevices] = useState<AudioDevice[]>([])
|
||||||
const [selectedDevice, setSelectedDevice] = useState('default')
|
const [selectedDevice, setSelectedDevice] = useState('default')
|
||||||
const [hotkeyBinding, setHotkeyBinding] = useState<HotkeyBinding | null>(null)
|
const [hotkeyBinding, setHotkeyBinding] = useState<HotkeyBinding | null>(null)
|
||||||
|
|
@ -199,8 +200,45 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 3: 완료 */}
|
{/* Step 3: Ollama 설치 */}
|
||||||
{step === 3 && (
|
{step === 3 && (
|
||||||
|
<Box>
|
||||||
|
<Stack direction="row" alignItems="center" gap={1} mb={3}>
|
||||||
|
<Led color="amber" size={12} />
|
||||||
|
<Typography sx={{ fontFamily: d3roFontMono, fontWeight: 700, fontSize: '14px' }}>
|
||||||
|
Ollama 설치 (선택)
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 2 }}>
|
||||||
|
번역, 요약 등 LLM 후처리를 사용하려면 Ollama가 필요합니다.
|
||||||
|
음성 받아쓰기 자체는 Ollama 없이도 동작합니다.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
endIcon={<OpenInNewIcon sx={{ fontSize: 14 }} />}
|
||||||
|
onClick={() => window.electronAPI.system.openExternal({ url: 'https://ollama.com/download' })}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mb: 1.5, fontFamily: d3roFontMono }}
|
||||||
|
>
|
||||||
|
Ollama 다운로드
|
||||||
|
</Button>
|
||||||
|
<Box sx={{ p: 1.5, borderRadius: '8px', bgcolor: d3roPalette.bg.inset, boxShadow: 'inset 0 1px 4px rgba(0,0,0,0.3)', mb: 3 }}>
|
||||||
|
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '11px', color: d3roPalette.accent.amber }}>
|
||||||
|
$ ollama pull qwen3:4b
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ fontSize: '10px', color: d3roPalette.text.inactive, mt: 0.5 }}>
|
||||||
|
설치 후 터미널에서 모델을 다운로드하세요
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Button onClick={() => setStep(2)} sx={{ color: d3roPalette.text.inactive }}>뒤로</Button>
|
||||||
|
<Button variant="contained" onClick={() => setStep(4)}>다음</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 4: 완료 */}
|
||||||
|
{step === 4 && (
|
||||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||||
<CheckCircleIcon sx={{ fontSize: 48, color: d3roPalette.tag.green, mb: 2 }} />
|
<CheckCircleIcon sx={{ fontSize: 48, color: d3roPalette.tag.green, mb: 2 }} />
|
||||||
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '18px', fontWeight: 700, mb: 1 }}>
|
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '18px', fontWeight: 700, mb: 1 }}>
|
||||||
|
|
|
||||||
|
|
@ -687,10 +687,28 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
||||||
|
|
||||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '11px' }}>
|
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '11px', mb: 2 }}>
|
||||||
Speakly 리버스엔지니어링 노하우 기반 로컬 AI 음성 어시스턴트.
|
Speakly 리버스엔지니어링 노하우 기반 로컬 AI 음성 어시스턴트.
|
||||||
클라우드 의존성 없이 완전 로컬로 동작합니다.
|
클라우드 의존성 없이 완전 로컬로 동작합니다.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
// 온보딩 플래그 리셋 → 다음 Settings 닫기 시 온보딩 다시 표시
|
||||||
|
window.electronAPI.config.set({
|
||||||
|
key: 'onboardingCompleted' as keyof import('@shared/types').AppConfig,
|
||||||
|
value: false as never,
|
||||||
|
})
|
||||||
|
onClose()
|
||||||
|
// 약간의 딜레이 후 AppLayout이 온보딩을 다시 감지
|
||||||
|
setTimeout(() => window.location.reload(), 300)
|
||||||
|
}}
|
||||||
|
sx={{ fontFamily: d3roFontMono, fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
초기 설정 안내 다시 보기
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
// src/renderer/components/StatusBar.tsx
|
// src/renderer/components/StatusBar.tsx
|
||||||
// 인스트루먼트 섀시 하단 — 각인 스타일 상태 표시
|
// 인스트루먼트 섀시 하단 — 각인 스타일 상태 표시 + Ollama 오프라인 넛징
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Box, Typography } from '@mui/material'
|
import { Box, Typography, Fade } from '@mui/material'
|
||||||
import { Led } from './ds'
|
import { Led } from './ds'
|
||||||
import { d3roPalette, d3roFontMono } from '../theme'
|
import { d3roPalette, d3roFontMono } from '../theme'
|
||||||
|
import { OllamaGuideModal } from './OllamaGuideModal'
|
||||||
import type { LLMStatus } from '@shared/types'
|
import type { LLMStatus } from '@shared/types'
|
||||||
|
|
||||||
export function StatusBar(): React.ReactElement {
|
export function StatusBar(): React.ReactElement {
|
||||||
const [llmStatus, setLlmStatus] = useState<LLMStatus | null>(null)
|
const [llmStatus, setLlmStatus] = useState<LLMStatus | null>(null)
|
||||||
|
const [showNudge, setShowNudge] = useState(false)
|
||||||
|
const [guideOpen, setGuideOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electronAPI.llm.getStatus().then((r) => {
|
window.electronAPI.llm.getStatus().then((r) => {
|
||||||
|
|
@ -27,37 +30,115 @@ export function StatusBar(): React.ReactElement {
|
||||||
|
|
||||||
const connected = llmStatus?.connectionState === 'connected'
|
const connected = llmStatus?.connectionState === 'connected'
|
||||||
|
|
||||||
|
// 오프라인 3초 후 넛징 버블 표시
|
||||||
|
useEffect(() => {
|
||||||
|
if (!connected) {
|
||||||
|
const timer = setTimeout(() => setShowNudge(true), 3000)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
setShowNudge(false)
|
||||||
|
return undefined
|
||||||
|
}, [connected])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
sx={{
|
<Box
|
||||||
display: 'flex',
|
sx={{
|
||||||
alignItems: 'center',
|
position: 'relative',
|
||||||
gap: 2,
|
display: 'flex',
|
||||||
px: 2,
|
alignItems: 'center',
|
||||||
py: 0.5,
|
gap: 2,
|
||||||
borderTop: `1px solid ${d3roPalette.border.subtle}`,
|
px: 2,
|
||||||
bgcolor: d3roPalette.bg.sidebar,
|
py: 0.5,
|
||||||
minHeight: 28,
|
borderTop: `1px solid ${d3roPalette.border.subtle}`,
|
||||||
}}
|
bgcolor: d3roPalette.bg.sidebar,
|
||||||
>
|
minHeight: 28,
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
}}
|
||||||
<Led color={connected ? 'green' : 'red'} size={6} />
|
>
|
||||||
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '9px', color: d3roPalette.text.inactive, letterSpacing: '1px', fontWeight: 700 }}>
|
{/* 넛징 버블 */}
|
||||||
{connected ? 'OLLAMA' : 'OFFLINE'}
|
<Fade in={showNudge && !connected}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 32,
|
||||||
|
left: 8,
|
||||||
|
bgcolor: d3roPalette.bg.card,
|
||||||
|
border: `1px solid ${d3roPalette.border.default}`,
|
||||||
|
borderRadius: '10px',
|
||||||
|
px: 2,
|
||||||
|
py: 1.5,
|
||||||
|
boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
|
||||||
|
maxWidth: 280,
|
||||||
|
zIndex: 100,
|
||||||
|
// 말풍선 삼각형
|
||||||
|
'&::after': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -6,
|
||||||
|
left: 16,
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
bgcolor: d3roPalette.bg.card,
|
||||||
|
border: `1px solid ${d3roPalette.border.default}`,
|
||||||
|
borderTop: 'none',
|
||||||
|
borderLeft: 'none',
|
||||||
|
transform: 'rotate(45deg)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ fontSize: '12px', color: d3roPalette.text.primary, mb: 0.5, fontWeight: 600 }}>
|
||||||
|
Ollama가 실행되지 않고 있어요
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ fontSize: '11px', color: d3roPalette.text.inactive, mb: 1 }}>
|
||||||
|
LLM 후처리(번역, 요약 등)를 사용하려면 Ollama가 필요합니다.
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
onClick={() => { setGuideOpen(true); setShowNudge(false) }}
|
||||||
|
sx={{
|
||||||
|
fontSize: '11px',
|
||||||
|
color: d3roPalette.accent.amber,
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': { textDecoration: 'underline' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
설치 안내 보기 →
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||||
|
<Led color={connected ? 'green' : 'red'} size={6} />
|
||||||
|
<Typography
|
||||||
|
onClick={() => !connected && setGuideOpen(true)}
|
||||||
|
sx={{
|
||||||
|
fontFamily: d3roFontMono,
|
||||||
|
fontSize: '9px',
|
||||||
|
color: connected ? d3roPalette.text.inactive : d3roPalette.tag.red,
|
||||||
|
letterSpacing: '1px',
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: connected ? 'default' : 'pointer',
|
||||||
|
'&:hover': connected ? {} : { textDecoration: 'underline' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{connected ? 'OLLAMA' : 'OFFLINE'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{llmStatus?.activeModel && (
|
||||||
|
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '9px', color: d3roPalette.text.dimLabel, letterSpacing: '0.5px' }}>
|
||||||
|
{llmStatus.activeModel.toUpperCase()}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ flex: 1 }} />
|
||||||
|
|
||||||
|
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '9px', color: d3roPalette.text.muted, letterSpacing: '1px', fontWeight: 700 }}>
|
||||||
|
PRECISION DATA LINK
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{llmStatus?.activeModel && (
|
<OllamaGuideModal open={guideOpen} onClose={() => setGuideOpen(false)} />
|
||||||
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '9px', color: d3roPalette.text.dimLabel, letterSpacing: '0.5px' }}>
|
</>
|
||||||
{llmStatus.activeModel.toUpperCase()}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box sx={{ flex: 1 }} />
|
|
||||||
|
|
||||||
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '9px', color: d3roPalette.text.muted, letterSpacing: '1px', fontWeight: 700 }}>
|
|
||||||
PRECISION DATA LINK
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue