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
|
|
@ -24,19 +24,22 @@ import {
|
|||
Stack,
|
||||
Paper,
|
||||
} from '@mui/material'
|
||||
import { X, Keyboard, Pencil, Mic, Lock, Cloud } from 'lucide-react'
|
||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||
import { X, Keyboard, Pencil, Mic, Lock, Cloud, RefreshCw, Play, Cpu, HelpCircle, Sparkles, CheckCircle2 } from 'lucide-react'
|
||||
import { d3roPalette, d3roFontMono, d3roShadow, d3roRadius, d3roTypo } from '@d3ro/ui/theme'
|
||||
import { Led } from '@d3ro/ui/components/ds'
|
||||
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
||||
import { formatHotkeyLabel, formatHotkeySegments } from '../utils/format-hotkey'
|
||||
import { LicenseTab } from './LicenseTab'
|
||||
import { CloudSyncSection } from './CloudSyncSection'
|
||||
import { STTTab } from './STTTab'
|
||||
import { useI18n, LOCALE_META } from '@d3ro/i18n'
|
||||
import type { Locale } from '@d3ro/i18n'
|
||||
import type { ThemeMode, AppConfig, HotkeyBinding, AudioDevice } from '@d3ro/core/types'
|
||||
import type { ThemeMode, AppConfig, HotkeyBinding, AudioDevice, LLMModel, LLMStatus } from '@d3ro/core/types'
|
||||
import { Feature } from '@d3ro/core/types'
|
||||
|
||||
interface SettingsModalProps {
|
||||
open: boolean
|
||||
initialTab?: number
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
|
|
@ -181,12 +184,18 @@ function VoiceModeCard({
|
|||
)
|
||||
}
|
||||
|
||||
export function SettingsModal({ open, onClose }: SettingsModalProps): React.ReactElement {
|
||||
export function SettingsModal({ open, initialTab = 0, onClose }: SettingsModalProps): React.ReactElement {
|
||||
const { t, locale, setLocale } = useI18n()
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
const [activeTab, setActiveTab] = useState(initialTab)
|
||||
const [config, setConfig] = useState<Partial<AppConfig>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && initialTab !== undefined) {
|
||||
setActiveTab(initialTab)
|
||||
}
|
||||
}, [open, initialTab])
|
||||
|
||||
// 음성 모드 상태
|
||||
const [dictationEnabled, setDictationEnabled] = useState(true)
|
||||
const [dictationBinding, setDictationBinding] = useState<HotkeyBinding | null>(null)
|
||||
|
|
@ -199,8 +208,6 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
|||
const [hotkeyModalOpen, setHotkeyModalOpen] = useState(false)
|
||||
const [hotkeyModalTarget, setHotkeyModalTarget] = useState<'dictation' | 'handsFree' | 'caption'>('dictation')
|
||||
|
||||
// LLM 모델 목록
|
||||
const [llmModels, setLlmModels] = useState<Array<{ id: string; name: string; parameterSize: string }>>([])
|
||||
|
||||
// 오디오 디바이스
|
||||
const [audioDevices, setAudioDevices] = useState<AudioDevice[]>([])
|
||||
|
|
@ -208,10 +215,32 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
|||
const [micTesting, setMicTesting] = useState(false)
|
||||
const [micLevel, setMicLevel] = useState(0)
|
||||
|
||||
// Ollama 상태
|
||||
const [ollamaStatus, setOllamaStatus] = useState<LLMStatus | null>(null)
|
||||
const [ollamaModels, setOllamaModels] = useState<LLMModel[]>([])
|
||||
const [checkingOllama, setCheckingOllama] = useState(false)
|
||||
const [startingOllama, setStartingOllama] = useState(false)
|
||||
const [ollamaFeedback, setOllamaFeedback] = useState<string | null>(null)
|
||||
|
||||
const refreshOllama = useCallback(async () => {
|
||||
setCheckingOllama(true)
|
||||
try {
|
||||
const res = await window.electronAPI.llm.checkConnection()
|
||||
if (res.success && res.data) {
|
||||
setOllamaModels(res.data.models)
|
||||
}
|
||||
const st = await window.electronAPI.llm.getStatus()
|
||||
if (st.success) setOllamaStatus(st.data)
|
||||
} finally {
|
||||
setCheckingOllama(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 설정 로드
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setLoading(true)
|
||||
refreshOllama()
|
||||
|
||||
Promise.all([
|
||||
window.electronAPI.config.getAll(),
|
||||
|
|
@ -240,11 +269,7 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
|||
if (selectedResult.success && selectedResult.data) setSelectedDeviceId(selectedResult.data)
|
||||
})
|
||||
|
||||
// LLM 모델 목록 로드
|
||||
window.electronAPI.llm.getModels().then((resp) => {
|
||||
if (resp.success) setLlmModels(resp.data)
|
||||
})
|
||||
}, [open])
|
||||
}, [open, refreshOllama])
|
||||
|
||||
const updateConfig = useCallback((key: keyof AppConfig, value: AppConfig[keyof AppConfig]) => {
|
||||
setConfig((prev) => ({ ...prev, [key]: value }))
|
||||
|
|
@ -618,185 +643,189 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
|||
|
||||
{/* ── STT 탭 ───────────────────────────────── */}
|
||||
<TabPanel value={activeTab} index={2}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<FormControl size="small">
|
||||
<InputLabel>{t('settings.whisperModel')}</InputLabel>
|
||||
<Select
|
||||
label={t('settings.whisperModel')}
|
||||
value={config.sttModelId ?? 'base'}
|
||||
onChange={(e) => updateConfig('sttModelId', e.target.value)}
|
||||
>
|
||||
<MenuItem value="tiny">{t('settings.model.tiny')}</MenuItem>
|
||||
<MenuItem value="base">{t('settings.model.base')}</MenuItem>
|
||||
<MenuItem value="small">{t('settings.model.small')}</MenuItem>
|
||||
<MenuItem value="medium">{t('settings.model.medium')}</MenuItem>
|
||||
<MenuItem value="large-v3">{t('settings.model.large')}</MenuItem>
|
||||
<MenuItem value="large-v3-turbo">{t('settings.model.largeTurbo')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{t('settings.sttLanguage')}</InputLabel>
|
||||
<Select
|
||||
label={t('settings.sttLanguage')}
|
||||
value={config.sttLanguage ?? 'auto'}
|
||||
onChange={(e) => updateConfig('sttLanguage', e.target.value)}
|
||||
>
|
||||
<MenuItem value="auto">{t('settings.sttLang.auto')}</MenuItem>
|
||||
<MenuItem value="ko">{t('settings.sttLang.ko')}</MenuItem>
|
||||
<MenuItem value="en">{t('settings.sttLang.en')}</MenuItem>
|
||||
<MenuItem value="ja">{t('settings.sttLang.ja')}</MenuItem>
|
||||
<MenuItem value="zh">{t('settings.sttLang.zh')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px' }}>
|
||||
{t('settings.diarization')}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label={t('settings.hfToken')}
|
||||
type="password"
|
||||
value={(config as Record<string, unknown>)['hfToken'] as string ?? ''}
|
||||
onChange={(e) => updateConfig('hfToken' as keyof AppConfig, e.target.value as never)}
|
||||
fullWidth
|
||||
size="small"
|
||||
helperText={t('settings.hfTokenHint')}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={(config as Record<string, unknown>)['diarizationEnabled'] as boolean ?? false}
|
||||
onChange={(e) => updateConfig('diarizationEnabled' as keyof AppConfig, e.target.checked as never)}
|
||||
size="small"
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: d3roPalette.tag.purple },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
|
||||
backgroundColor: d3roPalette.tag.purple,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontSize: d3roTypo.small.size }}>{t('settings.diarization')}</Typography>
|
||||
<Typography variant="caption" sx={{ color: d3roPalette.text.inactive, fontSize: '11px' }}>
|
||||
{t('settings.diarizationHint')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<STTTab config={config} updateConfig={updateConfig} />
|
||||
</TabPanel>
|
||||
|
||||
{/* ── LLM 탭 ──────────────────────────────── */}
|
||||
<TabPanel value={activeTab} index={3}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px' }}>
|
||||
{t('settings.llmBackend')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
{/* LLM 백엔드 선택 */}
|
||||
<Box>
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px', mb: 1, display: 'block' }}>
|
||||
AI 처리 백엔드 선택
|
||||
</Typography>
|
||||
<FormControl size="small" fullWidth>
|
||||
<Select
|
||||
value={config.llmBackend ?? 'local'}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value as 'local' | 'online'
|
||||
updateConfig('llmBackend', val)
|
||||
updateConfig('appUsageMode', val)
|
||||
}}
|
||||
>
|
||||
<MenuItem value="local">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Cpu size={16} color={d3roPalette.tag.green} />
|
||||
<span>로컬 Ollama 엔진 (100% 무료 / 오프라인 독립 실행)</span>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="online">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Cloud size={16} color={d3roPalette.tag.blue} />
|
||||
<span>D3RO 온라인 클라우드 API</span>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{t('settings.llmBackend')}</InputLabel>
|
||||
<Select
|
||||
label={t('settings.llmBackend')}
|
||||
value={config.llmBackend ?? 'local'}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value
|
||||
updateConfig('llmBackend', next)
|
||||
if (next === 'premium') {
|
||||
// Premium 선택 시 항상 라이선스 모달 — 현재 티어/쿼터/업그레이드 안내
|
||||
window.dispatchEvent(new Event('d3ro:open-license-modal'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="local">{t('settings.backend.local')}</MenuItem>
|
||||
<MenuItem value="premium">{t('settings.backend.premium')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{/* 로컬 Ollama 설정 */}
|
||||
{config.llmBackend !== 'online' && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{/* Ollama 연결 상태 카드 */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
border: `1px solid ${ollamaStatus?.connectionState === 'connected' ? d3roPalette.tag.green : d3roPalette.border.default}`,
|
||||
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={ollamaStatus?.connectionState === 'connected' ? 'green' : 'red'} size={8} />
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: '13px', color: ollamaStatus?.connectionState === 'connected' ? d3roPalette.tag.green : d3roPalette.tag.red }}>
|
||||
{ollamaStatus?.connectionState === 'connected'
|
||||
? `Ollama 연결됨 ${ollamaStatus.serverVersion ? `(v${ollamaStatus.serverVersion})` : ''}`
|
||||
: 'Ollama 오프라인 (서버 실행 필요)'}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '11px', color: d3roPalette.text.secondary }}>
|
||||
설치된 모델: {ollamaModels.length}개 | 현재 활성: {config.llmModelId ?? 'gemma2:2b'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ color: d3roPalette.text.inactive, fontSize: d3roTypo.label.size }}>
|
||||
{config.llmBackend === 'premium'
|
||||
? t('settings.backend.premiumHint')
|
||||
: t('settings.backend.localHint')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<RefreshCw size={13} className={checkingOllama ? 'animate-spin' : ''} />}
|
||||
onClick={refreshOllama}
|
||||
disabled={checkingOllama}
|
||||
sx={{ fontSize: '11px', fontFamily: d3roFontMono }}
|
||||
>
|
||||
연결 확인
|
||||
</Button>
|
||||
{ollamaStatus?.connectionState !== 'connected' && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<Play size={13} />}
|
||||
onClick={async () => {
|
||||
setStartingOllama(true)
|
||||
setOllamaFeedback(null)
|
||||
try {
|
||||
const res = await window.electronAPI.llm.startServer()
|
||||
if (res.success) {
|
||||
if (res.data === 'running') setOllamaFeedback('Ollama가 이미 실행 중입니다.')
|
||||
else if (res.data === 'starting') {
|
||||
setOllamaFeedback('Ollama 서버를 시작했습니다.')
|
||||
setTimeout(() => refreshOllama(), 2000)
|
||||
} else if (res.data === 'not-installed') {
|
||||
setOllamaFeedback('Ollama가 설치되어 있지 않습니다. 가이드를 열어 설치해 주세요.')
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setStartingOllama(false)
|
||||
}
|
||||
}}
|
||||
disabled={startingOllama}
|
||||
sx={{ fontSize: '11px', bgcolor: d3roPalette.accent.main, color: d3roPalette.bg.app, fontWeight: 700 }}
|
||||
>
|
||||
{startingOllama ? '실행 중' : 'Ollama 실행'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
startIcon={<HelpCircle size={13} />}
|
||||
onClick={() => window.dispatchEvent(new CustomEvent('d3ro:open-ollama-guide'))}
|
||||
sx={{ fontSize: '11px', color: d3roPalette.accent.main }}
|
||||
>
|
||||
가이드/모델 다운로드
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||
{ollamaFeedback && (
|
||||
<Typography sx={{ fontSize: '11px', color: d3roPalette.tag.orange }}>
|
||||
{ollamaFeedback}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px' }}>
|
||||
{t('settings.conversationBackend')}
|
||||
</Typography>
|
||||
{/* 서버 URL & 모델 선택 */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2 }}>
|
||||
<TextField
|
||||
label={t('settings.ollamaUrl')}
|
||||
value={config.ollamaServerUrl ?? 'http://localhost:11434'}
|
||||
onChange={(e) => updateConfig('ollamaServerUrl', e.target.value)}
|
||||
fullWidth
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{t('settings.conversationBackend')}</InputLabel>
|
||||
<Select
|
||||
label={t('settings.conversationBackend')}
|
||||
value={config.conversationBackend ?? 'local'}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value
|
||||
updateConfig('conversationBackend', next)
|
||||
if (next === 'realtime') {
|
||||
// Realtime은 로그인+구독 필요 — 라이선스 모달로 현재 티어 안내
|
||||
window.dispatchEvent(new Event('d3ro:open-license-modal'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="local">{t('settings.convBackend.local')}</MenuItem>
|
||||
<MenuItem value="realtime">{t('settings.convBackend.realtime')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Typography variant="body2" sx={{ color: d3roPalette.text.inactive, fontSize: d3roTypo.label.size }}>
|
||||
{config.conversationBackend === 'realtime'
|
||||
? t('settings.convBackend.realtimeHint')
|
||||
: t('settings.convBackend.localHint')}
|
||||
</Typography>
|
||||
|
||||
{config.llmBackend !== 'premium' && (
|
||||
<>
|
||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px' }}>
|
||||
{t('settings.ollamaServer')}
|
||||
</Typography>
|
||||
<FormControl size="small" fullWidth>
|
||||
<InputLabel>활성 Ollama 모델</InputLabel>
|
||||
<Select
|
||||
label="활성 Ollama 모델"
|
||||
value={config.llmModelId ?? 'gemma2:2b'}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
updateConfig('llmModelId', val)
|
||||
window.electronAPI.llm.setModel({ modelId: val })
|
||||
}}
|
||||
>
|
||||
{/* 설치된 모델 목록 */}
|
||||
{ollamaModels.map((m) => (
|
||||
<MenuItem key={m.id} value={m.name}>
|
||||
{m.name} ({Math.round(m.sizeBytes / (1024 * 1024 * 1024) * 10) / 10} GB)
|
||||
</MenuItem>
|
||||
))}
|
||||
{/* 설치된 모델 목록이 없거나 기본 추천 추가 */}
|
||||
{ollamaModels.length === 0 && (
|
||||
<MenuItem value="gemma2:2b">gemma2:2b (추천)</MenuItem>
|
||||
)}
|
||||
{!ollamaModels.some((m) => m.name === 'llama3.2:3b') && (
|
||||
<MenuItem value="llama3.2:3b">llama3.2:3b</MenuItem>
|
||||
)}
|
||||
{!ollamaModels.some((m) => m.name === 'qwen2.5:3b') && (
|
||||
<MenuItem value="qwen2.5:3b">qwen2.5:3b</MenuItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 온라인 API 설정 */}
|
||||
{config.llmBackend === 'online' && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<TextField
|
||||
label={t('settings.ollamaUrl')}
|
||||
value={config.ollamaServerUrl ?? 'http://localhost:11434'}
|
||||
onChange={(e) => updateConfig('ollamaServerUrl', e.target.value)}
|
||||
label="API 서버 URL"
|
||||
value={config.onlineApiUrl ?? 'http://localhost:5000'}
|
||||
onChange={(e) => updateConfig('onlineApiUrl', e.target.value)}
|
||||
fullWidth
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<Typography variant="body2" sx={{ color: d3roPalette.text.inactive, fontSize: d3roTypo.label.size }}>
|
||||
{t('settings.ollamaHint')}
|
||||
계정 상태: {config.authToken ? `${config.userEmail ?? '로그인됨'} (온라인 API 가용)` : '로그인 필요'}
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px' }}>
|
||||
{t('settings.llmModel')}
|
||||
</Typography>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{t('settings.llmModel')}</InputLabel>
|
||||
<Select
|
||||
label={t('settings.llmModel')}
|
||||
value={(config as Record<string, unknown>)['llmModelId'] as string ?? ''}
|
||||
onChange={(e) => {
|
||||
updateConfig('llmModelId' as keyof AppConfig, e.target.value as never)
|
||||
}}
|
||||
>
|
||||
{llmModels.map((model) => (
|
||||
<MenuItem key={model.id} value={model.id}>
|
||||
{model.name} ({model.parameterSize})
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue