Phase 9 완료: 전체 남은 기능 구현
1. RecordingTip: thinking 상태 전환 시 opacity 보장 2. 히스토리 오디오 경로: WAV 파일 경로를 DB에 기록 3. 마이크 테스트 UI: Settings Audio 탭에 TEST 버튼 + 레벨 바 4. 온보딩 플로우: 첫 실행 시 3단계 안내 (환영→마이크→핫키→완료) 5. MetalDial 컴포넌트: 동심원 그루브, 회전 포인터, LED 인디케이터
This commit is contained in:
parent
d7074e359d
commit
c338a19c90
7 changed files with 431 additions and 4 deletions
231
src/renderer/components/OnboardingModal.tsx
Normal file
231
src/renderer/components/OnboardingModal.tsx
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// src/renderer/components/OnboardingModal.tsx
|
||||
// 첫 실행 시 마이크 + 핫키 설정 안내
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Stack,
|
||||
Chip,
|
||||
} from '@mui/material'
|
||||
import MicIcon from '@mui/icons-material/Mic'
|
||||
import KeyboardIcon from '@mui/icons-material/Keyboard'
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||
import { d3roPalette, d3roFontMono } from '../theme'
|
||||
import { Led } from './ds'
|
||||
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
||||
import type { HotkeyBinding, AudioDevice } from '@shared/types'
|
||||
|
||||
interface OnboardingModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.ReactElement {
|
||||
const [step, setStep] = useState(0) // 0: 환영, 1: 마이크, 2: 핫키, 3: 완료
|
||||
const [devices, setDevices] = useState<AudioDevice[]>([])
|
||||
const [selectedDevice, setSelectedDevice] = useState('default')
|
||||
const [hotkeyBinding, setHotkeyBinding] = useState<HotkeyBinding | null>(null)
|
||||
const [hotkeyModalOpen, setHotkeyModalOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setStep(0)
|
||||
window.electronAPI.audio.getDevices().then((r) => {
|
||||
if (r.success) setDevices(r.data)
|
||||
})
|
||||
window.electronAPI.hotkey.getDictationShortcut().then((r) => {
|
||||
if (r.success && r.data) setHotkeyBinding(r.data)
|
||||
})
|
||||
}, [open])
|
||||
|
||||
const handleFinish = () => {
|
||||
// 온보딩 완료 플래그 저장
|
||||
window.electronAPI.config.set({ key: 'onboardingCompleted' as keyof import('@shared/types').AppConfig, value: true as never })
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleHotkeySave = (binding: HotkeyBinding) => {
|
||||
setHotkeyBinding(binding)
|
||||
window.electronAPI.hotkey.setDictationShortcut({ binding })
|
||||
window.electronAPI.hotkey.setEnabled({ enabled: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: d3roPalette.bg.chassis,
|
||||
backgroundImage: 'none',
|
||||
border: `1px solid ${d3roPalette.border.subtle}`,
|
||||
boxShadow: '0 40px 80px rgba(0,0,0,0.8)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogContent sx={{ p: 4 }}>
|
||||
{/* Step 0: 환영 */}
|
||||
{step === 0 && (
|
||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||
<Led color="amber" pulse size={16} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: '24px',
|
||||
fontWeight: 300,
|
||||
color: d3roPalette.accent.amber,
|
||||
mt: 3,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
D3RO-VOICE
|
||||
</Typography>
|
||||
<Typography sx={{ color: d3roPalette.text.secondary, mb: 4 }}>
|
||||
타이핑 없이, 음성으로. 로컬 AI 음성 어시스턴트입니다.
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={() => setStep(1)} fullWidth>
|
||||
시작하기
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Step 1: 마이크 */}
|
||||
{step === 1 && (
|
||||
<Box>
|
||||
<Stack direction="row" alignItems="center" gap={1} mb={3}>
|
||||
<MicIcon sx={{ color: d3roPalette.accent.amber }} />
|
||||
<Typography sx={{ fontFamily: d3roFontMono, fontWeight: 700, fontSize: '14px' }}>
|
||||
마이크 설정
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 2 }}>
|
||||
사용할 마이크를 선택하세요. 나중에 설정에서 변경할 수 있습니다.
|
||||
</Typography>
|
||||
<Stack spacing={1} mb={3}>
|
||||
{devices.map((d) => (
|
||||
<Box
|
||||
key={d.deviceId}
|
||||
onClick={() => {
|
||||
setSelectedDevice(d.deviceId)
|
||||
window.electronAPI.audio.setSelectedDevice({ deviceId: d.deviceId })
|
||||
}}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
bgcolor: selectedDevice === d.deviceId ? d3roPalette.accent.amberDim : d3roPalette.bg.inset,
|
||||
border: selectedDevice === d.deviceId
|
||||
? `1px solid ${d3roPalette.accent.amber}`
|
||||
: `1px solid ${d3roPalette.border.subtle}`,
|
||||
'&:hover': { bgcolor: d3roPalette.bg.cardHover },
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontSize: '13px' }}>
|
||||
{d.label}{d.isDefault ? ' (기본)' : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button onClick={() => setStep(0)} sx={{ color: d3roPalette.text.inactive }}>뒤로</Button>
|
||||
<Button variant="contained" onClick={() => setStep(2)}>다음</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Step 2: 핫키 */}
|
||||
{step === 2 && (
|
||||
<Box>
|
||||
<Stack direction="row" alignItems="center" gap={1} mb={3}>
|
||||
<KeyboardIcon sx={{ color: d3roPalette.accent.amber }} />
|
||||
<Typography sx={{ fontFamily: d3roFontMono, fontWeight: 700, fontSize: '14px' }}>
|
||||
단축키 설정
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 2 }}>
|
||||
받아쓰기 단축키를 설정하세요. 키를 누르고 있는 동안 녹음됩니다.
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: '10px',
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
boxShadow: 'inset 0 2px 6px rgba(0,0,0,0.4)',
|
||||
textAlign: 'center',
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
{hotkeyBinding ? (
|
||||
<Stack direction="row" spacing={1} justifyContent="center" alignItems="center">
|
||||
<Led color="green" size={8} />
|
||||
{hotkeyBinding.displayLabel.split(' + ').map((key) => (
|
||||
<Chip
|
||||
key={key}
|
||||
label={key}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontWeight: 700,
|
||||
bgcolor: d3roPalette.bg.chassis,
|
||||
color: d3roPalette.text.primary,
|
||||
border: `1px solid ${d3roPalette.border.default}`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography sx={{ color: d3roPalette.text.inactive, fontSize: '13px' }}>
|
||||
단축키가 설정되지 않았습니다
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={() => setHotkeyModalOpen(true)}
|
||||
sx={{ mb: 3, fontFamily: d3roFontMono }}
|
||||
>
|
||||
{hotkeyBinding ? '단축키 변경' : '단축키 설정'}
|
||||
</Button>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button onClick={() => setStep(1)} sx={{ color: d3roPalette.text.inactive }}>뒤로</Button>
|
||||
<Button variant="contained" onClick={() => setStep(3)}>다음</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Step 3: 완료 */}
|
||||
{step === 3 && (
|
||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||
<CheckCircleIcon sx={{ fontSize: 48, color: d3roPalette.tag.green, mb: 2 }} />
|
||||
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '18px', fontWeight: 700, mb: 1 }}>
|
||||
설정 완료!
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: d3roPalette.text.secondary, mb: 4 }}>
|
||||
{hotkeyBinding
|
||||
? `${hotkeyBinding.displayLabel} 키를 누르고 말하면 음성이 텍스트로 변환됩니다.`
|
||||
: '설정에서 단축키를 지정하면 음성 받아쓰기를 시작할 수 있습니다.'}
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={handleFinish} fullWidth>
|
||||
시작하기
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<HotkeyRecordModal
|
||||
open={hotkeyModalOpen}
|
||||
onClose={() => setHotkeyModalOpen(false)}
|
||||
onSave={handleHotkeySave}
|
||||
currentBinding={hotkeyBinding}
|
||||
title="받아쓰기 단축키 설정"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue