feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
|
|
@ -1,6 +1,7 @@
|
|||
// src/renderer/components/OllamaGuideModal.tsx
|
||||
// Ollama 설치/설정 안내 모달
|
||||
// Ollama 설치/설정/모델 다운로드 종합 인터랙티브 모달 (v2 Midnight Glass)
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
|
|
@ -11,63 +12,291 @@ import {
|
|||
Button,
|
||||
Divider,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Paper,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
} from '@mui/material'
|
||||
import { X, ExternalLink, Copy } from 'lucide-react'
|
||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||
import {
|
||||
X,
|
||||
ExternalLink,
|
||||
Copy,
|
||||
Check,
|
||||
RefreshCw,
|
||||
Play,
|
||||
Download,
|
||||
CheckCircle2,
|
||||
Cpu,
|
||||
Sparkles,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
d3roPalette,
|
||||
d3roFontSans,
|
||||
d3roFontMono,
|
||||
d3roTypo,
|
||||
d3roShadow,
|
||||
d3roRadius,
|
||||
typoSx,
|
||||
} from '@d3ro/ui/theme'
|
||||
import { Led } from '@d3ro/ui/components/ds'
|
||||
import { useI18n } from '@d3ro/i18n'
|
||||
import type { LLMModel, LLMStatus } from '@d3ro/core/types'
|
||||
|
||||
interface OllamaGuideModalProps {
|
||||
export interface OllamaGuideModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function CodeBlock({ children }: { children: string }): React.ReactElement {
|
||||
interface RecommendedModel {
|
||||
id: string
|
||||
name: string
|
||||
size: string
|
||||
description: string
|
||||
recommended?: boolean
|
||||
}
|
||||
|
||||
const RECOMMENDED_MODELS: RecommendedModel[] = [
|
||||
{
|
||||
id: 'gemma2:2b',
|
||||
name: 'Gemma 2 (2B)',
|
||||
size: '1.6 GB',
|
||||
description: '구글 경량 모델. 빠른 응답 속도와 저사양 PC에 최적화 (기본 추천)',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
id: 'llama3.2:3b',
|
||||
name: 'Llama 3.2 (3B)',
|
||||
size: '2.0 GB',
|
||||
description: '메타의 최신 경량 모델. 텍스트 다듬기 및 문법 교정에 우수',
|
||||
},
|
||||
{
|
||||
id: 'qwen2.5:3b',
|
||||
name: 'Qwen 2.5 (3B)',
|
||||
size: '1.9 GB',
|
||||
description: '한국어 및 다국어 이해도가 매우 뛰어난 고성능 모델',
|
||||
},
|
||||
{
|
||||
id: 'gemma2:9b',
|
||||
name: 'Gemma 2 (9B)',
|
||||
size: '5.4 GB',
|
||||
description: '고성능 모델. 복잡한 요약 및 번역에 적합 (RAM 16GB+ 권장)',
|
||||
},
|
||||
]
|
||||
|
||||
function CodeBlock({ code }: { code: string }): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}, [code])
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
border: `1px solid ${d3roPalette.glass.hairlineStrong}`,
|
||||
borderRadius: d3roRadius.small,
|
||||
p: 1.5,
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: d3roTypo.small.size,
|
||||
color: d3roPalette.accent.main,
|
||||
px: 2,
|
||||
py: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: d3roShadow.inset,
|
||||
}}
|
||||
>
|
||||
<span>{children}</span>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => navigator.clipboard.writeText(children)}
|
||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.main } }}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: d3roTypo.small.size,
|
||||
color: d3roPalette.accent.light,
|
||||
letterSpacing: '0.02em',
|
||||
userSelect: 'all',
|
||||
}}
|
||||
>
|
||||
<Copy size={14} />
|
||||
</IconButton>
|
||||
{code}
|
||||
</Typography>
|
||||
<Tooltip title={copied ? t('common.copy') : t('common.copy')} placement="top">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleCopy}
|
||||
sx={{
|
||||
color: copied ? d3roPalette.tag.green : d3roPalette.text.inactive,
|
||||
p: 0.5,
|
||||
ml: 1,
|
||||
'&:hover': {
|
||||
color: copied ? d3roPalette.tag.green : d3roPalette.accent.light,
|
||||
bgcolor: d3roPalette.glass.raised,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
const [llmStatus, setLlmStatus] = useState<LLMStatus | null>(null)
|
||||
const [installedModels, setInstalledModels] = useState<LLMModel[]>([])
|
||||
const [activeModel, setActiveModel] = useState<string>('gemma2:2b')
|
||||
const [checking, setChecking] = useState(false)
|
||||
const [starting, setStarting] = useState(false)
|
||||
const [startMessage, setStartMessage] = useState<string | null>(null)
|
||||
|
||||
const handleOpenLink = (url: string) => {
|
||||
// Download state
|
||||
const [pullingModelId, setPullingModelId] = useState<string | null>(null)
|
||||
const [pullProgress, setPullProgress] = useState<{
|
||||
status: string
|
||||
percent: number
|
||||
completed: number
|
||||
total: number
|
||||
}>({ status: '', percent: 0, completed: 0, total: 0 })
|
||||
|
||||
// Test prompt state
|
||||
const [testPrompt, setTestPrompt] = useState('안녕하세요 오늘의 날씨를 알려줘')
|
||||
const [testResult, setTestResult] = useState<string | null>(null)
|
||||
const [testing, setTesting] = useState(false)
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
setChecking(true)
|
||||
try {
|
||||
const res = await window.electronAPI.llm.checkConnection()
|
||||
if (res.success && res.data) {
|
||||
setInstalledModels(res.data.models)
|
||||
}
|
||||
const st = await window.electronAPI.llm.getStatus()
|
||||
if (st.success) {
|
||||
setLlmStatus(st.data)
|
||||
}
|
||||
const active = await window.electronAPI.llm.getActiveModel()
|
||||
if (active.success && active.data) {
|
||||
setActiveModel(active.data)
|
||||
}
|
||||
} finally {
|
||||
setChecking(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
refreshStatus()
|
||||
|
||||
const unsubPull = window.electronAPI.llm.onPullProgress((e) => {
|
||||
setPullingModelId(e.modelId)
|
||||
setPullProgress({
|
||||
status: e.status,
|
||||
percent: e.percent,
|
||||
completed: e.completed,
|
||||
total: e.total,
|
||||
})
|
||||
if (e.percent >= 100 || e.status === 'success') {
|
||||
setTimeout(() => {
|
||||
setPullingModelId(null)
|
||||
refreshStatus()
|
||||
}, 1200)
|
||||
}
|
||||
})
|
||||
|
||||
const unsubStatus = window.electronAPI.llm.onStatusChanged((e) => {
|
||||
setLlmStatus(e.status)
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubPull()
|
||||
unsubStatus()
|
||||
}
|
||||
}, [open, refreshStatus])
|
||||
|
||||
const handleOpenLink = useCallback((url: string) => {
|
||||
window.electronAPI.system.openExternal({ url })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleStartOllama = useCallback(async () => {
|
||||
setStarting(true)
|
||||
setStartMessage(null)
|
||||
try {
|
||||
const res = await window.electronAPI.llm.startServer()
|
||||
if (res.success) {
|
||||
if (res.data === 'running') {
|
||||
setStartMessage('Ollama가 이미 실행 중입니다.')
|
||||
} else if (res.data === 'starting') {
|
||||
setStartMessage('Ollama 서버를 시작했습니다. 연결 확인 중...')
|
||||
setTimeout(() => refreshStatus(), 2000)
|
||||
} else if (res.data === 'not-installed') {
|
||||
setStartMessage('Ollama가 설치되어 있지 않습니다. 다운로드 버튼을 눌러 설치해 주세요.')
|
||||
} else {
|
||||
setStartMessage('Ollama 실행에 실패했습니다. 수동으로 Ollama를 실행해 주세요.')
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setStarting(false)
|
||||
}
|
||||
}, [refreshStatus])
|
||||
|
||||
const handlePullModel = useCallback(async (modelId: string) => {
|
||||
setPullingModelId(modelId)
|
||||
setPullProgress({ status: '다운로드 시작 중...', percent: 0, completed: 0, total: 0 })
|
||||
try {
|
||||
await window.electronAPI.llm.pullModel({ modelId })
|
||||
} catch {
|
||||
setPullingModelId(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSelectModel = useCallback(async (modelId: string) => {
|
||||
await window.electronAPI.llm.setModel({ modelId })
|
||||
await window.electronAPI.config.set({ key: 'llmModelId', value: modelId })
|
||||
setActiveModel(modelId)
|
||||
await refreshStatus()
|
||||
}, [refreshStatus])
|
||||
|
||||
const handleRunTest = useCallback(async () => {
|
||||
if (!testPrompt.trim()) return
|
||||
setTesting(true)
|
||||
setTestResult(null)
|
||||
try {
|
||||
const res = await window.electronAPI.llm.process({
|
||||
text: testPrompt,
|
||||
action: 'refine',
|
||||
})
|
||||
if (res.success && res.data) {
|
||||
setTestResult(res.data.processedText)
|
||||
} else {
|
||||
setTestResult(`오류: ${res.error?.message ?? '응답 실패'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
setTestResult(`실패: ${err instanceof Error ? err.message : String(err)}`)
|
||||
} finally {
|
||||
setTesting(false)
|
||||
}
|
||||
}, [testPrompt])
|
||||
|
||||
const isConnected = llmStatus?.connectionState === 'connected'
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: d3roPalette.bg.chassis,
|
||||
bgcolor: d3roPalette.bg.card,
|
||||
backgroundImage: 'none',
|
||||
border: `1px solid ${d3roPalette.border.subtle}`,
|
||||
borderRadius: d3roRadius.card,
|
||||
border: `1px solid ${d3roPalette.glass.hairlineStrong}`,
|
||||
boxShadow: d3roShadow.dialog,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
@ -76,86 +305,382 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac
|
|||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontFamily: d3roFontMono,
|
||||
fontWeight: 700,
|
||||
fontSize: '14px',
|
||||
letterSpacing: '1px',
|
||||
color: d3roPalette.accent.main,
|
||||
py: 1.5,
|
||||
py: 2,
|
||||
px: 3,
|
||||
borderBottom: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
}}
|
||||
>
|
||||
{t('ollama.title')}
|
||||
<IconButton onClick={onClose} size="small" sx={{ color: d3roPalette.text.inactive }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Led color={isConnected ? 'green' : 'amber'} pulse={!isConnected} size={9} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: d3roFontSans,
|
||||
fontWeight: 700,
|
||||
fontSize: d3roTypo.heading.size,
|
||||
color: d3roPalette.text.primary,
|
||||
}}
|
||||
>
|
||||
Ollama 로컬 AI 설정 & 모델 관리
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
size="small"
|
||||
sx={{
|
||||
color: d3roPalette.text.inactive,
|
||||
'&:hover': { color: d3roPalette.text.primary, bgcolor: d3roPalette.glass.raised },
|
||||
}}
|
||||
>
|
||||
<X size={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 }}>
|
||||
{t('ollama.step1.title')}
|
||||
|
||||
<DialogContent sx={{ px: 3, py: 2.5, display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
{/* ── 실시간 상태 바 ── */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
border: `1px solid ${isConnected ? d3roPalette.tag.green : d3roPalette.glass.hairlineStrong}`,
|
||||
borderRadius: d3roRadius.small,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Led color={isConnected ? 'green' : 'red'} size={8} />
|
||||
<Box>
|
||||
<Typography sx={{ ...typoSx('heading'), fontSize: '13px', color: isConnected ? d3roPalette.tag.green : d3roPalette.tag.red }}>
|
||||
{isConnected
|
||||
? `Ollama 연결 완료 ${llmStatus?.serverVersion ? `(v${llmStatus.serverVersion})` : ''}`
|
||||
: 'Ollama 오프라인 (서버 실행 필요)'}
|
||||
</Typography>
|
||||
<Typography sx={{ ...typoSx('small'), color: d3roPalette.text.secondary }}>
|
||||
서버 URL: {llmStatus?.serverUrl || 'http://localhost:11434'} | 활성 모델: {activeModel || '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 1.5 }}>
|
||||
{t('ollama.step1.desc')}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<RefreshCw size={13} className={checking ? 'animate-spin' : ''} />}
|
||||
onClick={refreshStatus}
|
||||
disabled={checking}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: '11px',
|
||||
borderColor: d3roPalette.glass.hairlineStrong,
|
||||
color: d3roPalette.text.primary,
|
||||
}}
|
||||
>
|
||||
연결 확인
|
||||
</Button>
|
||||
{!isConnected && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<Play size={13} />}
|
||||
onClick={handleStartOllama}
|
||||
disabled={starting}
|
||||
sx={{
|
||||
fontFamily: d3roFontSans,
|
||||
fontSize: '11px',
|
||||
bgcolor: d3roPalette.accent.main,
|
||||
color: d3roPalette.bg.app,
|
||||
fontWeight: 700,
|
||||
'&:hover': { bgcolor: d3roPalette.accent.hover },
|
||||
}}
|
||||
>
|
||||
{starting ? '실행 중...' : 'Ollama 자동 실행'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{startMessage && (
|
||||
<Alert severity={isConnected ? 'success' : 'info'} sx={{ py: 0.5 }}>
|
||||
{startMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* ── Step 1: Ollama 설치 및 기동 ── */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip label="STEP 1" size="small" sx={{ fontWeight: 700, fontSize: '10px', height: 20, bgcolor: d3roPalette.accent.muted, color: d3roPalette.accent.main }} />
|
||||
<Typography sx={{ ...typoSx('heading'), color: d3roPalette.text.primary }}>
|
||||
Ollama 설치 및 백그라운드 실행
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ ...typoSx('body'), color: d3roPalette.text.secondary, pl: 1 }}>
|
||||
Ollama는 PC 내에서 100% 무료, 오프라인으로 LLM을 구동하는 오픈소스 엔진입니다.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ pl: 1, display: 'flex', gap: 1.5, flexWrap: 'wrap', pt: 0.5 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
endIcon={<ExternalLink size={14} />}
|
||||
onClick={() => handleOpenLink('https://ollama.com/download')}
|
||||
sx={{ fontFamily: d3roFontMono, fontSize: '11px' }}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: d3roTypo.small.size,
|
||||
borderColor: d3roPalette.accent.main,
|
||||
color: d3roPalette.accent.light,
|
||||
'&:hover': {
|
||||
borderColor: d3roPalette.accent.main,
|
||||
bgcolor: d3roPalette.glass.raised,
|
||||
},
|
||||
}}
|
||||
>
|
||||
ollama.com/download
|
||||
1. Ollama 다운로드 (ollama.com/download)
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<Play size={14} />}
|
||||
onClick={handleStartOllama}
|
||||
disabled={starting}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: d3roTypo.small.size,
|
||||
borderColor: d3roPalette.glass.hairlineStrong,
|
||||
color: d3roPalette.text.primary,
|
||||
}}
|
||||
>
|
||||
2. 설치 후 자동 기동
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ borderColor: d3roPalette.glass.hairline }} />
|
||||
|
||||
{/* ── Step 2: 추천 모델 다운로드 ── */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip label="STEP 2" size="small" sx={{ fontWeight: 700, fontSize: '10px', height: 20, bgcolor: d3roPalette.accent.muted, color: d3roPalette.accent.main }} />
|
||||
<Typography sx={{ ...typoSx('heading'), color: d3roPalette.text.primary }}>
|
||||
한국어 최적화 추천 모델 1-클릭 다운로드
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: '11px', color: d3roPalette.text.dimLabel, fontFamily: d3roFontMono }}>
|
||||
설치된 모델: {installedModels.length}개
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ ...typoSx('body'), color: d3roPalette.text.secondary, pl: 1 }}>
|
||||
음성 인식 후 텍스트 다듬기, 번역, 요약에 가장 안정적인 경량 AI 모델들입니다.
|
||||
</Typography>
|
||||
|
||||
{/* 모델 카드 그리드 */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 1.5, pl: 1 }}>
|
||||
{RECOMMENDED_MODELS.map((model) => {
|
||||
const isInstalled = installedModels.some((m) => m.name === model.id || m.name.startsWith(model.id.split(':')[0]))
|
||||
const isActive = activeModel === model.id || activeModel.startsWith(model.id.split(':')[0])
|
||||
const isDownloading = pullingModelId === model.id
|
||||
|
||||
return (
|
||||
<Paper
|
||||
key={model.id}
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 1.75,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
border: `1px solid ${isActive ? d3roPalette.accent.main : d3roPalette.glass.hairlineStrong}`,
|
||||
borderRadius: d3roRadius.small,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: '13px', color: d3roPalette.text.primary }}>
|
||||
{model.name}
|
||||
</Typography>
|
||||
{model.recommended && (
|
||||
<Chip label="추천" size="small" sx={{ height: 18, fontSize: '9px', fontWeight: 700, bgcolor: d3roPalette.tag.greenBg, color: d3roPalette.tag.green }} />
|
||||
)}
|
||||
</Box>
|
||||
<Chip label={model.size} size="small" sx={{ height: 18, fontSize: '10px', fontFamily: d3roFontMono }} />
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: '11px', color: d3roPalette.text.secondary, lineHeight: 1.4 }}>
|
||||
{model.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 다운로드 진행률 또는 액션 버튼 */}
|
||||
{isDownloading ? (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography sx={{ fontSize: '10px', color: d3roPalette.accent.light }}>
|
||||
{pullProgress.status || '다운로드 중...'}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '10px', fontFamily: d3roFontMono, color: d3roPalette.text.primary }}>
|
||||
{pullProgress.percent}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress variant="determinate" value={pullProgress.percent} sx={{ height: 4, borderRadius: 2 }} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 1, mt: 0.5 }}>
|
||||
{isInstalled ? (
|
||||
<Button
|
||||
size="small"
|
||||
variant={isActive ? 'contained' : 'outlined'}
|
||||
onClick={() => handleSelectModel(model.id)}
|
||||
startIcon={isActive ? <CheckCircle2 size={13} /> : undefined}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: '11px',
|
||||
py: 0.25,
|
||||
bgcolor: isActive ? d3roPalette.tag.green : 'transparent',
|
||||
borderColor: isActive ? d3roPalette.tag.green : d3roPalette.glass.hairlineStrong,
|
||||
color: isActive ? d3roPalette.bg.app : d3roPalette.text.primary,
|
||||
'&:hover': {
|
||||
bgcolor: isActive ? d3roPalette.tag.green : d3roPalette.glass.raised,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isActive ? '사용 중' : '기본으로 선택'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<Download size={13} />}
|
||||
onClick={() => handlePullModel(model.id)}
|
||||
disabled={!isConnected || Boolean(pullingModelId)}
|
||||
sx={{
|
||||
fontFamily: d3roFontSans,
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
py: 0.25,
|
||||
bgcolor: d3roPalette.accent.main,
|
||||
color: d3roPalette.bg.app,
|
||||
'&:hover': { bgcolor: d3roPalette.accent.hover },
|
||||
}}
|
||||
>
|
||||
다운로드 (Pull)
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* 터미널 명령어 수동 입력 안내 */}
|
||||
<Box sx={{ pl: 1, display: 'flex', flexDirection: 'column', gap: 1, pt: 1 }}>
|
||||
<Typography sx={{ fontSize: '11px', color: d3roPalette.text.dimLabel }}>
|
||||
직접 터미널에서 다운로드하려면 아래 명령어를 실행하세요:
|
||||
</Typography>
|
||||
<CodeBlock code="ollama pull gemma2:2b" />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ borderColor: d3roPalette.glass.hairline }} />
|
||||
|
||||
{/* ── Step 3: 실시간 연결 및 테스트 ── */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip label="STEP 3" size="small" sx={{ fontWeight: 700, fontSize: '10px', height: 20, bgcolor: d3roPalette.tag.greenBg, color: d3roPalette.tag.green }} />
|
||||
<Typography sx={{ ...typoSx('heading'), color: d3roPalette.text.primary }}>
|
||||
동작 테스트 (AI 문장 정제)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ pl: 1, display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={testPrompt}
|
||||
onChange={(e) => setTestPrompt(e.target.value)}
|
||||
placeholder="테스트할 음성 전사 문장 입력"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
fontSize: '12px',
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleRunTest}
|
||||
disabled={!isConnected || testing}
|
||||
startIcon={testing ? <CircularProgress size={12} color="inherit" /> : <Zap size={14} />}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
minWidth: 100,
|
||||
bgcolor: d3roPalette.accent.main,
|
||||
color: d3roPalette.bg.app,
|
||||
'&:hover': { bgcolor: d3roPalette.accent.hover },
|
||||
}}
|
||||
>
|
||||
{testing ? '처리 중' : '테스트'}
|
||||
</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 }}>
|
||||
{t('ollama.step2.title')}
|
||||
{testResult && (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
ml: 1,
|
||||
p: 1.5,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
border: `1px solid ${d3roPalette.glass.hairlineStrong}`,
|
||||
borderRadius: d3roRadius.small,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '11px', color: d3roPalette.accent.light, fontWeight: 700, mb: 0.5 }}>
|
||||
AI 정제 결과 ({activeModel}):
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 1.5 }}>
|
||||
{t('ollama.step2.desc')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<CodeBlock>ollama pull gemma4:e4b</CodeBlock>
|
||||
<Typography variant="caption" sx={{ color: d3roPalette.text.inactive }}>
|
||||
{t('ollama.step2.alt')}
|
||||
<Typography sx={{ fontSize: '12px', color: d3roPalette.text.primary }}>
|
||||
{testResult}
|
||||
</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 }}>
|
||||
{t('ollama.step3.title')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary }}>
|
||||
{t('ollama.step3.desc')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2, bgcolor: d3roPalette.bg.app }}>
|
||||
<Button onClick={onClose} variant="contained">
|
||||
|
||||
<DialogActions
|
||||
sx={{
|
||||
px: 3,
|
||||
py: 2,
|
||||
borderTop: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '11px', color: d3roPalette.text.dimLabel }}>
|
||||
D3RO Voice는 인터넷 없이도 로컬에서 100% 프라이빗하게 작동합니다.
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="contained"
|
||||
sx={{
|
||||
fontFamily: d3roFontSans,
|
||||
fontSize: d3roTypo.small.size,
|
||||
fontWeight: 600,
|
||||
borderRadius: d3roRadius.button,
|
||||
}}
|
||||
>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue