Some checks failed
deploy-site / deploy (push) Failing after 13m48s
The Ollama setup guide still led with two retired models while the app already defaults to the newer one, so a fresh setup would install a model the app does not use. The guide now recommends the same model as the rest of the app and lists current lightweight alternatives.
683 lines
24 KiB
TypeScript
683 lines
24 KiB
TypeScript
// src/renderer/components/OllamaGuideModal.tsx
|
|
// Ollama 설치/설정/모델 다운로드 종합 인터랙티브 모달 (v2 Midnight Glass)
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import {
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Box,
|
|
Typography,
|
|
Button,
|
|
Divider,
|
|
IconButton,
|
|
Tooltip,
|
|
Paper,
|
|
Chip,
|
|
LinearProgress,
|
|
TextField,
|
|
CircularProgress,
|
|
Alert,
|
|
} from '@mui/material'
|
|
import {
|
|
X,
|
|
ExternalLink,
|
|
Copy,
|
|
Check,
|
|
RefreshCw,
|
|
Play,
|
|
Download,
|
|
CheckCircle2,
|
|
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'
|
|
|
|
export interface OllamaGuideModalProps {
|
|
open: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
interface RecommendedModel {
|
|
id: string
|
|
name: string
|
|
size: string
|
|
description: string
|
|
recommended?: boolean
|
|
}
|
|
|
|
const RECOMMENDED_MODELS: RecommendedModel[] = [
|
|
{
|
|
id: 'gemma4:e4b',
|
|
name: 'Gemma 4 (E4B)',
|
|
size: '약 4 GB',
|
|
description: '이 앱의 기본 로컬 모델. 추론 토큰을 쓰지 않아 받아쓰기 다듬기에 가장 빠름 (기본 추천)',
|
|
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: 'phi4',
|
|
name: 'Phi 4',
|
|
size: '9.1 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,
|
|
px: 2,
|
|
py: 1,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
boxShadow: d3roShadow.inset,
|
|
}}
|
|
>
|
|
<Typography
|
|
component="span"
|
|
sx={{
|
|
fontFamily: d3roFontMono,
|
|
fontSize: d3roTypo.small.size,
|
|
color: d3roPalette.accent.light,
|
|
letterSpacing: '0.02em',
|
|
userSelect: 'all',
|
|
}}
|
|
>
|
|
{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>('gemma4:e4b')
|
|
const [checking, setChecking] = useState(false)
|
|
const [starting, setStarting] = useState(false)
|
|
const [startMessage, setStartMessage] = useState<string | null>(null)
|
|
|
|
// 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="md"
|
|
fullWidth
|
|
PaperProps={{
|
|
sx: {
|
|
bgcolor: d3roPalette.bg.card,
|
|
backgroundImage: 'none',
|
|
borderRadius: d3roRadius.card,
|
|
border: `1px solid ${d3roPalette.glass.hairlineStrong}`,
|
|
boxShadow: d3roShadow.dialog,
|
|
overflow: 'hidden',
|
|
},
|
|
}}
|
|
>
|
|
<DialogTitle
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
py: 2,
|
|
px: 3,
|
|
borderBottom: `1px solid ${d3roPalette.glass.hairline}`,
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
<Led color={isConnected ? 'green' : 'amber'} pulse={!isConnected} size={9} />
|
|
<Typography
|
|
sx={{
|
|
fontFamily: d3roFontSans,
|
|
fontWeight: 500,
|
|
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>
|
|
|
|
<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>
|
|
</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: 500,
|
|
'&:hover': { bgcolor: d3roPalette.accent.light },
|
|
}}
|
|
>
|
|
{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: 500, fontSize: '10px', height: 20, bgcolor: d3roPalette.accent.dim, 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: d3roTypo.small.size,
|
|
borderColor: d3roPalette.accent.main,
|
|
color: d3roPalette.accent.light,
|
|
'&:hover': {
|
|
borderColor: d3roPalette.accent.main,
|
|
bgcolor: d3roPalette.glass.raised,
|
|
},
|
|
}}
|
|
>
|
|
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: 500, fontSize: '10px', height: 20, bgcolor: d3roPalette.accent.dim, 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: 500, fontSize: '13px', color: d3roPalette.text.primary }}>
|
|
{model.name}
|
|
</Typography>
|
|
{model.recommended && (
|
|
<Chip label="추천" size="small" sx={{ height: 18, fontSize: '9px', fontWeight: 500, 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: 500,
|
|
py: 0.25,
|
|
bgcolor: d3roPalette.accent.main,
|
|
color: d3roPalette.bg.app,
|
|
'&:hover': { bgcolor: d3roPalette.accent.light },
|
|
}}
|
|
>
|
|
다운로드 (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 gemma4:e4b" />
|
|
</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: 500, 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: 500,
|
|
minWidth: 100,
|
|
bgcolor: d3roPalette.accent.main,
|
|
color: d3roPalette.bg.app,
|
|
'&:hover': { bgcolor: d3roPalette.accent.light },
|
|
}}
|
|
>
|
|
{testing ? '처리 중' : '테스트'}
|
|
</Button>
|
|
</Box>
|
|
|
|
{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: 500, mb: 0.5 }}>
|
|
AI 정제 결과 ({activeModel}):
|
|
</Typography>
|
|
<Typography sx={{ fontSize: '12px', color: d3roPalette.text.primary }}>
|
|
{testResult}
|
|
</Typography>
|
|
</Paper>
|
|
)}
|
|
</Box>
|
|
</DialogContent>
|
|
|
|
<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>
|
|
)
|
|
}
|