// src/renderer/pages/CommandsPage.tsx // 커스텀 LLM 명령어 관리: 클릭하면 활성 명령어로 설정 → 다음 녹음 시 적용 import { useState, useEffect, useCallback } from 'react' 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' interface CustomInstruction { id: string name: string description: string prompt: string icon: string isBuiltin: boolean order: number } export function CommandsPage(): React.ReactElement { const [instructions, setInstructions] = useState([]) const [loading, setLoading] = useState(true) const [dialogOpen, setDialogOpen] = useState(false) const [editId, setEditId] = useState(null) const [formName, setFormName] = useState('') const [formDesc, setFormDesc] = useState('') const [formPrompt, setFormPrompt] = useState('') const [activeId, setActiveId] = useState(null) const loadData = useCallback(async () => { setLoading(true) 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() const unsub = window.electronAPI.app.onDataChanged((data) => { if (data.type === 'command-changed' && data.activeId) { setActiveId(data.activeId) } loadData() }) return unsub }, [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('') 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 }) 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 {/* 활성 명령어 표시 */} ACTIVE COMMAND {activeInstruction ? `● ${activeInstruction.name}` : '없음 — 명령어를 클릭하여 활성화'} {loading ? ( LOADING... ) : instructions.length === 0 ? ( NO COMMANDS — CLICK ADD TO CREATE ) : ( {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} 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 } }} > )} ) })} )} setDialogOpen(false)} maxWidth="sm" fullWidth> {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}}는 전사된 텍스트로 치환됩니다" /> ) }