diff --git a/src/main/services/VoiceModeService.ts b/src/main/services/VoiceModeService.ts index 00f52cd..7541ed7 100644 --- a/src/main/services/VoiceModeService.ts +++ b/src/main/services/VoiceModeService.ts @@ -468,9 +468,27 @@ class VoiceModeService extends EventEmitter { const llm = getLocalLLMService() const action = configGet('defaultLLMAction') - logger.info(`Processing with LLM (action: ${action})`) + let processedText: string - const processedText = await llm.processText(transcribedText, action) + if (action === 'custom') { + // 활성 명령어의 프롬프트를 사용 + const activeId = configGet('activeInstructionId' as keyof import('@shared/types').AppConfig) as unknown as string + let customPrompt = transcribedText + + if (activeId) { + const { getCustomInstructionService } = await import('./CustomInstructionService') + const instruction = getCustomInstructionService().getById(activeId) + if (instruction) { + customPrompt = instruction.prompt.replace(/\{\{text\}\}/g, transcribedText) + logger.info(`Using custom instruction: "${instruction.name}"`) + } + } + + processedText = await llm.processText(customPrompt, 'custom') + } else { + logger.info(`Processing with LLM (action: ${action})`) + processedText = await llm.processText(transcribedText, action) + } if (this._isInTerminalState()) return diff --git a/src/renderer/pages/CommandsPage.tsx b/src/renderer/pages/CommandsPage.tsx index 327e58c..ce26b35 100644 --- a/src/renderer/pages/CommandsPage.tsx +++ b/src/renderer/pages/CommandsPage.tsx @@ -1,11 +1,12 @@ // src/renderer/pages/CommandsPage.tsx -// 커스텀 LLM 명령어 관리: CRUD + 프리셋 5개 +// 커스텀 LLM 명령어 관리: 클릭하면 활성 명령어로 설정 → 다음 녹음 시 적용 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, Typography } from '@mui/material' import AddIcon from '@mui/icons-material/Add' import DeleteIcon from '@mui/icons-material/Delete' import EditIcon from '@mui/icons-material/Edit' +import CheckCircleIcon from '@mui/icons-material/CheckCircle' import { MetalCard, PhosphorText, Led } from '../components/ds' import { d3roPalette, d3roFontMono } from '../theme' @@ -27,18 +28,39 @@ export function CommandsPage(): React.ReactElement { const [formName, setFormName] = useState('') const [formDesc, setFormDesc] = useState('') const [formPrompt, setFormPrompt] = useState('') + const [activeId, setActiveId] = useState(null) const loadData = useCallback(async () => { setLoading(true) - const result = await window.electronAPI.instruction.getAll() - if (result.success) { - setInstructions(result.data as CustomInstruction[]) + const [instrResult, configResult] = await Promise.all([ + window.electronAPI.instruction.getAll(), + window.electronAPI.config.getAll(), + ]) + if (instrResult.success) { + setInstructions(instrResult.data as CustomInstruction[]) + } + if (configResult.success) { + const cfg = configResult.data as Record + setActiveId((cfg['activeInstructionId'] as string) ?? null) } setLoading(false) }, []) useEffect(() => { loadData() }, [loadData]) + const handleActivate = (id: string) => { + const newId = activeId === id ? null : id // 토글: 같은 거 누르면 해제 + setActiveId(newId) + // ConfigService에 저장 + defaultLLMAction을 'custom'으로 변경 + if (newId) { + window.electronAPI.config.set({ key: 'activeInstructionId' as keyof import('@shared/types').AppConfig, value: newId as never }) + window.electronAPI.config.set({ key: 'defaultLLMAction', value: 'custom' }) + } else { + window.electronAPI.config.set({ key: 'activeInstructionId' as keyof import('@shared/types').AppConfig, value: '' as never }) + window.electronAPI.config.set({ key: 'defaultLLMAction', value: 'none' }) + } + } + const openAdd = () => { setEditId(null) setFormName('') @@ -57,9 +79,7 @@ export function CommandsPage(): React.ReactElement { const handleSave = async () => { if (!formName.trim()) return - if (editId) { - // 편집 await window.electronAPI.instruction.update({ id: editId, name: formName.trim(), @@ -67,7 +87,6 @@ export function CommandsPage(): React.ReactElement { prompt: formPrompt.trim(), }) } else { - // 추가 await window.electronAPI.instruction.create({ name: formName.trim(), description: formDesc.trim(), @@ -80,12 +99,18 @@ export function CommandsPage(): React.ReactElement { const handleDelete = async (id: string) => { await window.electronAPI.instruction.delete({ id }) + if (activeId === id) { + setActiveId(null) + window.electronAPI.config.set({ key: 'defaultLLMAction', value: 'none' }) + } loadData() } + const activeInstruction = instructions.find((i) => i.id === activeId) + return ( - + LLM INSTRUCTIONS — {instructions.length} COMMANDS @@ -94,6 +119,16 @@ export function CommandsPage(): React.ReactElement { + {/* 활성 명령어 표시 */} + + + ACTIVE COMMAND + + + {activeInstruction ? `● ${activeInstruction.name}` : '없음 — 명령어를 클릭하여 활성화'} + + + {loading ? ( LOADING... ) : instructions.length === 0 ? ( @@ -104,41 +139,59 @@ export function CommandsPage(): React.ReactElement { ) : ( - {instructions.map((inst) => ( - - - - - - - {inst.name} + {instructions.map((inst) => { + const isActive = inst.id === activeId + return ( + handleActivate(inst.id)} + sx={{ + cursor: 'pointer', + borderRadius: '22px', + border: isActive ? `2px solid ${d3roPalette.accent.amber}` : '2px solid transparent', + transition: 'border-color 0.15s ease', + }} + > + + + + {isActive ? ( + + ) : ( + + )} + + + {inst.name} + + + {inst.description} + + - - {inst.description} + e.stopPropagation()}> + openEdit(inst)} + sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }} + > + + + {!inst.isBuiltin && ( + handleDelete(inst.id)} + sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }} + > + + + )} - - - openEdit(inst)} - sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }} - > - - - {!inst.isBuiltin && ( - handleDelete(inst.id)} - sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }} - > - - - )} - + - - ))} + ) + })} )} @@ -147,39 +200,13 @@ export function CommandsPage(): React.ReactElement { {editId ? '명령어 편집' : '명령어 추가'} - setFormName(e.target.value)} - fullWidth - autoFocus - sx={{ mt: 1 }} - /> - setFormDesc(e.target.value)} - fullWidth - sx={{ mt: 2 }} - /> - setFormPrompt(e.target.value)} - fullWidth - multiline - rows={4} - sx={{ mt: 2 }} - helperText="{{text}}는 전사된 텍스트로 치환됩니다" - /> + setFormName(e.target.value)} fullWidth autoFocus sx={{ mt: 1 }} /> + setFormDesc(e.target.value)} fullWidth sx={{ mt: 2 }} /> + setFormPrompt(e.target.value)} fullWidth multiline rows={4} sx={{ mt: 2 }} helperText="{{text}}는 전사된 텍스트로 치환됩니다" /> - - + +