Commands/Dictionary 미연결 버그 4건 수정
1. CommandsPage: handleSave/handleDelete에 실제 IPC 호출 연결 - instruction:create/update/delete 호출 추가 - 프리셋은 삭제 불가 (isBuiltin 체크) 2. Preload: instruction API 추가 (getAll/create/update/delete/reorder) - 기존에 preload 브릿지가 없어서 렌더러에서 호출 불가했음 3. Dictionary → STT initialPrompt 주입 - VoiceModeService._transcribe에서 DictionaryService.getPromptHints() 호출 - 사용자 사전 단어를 Whisper initialPrompt로 전달 4. DictionaryPage: 편집 기능 추가 - 각 항목에 Edit 버튼 추가 - Add/Edit 공용 다이얼로그 (editId로 분기)
This commit is contained in:
parent
6e9e8b1f29
commit
5596a52dcb
4 changed files with 185 additions and 44 deletions
|
|
@ -418,8 +418,18 @@ class VoiceModeService extends EventEmitter {
|
||||||
const stt = getLocalSTTService()
|
const stt = getLocalSTTService()
|
||||||
const language = configGet('sttLanguage')
|
const language = configGet('sttLanguage')
|
||||||
|
|
||||||
|
// Dictionary → STT initialPrompt 주입 (Speakly 패턴)
|
||||||
|
let initialPrompt: string | undefined
|
||||||
|
try {
|
||||||
|
const { getDictionaryService } = await import('./DictionaryService')
|
||||||
|
initialPrompt = getDictionaryService().getPromptHints() || undefined
|
||||||
|
} catch {
|
||||||
|
// DictionaryService 미초기화 시 무시
|
||||||
|
}
|
||||||
|
|
||||||
const result: TranscriptionResult = await stt.transcribe(merged, {
|
const result: TranscriptionResult = await stt.transcribe(merged, {
|
||||||
language: language === 'auto' ? undefined : language
|
language: language === 'auto' ? undefined : language,
|
||||||
|
initialPrompt,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this._isInTerminalState()) return
|
if (this._isInTerminalState()) return
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,18 @@ const electronAPI = {
|
||||||
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)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Instruction (커스텀 명령어) ────────────────────────
|
||||||
|
instruction: {
|
||||||
|
getAll: () => invoke<unknown[]>('instruction:getAll'),
|
||||||
|
getById: (params: { id: string }) => invoke<unknown>('instruction:getById', params),
|
||||||
|
create: (params: { name: string; description: string; prompt: string }) =>
|
||||||
|
invoke<unknown>('instruction:create', params),
|
||||||
|
update: (params: { id: string; name?: string; description?: string; prompt?: string }) =>
|
||||||
|
invoke<unknown>('instruction:update', params),
|
||||||
|
delete: (params: { id: string }) => invoke<void>('instruction:delete', params),
|
||||||
|
reorder: (params: { ids: string[] }) => invoke<void>('instruction:reorder', params),
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// src/renderer/pages/CommandsPage.tsx
|
// src/renderer/pages/CommandsPage.tsx
|
||||||
// 인스트루먼트 미학: MetalCard + PhosphorText + Led
|
// 커스텀 LLM 명령어 관리: CRUD + 프리셋 5개
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Box, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField } from '@mui/material'
|
import { Box, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField } from '@mui/material'
|
||||||
|
|
@ -7,11 +7,16 @@ import AddIcon from '@mui/icons-material/Add'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
import { MetalCard, PhosphorText, Led } from '../components/ds'
|
import { MetalCard, PhosphorText, Led } from '../components/ds'
|
||||||
import { d3roPalette } from '../theme'
|
import { d3roPalette, d3roFontMono } from '../theme'
|
||||||
import type { IPCResult } from '@shared/errors'
|
|
||||||
|
|
||||||
interface CustomInstruction {
|
interface CustomInstruction {
|
||||||
id: string; name: string; description: string; prompt: string; icon: string; isBuiltin: boolean; order: number
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
prompt: string
|
||||||
|
icon: string
|
||||||
|
isBuiltin: boolean
|
||||||
|
order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommandsPage(): React.ReactElement {
|
export function CommandsPage(): React.ReactElement {
|
||||||
|
|
@ -25,20 +30,58 @@ export function CommandsPage(): React.ReactElement {
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
const result = await window.electronAPI.instruction.getAll()
|
||||||
const ipcResult = await (window.electronAPI as Record<string, unknown> & {
|
if (result.success) {
|
||||||
invoke: (channel: string, ...args: unknown[]) => Promise<IPCResult<CustomInstruction[]>>
|
setInstructions(result.data as CustomInstruction[])
|
||||||
}).invoke?.('instruction:getAll') as unknown as IPCResult<CustomInstruction[]> | undefined
|
}
|
||||||
if (ipcResult && ipcResult.success) setInstructions(ipcResult.data)
|
|
||||||
} catch { /* noop */ }
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => { loadData() }, [loadData])
|
useEffect(() => { loadData() }, [loadData])
|
||||||
|
|
||||||
const openAdd = () => { setEditId(null); setFormName(''); setFormDesc(''); setFormPrompt(''); setDialogOpen(true) }
|
const openAdd = () => {
|
||||||
const openEdit = (inst: CustomInstruction) => { setEditId(inst.id); setFormName(inst.name); setFormDesc(inst.description); setFormPrompt(inst.prompt); setDialogOpen(true) }
|
setEditId(null)
|
||||||
const handleSave = async () => { setDialogOpen(false); loadData() }
|
setFormName('')
|
||||||
|
setFormDesc('')
|
||||||
|
setFormPrompt('{{text}}를 다듬어주세요.')
|
||||||
|
setDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEdit = (inst: CustomInstruction) => {
|
||||||
|
setEditId(inst.id)
|
||||||
|
setFormName(inst.name)
|
||||||
|
setFormDesc(inst.description)
|
||||||
|
setFormPrompt(inst.prompt)
|
||||||
|
setDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!formName.trim()) return
|
||||||
|
|
||||||
|
if (editId) {
|
||||||
|
// 편집
|
||||||
|
await window.electronAPI.instruction.update({
|
||||||
|
id: editId,
|
||||||
|
name: formName.trim(),
|
||||||
|
description: formDesc.trim(),
|
||||||
|
prompt: formPrompt.trim(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 추가
|
||||||
|
await window.electronAPI.instruction.create({
|
||||||
|
name: formName.trim(),
|
||||||
|
description: formDesc.trim(),
|
||||||
|
prompt: formPrompt.trim(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setDialogOpen(false)
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
await window.electronAPI.instruction.delete({ id })
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ maxWidth: 900, mx: 'auto', p: 4 }}>
|
<Box sx={{ maxWidth: 900, mx: 'auto', p: 4 }}>
|
||||||
|
|
@ -46,7 +89,9 @@ export function CommandsPage(): React.ReactElement {
|
||||||
<PhosphorText variant="label" sx={{ color: d3roPalette.text.inactive }}>
|
<PhosphorText variant="label" sx={{ color: d3roPalette.text.inactive }}>
|
||||||
LLM INSTRUCTIONS — {instructions.length} COMMANDS
|
LLM INSTRUCTIONS — {instructions.length} COMMANDS
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
<Button variant="contained" startIcon={<AddIcon />} onClick={openAdd} size="small">ADD</Button>
|
<Button variant="contained" startIcon={<AddIcon />} onClick={openAdd} size="small">
|
||||||
|
ADD
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
@ -54,7 +99,7 @@ export function CommandsPage(): React.ReactElement {
|
||||||
) : instructions.length === 0 ? (
|
) : instructions.length === 0 ? (
|
||||||
<MetalCard>
|
<MetalCard>
|
||||||
<Box sx={{ py: 6, textAlign: 'center' }}>
|
<Box sx={{ py: 6, textAlign: 'center' }}>
|
||||||
<PhosphorText variant="dim">BUILT-IN: TRANSLATE, SUMMARIZE, FORMAL, CODE, FREE</PhosphorText>
|
<PhosphorText variant="dim">NO COMMANDS — CLICK ADD TO CREATE</PhosphorText>
|
||||||
</Box>
|
</Box>
|
||||||
</MetalCard>
|
</MetalCard>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -65,16 +110,28 @@ export function CommandsPage(): React.ReactElement {
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1 }}>
|
||||||
<Led color={inst.isBuiltin ? 'amber' : 'green'} size={6} />
|
<Led color={inst.isBuiltin ? 'amber' : 'green'} size={6} />
|
||||||
<Box>
|
<Box>
|
||||||
<Box sx={{ fontSize: '14px', fontWeight: 600, color: d3roPalette.text.primary }}>{inst.name}</Box>
|
<Box sx={{ fontSize: '14px', fontWeight: 600, color: d3roPalette.text.primary }}>
|
||||||
<Box sx={{ fontSize: '11px', color: d3roPalette.text.inactive, mt: 0.25 }}>{inst.description}</Box>
|
{inst.name}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ fontSize: '11px', color: d3roPalette.text.inactive, mt: 0.25 }}>
|
||||||
|
{inst.description}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||||
<IconButton size="small" onClick={() => openEdit(inst)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openEdit(inst)}
|
||||||
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
|
>
|
||||||
<EditIcon sx={{ fontSize: 16 }} />
|
<EditIcon sx={{ fontSize: 16 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{!inst.isBuiltin && (
|
{!inst.isBuiltin && (
|
||||||
<IconButton size="small" sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleDelete(inst.id)}
|
||||||
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}
|
||||||
|
>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<DeleteIcon sx={{ fontSize: 16 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|
@ -86,15 +143,43 @@ export function CommandsPage(): React.ReactElement {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
|
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||||
<DialogTitle sx={{ fontWeight: 700 }}>{editId ? 'Edit Command' : 'Add Command'}</DialogTitle>
|
<DialogTitle sx={{ fontWeight: 700 }}>
|
||||||
|
{editId ? '명령어 편집' : '명령어 추가'}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField label="Name" value={formName} onChange={(e) => setFormName(e.target.value)} fullWidth autoFocus sx={{ mt: 1 }} />
|
<TextField
|
||||||
<TextField label="Description" value={formDesc} onChange={(e) => setFormDesc(e.target.value)} fullWidth sx={{ mt: 2 }} />
|
label="이름"
|
||||||
<TextField label="Prompt Template" value={formPrompt} onChange={(e) => setFormPrompt(e.target.value)} fullWidth multiline rows={4} sx={{ mt: 2 }} helperText="Use {{text}} for transcribed text" />
|
value={formName}
|
||||||
|
onChange={(e) => setFormName(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
autoFocus
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="설명"
|
||||||
|
value={formDesc}
|
||||||
|
onChange={(e) => setFormDesc(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="프롬프트 템플릿"
|
||||||
|
value={formPrompt}
|
||||||
|
onChange={(e) => setFormPrompt(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
helperText="{{text}}는 전사된 텍스트로 치환됩니다"
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
<Button onClick={() => setDialogOpen(false)} color="secondary" variant="contained">Cancel</Button>
|
<Button onClick={() => setDialogOpen(false)} sx={{ color: d3roPalette.text.inactive }}>
|
||||||
<Button onClick={handleSave} variant="contained" disabled={!formName.trim()}>Save</Button>
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} variant="contained" disabled={!formName.trim()}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Box, TextField, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, InputAdornment } from '@mui/material'
|
import { Box, TextField, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, InputAdornment } from '@mui/material'
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
import AddIcon from '@mui/icons-material/Add'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
import SearchIcon from '@mui/icons-material/Search'
|
import SearchIcon from '@mui/icons-material/Search'
|
||||||
import { MetalCard, PhosphorText, PhysicalButton } from '../components/ds'
|
import { MetalCard, PhosphorText, PhysicalButton } from '../components/ds'
|
||||||
import { d3roPalette, d3roFontMono } from '../theme'
|
import { d3roPalette, d3roFontMono } from '../theme'
|
||||||
|
|
@ -16,9 +17,10 @@ export function DictionaryPage(): React.ReactElement {
|
||||||
const [data, setData] = useState<DictPageData | null>(null)
|
const [data, setData] = useState<DictPageData | null>(null)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [addOpen, setAddOpen] = useState(false)
|
const [dialogOpen, setDialogOpen] = useState(false)
|
||||||
const [newWord, setNewWord] = useState('')
|
const [editId, setEditId] = useState<string | null>(null)
|
||||||
const [newPronunciation, setNewPronunciation] = useState('')
|
const [formWord, setFormWord] = useState('')
|
||||||
|
const [formPronunciation, setFormPronunciation] = useState('')
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
@ -31,10 +33,36 @@ export function DictionaryPage(): React.ReactElement {
|
||||||
|
|
||||||
useEffect(() => { loadData() }, [loadData])
|
useEffect(() => { loadData() }, [loadData])
|
||||||
|
|
||||||
const handleAdd = async () => {
|
const openAdd = () => {
|
||||||
if (!newWord.trim()) return
|
setEditId(null)
|
||||||
await window.electronAPI.dictionary.add({ word: newWord.trim(), pronunciation: newPronunciation.trim() || undefined })
|
setFormWord('')
|
||||||
setNewWord(''); setNewPronunciation(''); setAddOpen(false); loadData()
|
setFormPronunciation('')
|
||||||
|
setDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEdit = (entry: DictionaryEntry) => {
|
||||||
|
setEditId(entry.id)
|
||||||
|
setFormWord(entry.word)
|
||||||
|
setFormPronunciation(entry.pronunciation ?? '')
|
||||||
|
setDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!formWord.trim()) return
|
||||||
|
if (editId) {
|
||||||
|
await window.electronAPI.dictionary.update({
|
||||||
|
id: editId,
|
||||||
|
word: formWord.trim(),
|
||||||
|
pronunciation: formPronunciation.trim() || undefined,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await window.electronAPI.dictionary.add({
|
||||||
|
word: formWord.trim(),
|
||||||
|
pronunciation: formPronunciation.trim() || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setDialogOpen(false)
|
||||||
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -43,7 +71,7 @@ export function DictionaryPage(): React.ReactElement {
|
||||||
<PhosphorText variant="label" sx={{ color: d3roPalette.text.inactive }}>
|
<PhosphorText variant="label" sx={{ color: d3roPalette.text.inactive }}>
|
||||||
CUSTOM DICTIONARY — {data?.total ?? 0} WORDS
|
CUSTOM DICTIONARY — {data?.total ?? 0} WORDS
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setAddOpen(true)} size="small">
|
<Button variant="contained" startIcon={<AddIcon />} onClick={openAdd} size="small">
|
||||||
ADD
|
ADD
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -81,25 +109,31 @@ export function DictionaryPage(): React.ReactElement {
|
||||||
{entry.category.toUpperCase()} · {entry.usageCount}× USED
|
{entry.category.toUpperCase()} · {entry.usageCount}× USED
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="small" onClick={() => { window.electronAPI.dictionary.delete({ id: entry.id }); loadData() }}
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
<IconButton size="small" onClick={() => openEdit(entry)}
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
||||||
</IconButton>
|
<EditIcon sx={{ fontSize: 16 }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton size="small" onClick={() => { window.electronAPI.dictionary.delete({ id: entry.id }); loadData() }}
|
||||||
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
||||||
|
<DeleteIcon sx={{ fontSize: 16 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</MetalCard>
|
</MetalCard>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog open={addOpen} onClose={() => setAddOpen(false)} maxWidth="xs" fullWidth>
|
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="xs" fullWidth>
|
||||||
<DialogTitle sx={{ fontWeight: 700 }}>Add Word</DialogTitle>
|
<DialogTitle sx={{ fontWeight: 700 }}>{editId ? '단어 편집' : '단어 추가'}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField label="Word" value={newWord} onChange={(e) => setNewWord(e.target.value)} fullWidth autoFocus sx={{ mt: 1 }} />
|
<TextField label="단어" value={formWord} onChange={(e) => setFormWord(e.target.value)} fullWidth autoFocus sx={{ mt: 1 }} />
|
||||||
<TextField label="Pronunciation (optional)" value={newPronunciation} onChange={(e) => setNewPronunciation(e.target.value)} fullWidth sx={{ mt: 2 }} />
|
<TextField label="발음 (선택)" value={formPronunciation} onChange={(e) => setFormPronunciation(e.target.value)} fullWidth sx={{ mt: 2 }} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
<Button onClick={() => setAddOpen(false)} color="secondary" variant="contained">Cancel</Button>
|
<Button onClick={() => setDialogOpen(false)} sx={{ color: d3roPalette.text.inactive }}>취소</Button>
|
||||||
<Button onClick={handleAdd} variant="contained" disabled={!newWord.trim()}>Add</Button>
|
<Button onClick={handleSave} variant="contained" disabled={!formWord.trim()}>저장</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue