diff --git a/src/renderer/components/AppLayout.tsx b/src/renderer/components/AppLayout.tsx
index 0a2a95c..0c39c7d 100644
--- a/src/renderer/components/AppLayout.tsx
+++ b/src/renderer/components/AppLayout.tsx
@@ -1,23 +1,14 @@
// src/renderer/components/AppLayout.tsx
-// 08-design-system.md SSOT 적용. 앰버 악센트, LED, 다크 카드.
+// 시안 A+B 융합: 인스트루먼트 섀시 사이드바 + 콘텐츠 영역
import { useState } from 'react'
-import {
- Box,
- Drawer,
- List,
- ListItemButton,
- ListItemIcon,
- ListItemText,
- Divider,
- Typography
-} from '@mui/material'
+import { Box, Typography, Tooltip } from '@mui/material'
import DashboardIcon from '@mui/icons-material/Dashboard'
import HistoryIcon from '@mui/icons-material/History'
import MenuBookIcon from '@mui/icons-material/MenuBook'
import ExtensionIcon from '@mui/icons-material/Extension'
import SettingsIcon from '@mui/icons-material/Settings'
-import { d3roPalette, d3roFontMono } from '../theme'
+import { Led } from './ds'
import { DashboardPage } from '../pages/DashboardPage'
import { HistoryPage } from '../pages/HistoryPage'
import { DictionaryPage } from '../pages/DictionaryPage'
@@ -27,13 +18,11 @@ import { StatusBar } from './StatusBar'
type Route = 'dashboard' | 'history' | 'dictionary' | 'commands'
-const DRAWER_WIDTH = 240
-
const NAV_ITEMS: Array<{ route: Route; label: string; icon: React.ReactElement }> = [
- { route: 'dashboard', label: 'Dashboard', icon: },
- { route: 'history', label: 'History', icon: },
- { route: 'dictionary', label: 'Dictionary', icon: },
- { route: 'commands', label: 'Commands', icon: }
+ { route: 'dashboard', label: 'DASH', icon: },
+ { route: 'history', label: 'HIST', icon: },
+ { route: 'dictionary', label: 'DICT', icon: },
+ { route: 'commands', label: 'CMD', icon: },
]
export function AppLayout(): React.ReactElement {
@@ -41,132 +30,126 @@ export function AppLayout(): React.ReactElement {
const [settingsOpen, setSettingsOpen] = useState(false)
return (
-
+
- {/* Sidebar Drawer */}
-
- {/* Header — 앰버 악센트 로고 */}
-
- {/* LED indicator */}
-
+ {/* 로고 LED */}
+
+
- D3RO VOICE
-
-
- v1.0
+ D3RO
-
-
- {/* Navigation label */}
-
- Navigation
-
-
-
- {NAV_ITEMS.map((item) => (
- setCurrentRoute(item.route)}
- sx={{ my: 0.5 }}
- >
- {
+ const isActive = currentRoute === item.route
+ return (
+
+ setCurrentRoute(item.route)}
sx={{
- minWidth: 36,
- color: currentRoute === item.route
- ? d3roPalette.accent.amber
- : d3roPalette.text.label,
+ width: 48,
+ height: 48,
+ borderRadius: '8px',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 0.5,
+ cursor: 'pointer',
+ bgcolor: isActive ? '#242528' : 'transparent',
+ boxShadow: isActive
+ ? 'inset 0 2px 6px rgba(0,0,0,0.8), inset 0 0 0 1px #000'
+ : '0 2px 4px rgba(0,0,0,0.3), inset 0 1px 1px rgba(255,255,255,0.06)',
+ color: isActive ? '#f25b29' : '#77797c',
+ transition: 'all 0.05s linear',
+ transform: isActive ? 'translateY(1px)' : 'none',
+ '&:active': {
+ transform: 'translateY(2px)',
+ boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.6)',
+ },
+ '&:hover': {
+ color: isActive ? '#f25b29' : '#aaa',
+ },
}}
>
{item.icon}
-
-
-
- ))}
-
+
+ {item.label}
+
+
+
+ )
+ })}
-
+ {/* 스페이서 */}
+
- {/* Bottom settings */}
-
- setSettingsOpen(true)}>
-
-
-
-
-
-
-
+ {/* Settings */}
+
+ setSettingsOpen(true)}
+ sx={{
+ width: 48,
+ height: 48,
+ borderRadius: '8px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ cursor: 'pointer',
+ color: '#77797c',
+ boxShadow: '0 2px 4px rgba(0,0,0,0.3), inset 0 1px 1px rgba(255,255,255,0.06)',
+ transition: 'all 0.05s linear',
+ '&:active': {
+ transform: 'translateY(2px)',
+ boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.6)',
+ },
+ '&:hover': { color: '#aaa' },
+ }}
+ >
+
+
+
+
- {/* Content Area */}
+ {/* ── 콘텐츠 영역 ────────────────────────── */}
{currentRoute === 'dashboard' && }
diff --git a/src/renderer/components/StatusBar.tsx b/src/renderer/components/StatusBar.tsx
index 72b8b77..e8b4dee 100644
--- a/src/renderer/components/StatusBar.tsx
+++ b/src/renderer/components/StatusBar.tsx
@@ -1,50 +1,29 @@
// src/renderer/components/StatusBar.tsx
-// 하단 상태 표시: LED 인디케이터 + 태그 시스템. 08-design-system.md SSOT.
+// 인스트루먼트 섀시 하단 — 각인 스타일 상태 표시
import { useState, useEffect } from 'react'
-import { Box, Typography, Chip } from '@mui/material'
-import { d3roPalette, d3roFontMono } from '../theme'
+import { Box, Typography } from '@mui/material'
+import { Led } from './ds'
import type { LLMStatus } from '@shared/types'
-function Led({ active, color }: { active: boolean; color?: string }): React.ReactElement {
- const c = color ?? d3roPalette.tag.green
- return (
-
- )
-}
+const MONO = 'MONO_PLACEHOLDER'
export function StatusBar(): React.ReactElement {
const [llmStatus, setLlmStatus] = useState(null)
useEffect(() => {
- window.electronAPI.llm.getStatus().then((result) => {
- if (result.success) setLlmStatus(result.data)
- })
-
- const unsub = window.electronAPI.llm.onStatusChanged((event) => {
- setLlmStatus(event.status)
+ window.electronAPI.llm.getStatus().then((r) => {
+ if (r.success) setLlmStatus(r.data)
})
+ const unsub = window.electronAPI.llm.onStatusChanged((e) => setLlmStatus(e.status))
const interval = setInterval(() => {
- window.electronAPI.llm.getStatus().then((result) => {
- if (result.success) setLlmStatus(result.data)
+ window.electronAPI.llm.getStatus().then((r) => {
+ if (r.success) setLlmStatus(r.data)
})
}, 5000)
- return () => {
- unsub()
- clearInterval(interval)
- }
+ return () => { unsub(); clearInterval(interval) }
}, [])
const connected = llmStatus?.connectionState === 'connected'
@@ -56,50 +35,29 @@ export function StatusBar(): React.ReactElement {
alignItems: 'center',
gap: 2,
px: 2,
- py: 0.75,
- borderTop: `1px solid ${d3roPalette.border.subtle}`,
- bgcolor: 'background.paper',
- minHeight: 32,
+ py: 0.5,
+ borderTop: '1px solid rgba(255,255,255,0.04)',
+ bgcolor: '#1e1f21',
+ minHeight: 28,
}}
>
- {/* Ollama 상태 */}
-
-
+
+
{connected ? 'OLLAMA' : 'OFFLINE'}
- {/* 활성 모델 태그 */}
{llmStatus?.activeModel && (
-
+
+ {llmStatus.activeModel.toUpperCase()}
+
)}
- {/* 스페이서 */}
- {/* 핫키 힌트 */}
-
- RIGHT ALT — DICTATE
+
+ PRECISION DATA LINK
)
diff --git a/src/renderer/components/ds/CrtDisplay.tsx b/src/renderer/components/ds/CrtDisplay.tsx
new file mode 100644
index 0000000..8b2a6b2
--- /dev/null
+++ b/src/renderer/components/ds/CrtDisplay.tsx
@@ -0,0 +1,274 @@
+// src/renderer/components/ds/CrtDisplay.tsx
+// 시안 A: CRT 디스플레이 — WebGL 셰이더 (스캔라인, 비네팅, 노이즈, 글리치, 파형)
+
+import { useRef, useEffect, useCallback } from 'react'
+import { Box } from '@mui/material'
+
+// ── WebGL 유틸 ─────────────────────────────────────────
+
+function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {
+ const shader = gl.createShader(type)
+ if (!shader) return null
+ gl.shaderSource(shader, source)
+ gl.compileShader(shader)
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ gl.deleteShader(shader)
+ return null
+ }
+ return shader
+}
+
+function createProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string): WebGLProgram | null {
+ const vs = createShader(gl, gl.VERTEX_SHADER, vsSource)
+ const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource)
+ if (!vs || !fs) return null
+ const prog = gl.createProgram()
+ if (!prog) return null
+ gl.attachShader(prog, vs)
+ gl.attachShader(prog, fs)
+ gl.linkProgram(prog)
+ return prog
+}
+
+const VERTEX_SHADER = `
+ attribute vec2 position;
+ varying vec2 vUv;
+ void main() {
+ gl_Position = vec4(position, 0.0, 1.0);
+ vUv = position * 0.5 + 0.5;
+ }
+`
+
+const FRAGMENT_SHADER = `
+ precision highp float;
+ varying vec2 vUv;
+ uniform float u_time;
+ uniform float u_glitch;
+ uniform float u_amp;
+ uniform float u_freq;
+
+ float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); }
+
+ void main() {
+ vec2 uv = vUv;
+
+ // Glitch displacement
+ if (u_glitch > 0.0) {
+ float gOffset = (random(vec2(uv.y * 5.0, u_time)) - 0.5) * u_glitch * 0.1;
+ uv.x += gOffset;
+ }
+
+ // Base phosphor background
+ vec3 color = vec3(0.02, 0.015, 0.01);
+
+ // Background Grid
+ float gridX = step(0.98, fract(uv.x * 15.0));
+ float gridY = step(0.98, fract(uv.y * 10.0));
+ color += vec3(0.1, 0.04, 0.02) * max(gridX, gridY) * (1.0 - u_glitch);
+
+ // Oscilloscope Waveform
+ float t = u_time * 0.5 + u_glitch * random(uv) * 0.1;
+ float waveY = sin((uv.x * u_freq) + t) * u_amp;
+ float waveDist = abs((uv.y - 0.5) - waveY);
+ float lineThick = 0.005 + (u_glitch * 0.02);
+ float waveGlow = smoothstep(lineThick * 4.0, 0.0, waveDist);
+ float waveCore = smoothstep(lineThick, 0.0, waveDist);
+
+ // Amber wave (#f25b29)
+ vec3 waveColor = vec3(0.95, 0.35, 0.16);
+ color += waveColor * waveCore;
+ color += waveColor * 0.4 * waveGlow;
+
+ // Scanlines
+ float scanline = sin(uv.y * 800.0 - u_time * 10.0) * 0.04;
+ color -= scanline;
+
+ // Noise
+ float noise = random(uv + u_time) * 0.08;
+ color += noise;
+
+ // Vignette
+ float dist = distance(vUv, vec2(0.5));
+ float vig = smoothstep(0.8, 0.4, dist);
+ color *= vig;
+
+ // Edge darkening
+ color *= smoothstep(0.0, 0.02, vUv.x) * smoothstep(1.0, 0.98, vUv.x);
+ color *= smoothstep(0.0, 0.05, vUv.y) * smoothstep(1.0, 0.95, vUv.y);
+
+ gl_FragColor = vec4(color, 1.0);
+ }
+`
+
+const QUAD = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1])
+
+// ── Props ──────────────────────────────────────────────
+
+interface CrtDisplayProps {
+ /** 파형 진폭 (0.0 ~ 1.0) */
+ amplitude?: number
+ /** 파형 주파수 */
+ frequency?: number
+ /** 글리치 트리거 (변경 시 글리치 발생) */
+ glitchTrigger?: number
+ /** 오버레이 콘텐츠 (인광 텍스트 등) */
+ children?: React.ReactNode
+ /** 높이 (기본 280px) */
+ height?: number | string
+}
+
+export function CrtDisplay({
+ amplitude = 0.1,
+ frequency = 8.0,
+ glitchTrigger = 0,
+ children,
+ height = 280,
+}: CrtDisplayProps): React.ReactElement {
+ const canvasRef = useRef(null)
+ const glRef = useRef<{
+ gl: WebGLRenderingContext
+ uTime: WebGLUniformLocation | null
+ uGlitch: WebGLUniformLocation | null
+ uAmp: WebGLUniformLocation | null
+ uFreq: WebGLUniformLocation | null
+ } | null>(null)
+ const animRef = useRef(0)
+ const startTimeRef = useRef(Date.now())
+ const glitchRef = useRef(0)
+ const ampRef = useRef(amplitude)
+ const freqRef = useRef(frequency)
+ const currentAmpRef = useRef(amplitude)
+ const currentFreqRef = useRef(frequency)
+
+ // amplitude/frequency 변경 추적
+ useEffect(() => {
+ ampRef.current = amplitude
+ freqRef.current = frequency
+ }, [amplitude, frequency])
+
+ // 글리치 트리거
+ useEffect(() => {
+ if (glitchTrigger > 0) {
+ glitchRef.current = 1.0
+ }
+ }, [glitchTrigger])
+
+ const render = useCallback(() => {
+ const ctx = glRef.current
+ if (!ctx) return
+
+ const { gl, uTime, uGlitch, uAmp, uFreq } = ctx
+
+ // Smoothing
+ currentAmpRef.current += (ampRef.current - currentAmpRef.current) * 0.1
+ currentFreqRef.current += (freqRef.current - currentFreqRef.current) * 0.1
+ glitchRef.current *= 0.85
+
+ gl.uniform1f(uTime, (Date.now() - startTimeRef.current) / 1000)
+ gl.uniform1f(uGlitch, glitchRef.current)
+ gl.uniform1f(uAmp, currentAmpRef.current)
+ gl.uniform1f(uFreq, currentFreqRef.current)
+
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
+ animRef.current = requestAnimationFrame(render)
+ }, [])
+
+ useEffect(() => {
+ const canvas = canvasRef.current
+ if (!canvas) return
+
+ const gl = canvas.getContext('webgl', { alpha: true })
+ if (!gl) return
+
+ canvas.width = canvas.clientWidth * 2
+ canvas.height = canvas.clientHeight * 2
+ gl.viewport(0, 0, canvas.width, canvas.height)
+
+ const prog = createProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER)
+ if (!prog) return
+
+ gl.useProgram(prog)
+
+ const buffer = gl.createBuffer()
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
+ gl.bufferData(gl.ARRAY_BUFFER, QUAD, gl.STATIC_DRAW)
+
+ const posLoc = gl.getAttribLocation(prog, 'position')
+ gl.enableVertexAttribArray(posLoc)
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0)
+
+ glRef.current = {
+ gl,
+ uTime: gl.getUniformLocation(prog, 'u_time'),
+ uGlitch: gl.getUniformLocation(prog, 'u_glitch'),
+ uAmp: gl.getUniformLocation(prog, 'u_amp'),
+ uFreq: gl.getUniformLocation(prog, 'u_freq'),
+ }
+
+ startTimeRef.current = Date.now()
+ animRef.current = requestAnimationFrame(render)
+
+ return () => {
+ cancelAnimationFrame(animRef.current)
+ }
+ }, [render])
+
+ return (
+
+ {/* Glass surface */}
+
+
+
+ {/* Glass reflection */}
+
+
+
+ {/* Content overlay (phosphor text) */}
+
+ {children}
+
+
+ )
+}
diff --git a/src/renderer/components/ds/InstrumentPanel.tsx b/src/renderer/components/ds/InstrumentPanel.tsx
new file mode 100644
index 0000000..0b66069
--- /dev/null
+++ b/src/renderer/components/ds/InstrumentPanel.tsx
@@ -0,0 +1,77 @@
+// src/renderer/components/ds/InstrumentPanel.tsx
+// 시안 A: 메탈 섀시 컨테이너 — 노이즈 텍스처, 각인 텍스트, 물리적 존재감
+
+import { Box, Typography } from '@mui/material'
+
+interface InstrumentPanelProps {
+ children: React.ReactNode
+ /** 상단 좌측 각인 */
+ engravingLeft?: string
+ /** 상단 우측 각인 */
+ engravingRight?: string
+ /** 하단 중앙 각인 */
+ engravingBottom?: string
+}
+
+export function InstrumentPanel({
+ children,
+ engravingLeft = 'D3RO-VOICE SYS.',
+ engravingRight = 'MOD-01 / TERMINAL',
+ engravingBottom = 'LOCAL AI VOICE ASSISTANT',
+}: InstrumentPanelProps): React.ReactElement {
+ return (
+
+ {/* 각인 텍스트 */}
+ {engravingLeft}
+ {engravingRight}
+ {engravingBottom}
+
+ {/* 콘텐츠 (z-index 5로 노이즈 위) */}
+
+ {children}
+
+
+ )
+}
+
+function Engraving({ children, sx }: { children: string; sx: Record }): React.ReactElement {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/renderer/components/ds/Led.tsx b/src/renderer/components/ds/Led.tsx
new file mode 100644
index 0000000..fb8fe80
--- /dev/null
+++ b/src/renderer/components/ds/Led.tsx
@@ -0,0 +1,50 @@
+// src/renderer/components/ds/Led.tsx
+// 시안 A: LED 인디케이터 — 물리적 LED, 활성 시 glow + pulse
+
+import { Box } from '@mui/material'
+
+type LedColor = 'amber' | 'green' | 'red' | 'orange' | 'off'
+
+const LED_COLORS: Record = {
+ amber: { bg: '#f25b29', glow: 'rgba(242, 91, 41, 0.6)' },
+ green: { bg: '#22c55e', glow: 'rgba(34, 197, 94, 0.6)' },
+ red: { bg: '#ef4444', glow: 'rgba(239, 68, 68, 0.6)' },
+ orange: { bg: '#f59e0b', glow: 'rgba(245, 158, 11, 0.6)' },
+ off: { bg: '#111', glow: 'transparent' },
+}
+
+interface LedProps {
+ color?: LedColor
+ pulse?: boolean
+ size?: number
+}
+
+export function Led({ color = 'off', pulse = false, size = 8 }: LedProps): React.ReactElement {
+ const c = LED_COLORS[color]
+ const isActive = color !== 'off'
+
+ return (
+
+ )
+}
diff --git a/src/renderer/components/ds/MetalCard.tsx b/src/renderer/components/ds/MetalCard.tsx
new file mode 100644
index 0000000..c72bf79
--- /dev/null
+++ b/src/renderer/components/ds/MetalCard.tsx
@@ -0,0 +1,29 @@
+// src/renderer/components/ds/MetalCard.tsx
+// 시안 A+B 융합: 메탈 카드 컨테이너 — 섀시 느낌의 인셋 패널
+
+import { Box } from '@mui/material'
+
+interface MetalCardProps {
+ children: React.ReactNode
+ inset?: boolean
+}
+
+export function MetalCard({ children, inset = false }: MetalCardProps): React.ReactElement {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/renderer/components/ds/PhosphorText.tsx b/src/renderer/components/ds/PhosphorText.tsx
new file mode 100644
index 0000000..c65f276
--- /dev/null
+++ b/src/renderer/components/ds/PhosphorText.tsx
@@ -0,0 +1,39 @@
+// src/renderer/components/ds/PhosphorText.tsx
+// 시안 A: 인광 텍스트 — 앰버 glow, 모노 폰트, CRT 느낌
+
+import { Typography, type TypographyProps } from '@mui/material'
+
+type PhosphorVariant = 'hero' | 'value' | 'label' | 'dim'
+
+const VARIANTS: Record = {
+ hero: { fontSize: '42px', color: '#f25b29', glow: 'rgba(242, 91, 41, 0.4)', fontWeight: 300 },
+ value: { fontSize: '20px', color: '#f25b29', glow: 'rgba(242, 91, 41, 0.3)', fontWeight: 400 },
+ label: { fontSize: '10px', color: '#5c2615', glow: 'none', fontWeight: 700 },
+ dim: { fontSize: '10px', color: '#77797c', glow: 'none', fontWeight: 400 },
+}
+
+interface PhosphorTextProps extends Omit {
+ variant?: PhosphorVariant
+}
+
+export function PhosphorText({ variant = 'value', sx, ...props }: PhosphorTextProps): React.ReactElement {
+ const v = VARIANTS[variant]
+
+ return (
+
+ )
+}
diff --git a/src/renderer/components/ds/PhysicalButton.tsx b/src/renderer/components/ds/PhysicalButton.tsx
new file mode 100644
index 0000000..c4257db
--- /dev/null
+++ b/src/renderer/components/ds/PhysicalButton.tsx
@@ -0,0 +1,43 @@
+// src/renderer/components/ds/PhysicalButton.tsx
+// 시안 A: 물리 버튼 — 돌출 그림자, 눌림 피드백, 선택 상태
+
+import { Button, type ButtonProps } from '@mui/material'
+
+interface PhysicalButtonProps extends Omit {
+ selected?: boolean
+}
+
+export function PhysicalButton({ selected = false, sx, ...props }: PhysicalButtonProps): React.ReactElement {
+ return (
+
+ )
+}
diff --git a/src/renderer/components/ds/index.ts b/src/renderer/components/ds/index.ts
new file mode 100644
index 0000000..e851354
--- /dev/null
+++ b/src/renderer/components/ds/index.ts
@@ -0,0 +1,9 @@
+// src/renderer/components/ds/index.ts
+// 디자인 시스템 컴포넌트 SSOT barrel export
+
+export { CrtDisplay } from './CrtDisplay'
+export { InstrumentPanel } from './InstrumentPanel'
+export { Led } from './Led'
+export { PhysicalButton } from './PhysicalButton'
+export { MetalCard } from './MetalCard'
+export { PhosphorText } from './PhosphorText'
diff --git a/src/renderer/pages/CommandsPage.tsx b/src/renderer/pages/CommandsPage.tsx
index c9d2e94..c5c7aa2 100644
--- a/src/renderer/pages/CommandsPage.tsx
+++ b/src/renderer/pages/CommandsPage.tsx
@@ -1,26 +1,16 @@
// src/renderer/pages/CommandsPage.tsx
-// 08-design-system.md SSOT: 다크 카드, 앰버 악센트, 태그 시스템
+// 인스트루먼트 미학: MetalCard + PhosphorText + Led
import { useState, useEffect, useCallback } from 'react'
-import {
- Box, Typography, Button, IconButton, Chip,
- Dialog, DialogTitle, DialogContent, DialogActions,
- TextField, Card, CardContent
-} from '@mui/material'
+import { Box, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField } 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 { d3roPalette } from '../theme'
+import { MetalCard, PhosphorText, Led } from '../components/ds'
import type { IPCResult } from '@shared/errors'
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 {
@@ -38,108 +28,62 @@ export function CommandsPage(): React.ReactElement {
const ipcResult = await (window.electronAPI as Record & {
invoke: (channel: string, ...args: unknown[]) => Promise>
}).invoke?.('instruction:getAll') as unknown as IPCResult | undefined
-
- if (ipcResult && ipcResult.success) {
- setInstructions(ipcResult.data)
- }
- } catch {
- // preload에 instruction API가 없을 수 있음
- }
+ if (ipcResult && ipcResult.success) setInstructions(ipcResult.data)
+ } catch { /* noop */ }
setLoading(false)
}, [])
useEffect(() => { loadData() }, [loadData])
- const openAdd = () => {
- setEditId(null)
- setFormName('')
- setFormDesc('')
- setFormPrompt('')
- setDialogOpen(true)
- }
-
- const openEdit = (inst: CustomInstruction) => {
- setEditId(inst.id)
- setFormName(inst.name)
- setFormDesc(inst.description)
- setFormPrompt(inst.prompt)
- setDialogOpen(true)
- }
-
- const handleSave = async () => {
- setDialogOpen(false)
- loadData()
- }
+ const openAdd = () => { setEditId(null); setFormName(''); setFormDesc(''); setFormPrompt(''); setDialogOpen(true) }
+ const openEdit = (inst: CustomInstruction) => { setEditId(inst.id); setFormName(inst.name); setFormDesc(inst.description); setFormPrompt(inst.prompt); setDialogOpen(true) }
+ const handleSave = async () => { setDialogOpen(false); loadData() }
return (
-
- {/* Header */}
-
-
- Commands
-
- Custom LLM instructions
-
-
- } onClick={openAdd}>
- Add Command
-
+
+
+
+ LLM INSTRUCTIONS — {instructions.length} COMMANDS
+
+ } onClick={openAdd} size="small">ADD
{loading ? (
- Loading...
+ LOADING...
) : instructions.length === 0 ? (
-
-
-
- Built-in commands: Translate, Summarize, Formal Rewrite, Code Explain, Free Prompt.
-
-
-
+
+
+ BUILT-IN: TRANSLATE, SUMMARIZE, FORMAL, CODE, FREE
+
+
) : (
-
+
{instructions.map((inst) => (
-
-
-
-
-
- {inst.name}
-
-
+
+
+
+
+
+ {inst.name}
+ {inst.description}
-
- {inst.description}
-
-
- openEdit(inst)}
- sx={{ color: d3roPalette.text.label, '&:hover': { color: d3roPalette.accent.amber } }}
- >
-
+
+ openEdit(inst)} sx={{ color: '#77797c', '&:hover': { color: '#f25b29' } }}>
+
{!inst.isBuiltin && (
-
-
+
+
)}
-
-
+
+
))}
)}
- {/* Dialog */}