'use client'; import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import CodeEditor from '@/components/admin/CodeEditor'; import { adminFetch } from '@/lib/adminFetch'; const SISTERS = ['harang', 'narang', 'darang', 'erang']; const FILES = ['SOUL.md', 'AGENTS.md', 'TOOLS.md', 'PROTOCOL.md', 'HEARTBEAT.md']; const DISPLAY_NAMES: Record = { harang: '🦊 ν•˜λž‘μ΄', narang: '🦊 λ‚˜λž‘μ΄', darang: '🐱 λ‹€λž‘μ΄', erang: '🐺 μ΄λž‘μ΄', }; const Layout = styled.div` display: grid; grid-template-columns: 180px 1fr; gap: 20px; `; const FileTree = styled.div` background: var(--bg-surface); border: 1px solid var(--border-color); border-radius: 10px; overflow: hidden; `; const TreeSection = styled.div` border-bottom: 1px solid var(--border-color); padding: 6px; &:last-child { border-bottom: none; } `; const TreeLabel = styled.div` font-size: 11px; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.06em; padding: 6px 8px 4px; `; const TreeItem = styled.button<{ $active: boolean }>` width: 100%; text-align: left; padding: 6px 10px; font-size: 12px; border: none; border-radius: 5px; cursor: pointer; background: ${({ $active }) => $active ? 'rgba(88,166,255,0.12)' : 'transparent'}; color: ${({ $active }) => $active ? '#58A6FF' : 'var(--text-secondary)'}; transition: background 0.15s; &:hover { background: rgba(240,246,252,0.06); color: var(--text-primary); } `; const EditorPane = styled.div` display: flex; flex-direction: column; gap: 12px; `; const Toolbar = styled.div` display: flex; align-items: center; gap: 10px; `; const PathLabel = styled.span` font-size: 13px; color: var(--text-secondary); font-family: monospace; flex: 1; `; const Btn = styled.button<{ $primary?: boolean; $danger?: boolean }>` padding: 7px 16px; border-radius: 7px; font-size: 13px; font-weight: 600; cursor: pointer; border: 1px solid; transition: all 0.15s; disabled: ${({ disabled }) => disabled ? 'not-allowed' : 'pointer'}; ${({ $primary }) => $primary && `background: rgba(88,166,255,0.12); border-color: rgba(88,166,255,0.4); color: #58A6FF; &:hover { background: rgba(88,166,255,0.22); }`} ${({ $danger }) => $danger && `background: transparent; border-color: rgba(240,246,252,0.15); color: #8B949E; &:hover { color: #E6EDF3; }`} ${({ $primary, $danger }) => !$primary && !$danger && `background: transparent; border-color: rgba(240,246,252,0.15); color: #8B949E; &:hover { color: #E6EDF3; }`} `; const ResultMsg = styled.div<{ $success: boolean }>` padding: 8px 12px; border-radius: 6px; font-size: 12px; font-family: monospace; background: ${({ $success }) => $success ? 'rgba(0,230,118,0.08)' : 'rgba(255,23,68,0.08)'}; color: ${({ $success }) => $success ? '#00FF00' : '#FF1744'}; border: 1px solid ${({ $success }) => $success ? 'rgba(0,230,118,0.3)' : 'rgba(255,23,68,0.3)'}; `; export default function HarnessPage() { const [selected, setSelected] = useState({ sister: 'narang', file: 'SOUL.md' }); const [content, setContent] = useState(''); const [saved, setSaved] = useState(''); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [result, setResult] = useState<{ success: boolean; msg: string } | null>(null); useEffect(() => { setContent(''); setSaved(''); setResult(null); setLoading(true); adminFetch(`/api/admin/harness/${selected.sister}/${selected.file}`) .then((r) => r.json()) .then((d) => { setContent(d.content ?? ''); setSaved(d.content ?? ''); }) .catch(() => setContent('(λ‘œλ“œ μ‹€νŒ¨)')) .finally(() => setLoading(false)); }, [selected.sister, selected.file]); const handleSave = async () => { setSaving(true); setResult(null); try { const res = await adminFetch(`/api/admin/harness/${selected.sister}/${selected.file}`, { method: 'PUT', body: JSON.stringify({ content }), }); if (res.status === 401) { setResult({ success: false, msg: '인증 μ‹€νŒ¨ β€” API Key λ˜λŠ” JWT 토큰을 확인해' }); return; } if (!res.ok) { const text = await res.text(); setResult({ success: false, msg: `μ„œλ²„ 였λ₯˜ ${res.status}: ${text.slice(0, 100)}` }); return; } const d = await res.json(); if (d.success) { setSaved(content); setResult({ success: true, msg: 'μ €μž₯ μ™„λ£Œ' }); } else { setResult({ success: false, msg: d.error ?? 'μ €μž₯ μ‹€νŒ¨' }); } } catch (e) { setResult({ success: false, msg: (e as Error).message }); } finally { setSaving(false); } }; const isDirty = content !== saved; return ( {SISTERS.map((s) => ( {DISPLAY_NAMES[s] ?? s} {FILES.map((f) => ( setSelected({ sister: s, file: f })} > {f} ))} ))} ~/.openclaw/workspace/{selected.file} ({DISPLAY_NAMES[selected.sister]}) {isDirty && ( ● 변경됨 )} { setContent(saved); setResult(null); }} $danger>되돌리기 {saving ? 'μ €μž₯ 쀑...' : 'πŸ’Ύ μ €μž₯'} {result && {result.msg}} {loading ? (
λ‘œλ”© 쀑...
) : ( )}
); }