claude-controller 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/autoloop.sh +382 -0
- package/bin/ctl +327 -5
- package/bin/native-app.py +5 -2
- package/bin/watchdog.sh +357 -0
- package/cognitive/__init__.py +14 -0
- package/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/dispatcher.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/evaluator.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/goal_engine.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/learning.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/orchestrator.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/planner.cpython-314.pyc +0 -0
- package/cognitive/dispatcher.py +192 -0
- package/cognitive/evaluator.py +289 -0
- package/cognitive/goal_engine.py +232 -0
- package/cognitive/learning.py +189 -0
- package/cognitive/orchestrator.py +303 -0
- package/cognitive/planner.py +207 -0
- package/cognitive/prompts/analyst.md +31 -0
- package/cognitive/prompts/coder.md +22 -0
- package/cognitive/prompts/reviewer.md +33 -0
- package/cognitive/prompts/tester.md +21 -0
- package/cognitive/prompts/writer.md +25 -0
- package/config.sh +6 -1
- package/dag/__init__.py +5 -0
- package/dag/__pycache__/__init__.cpython-314.pyc +0 -0
- package/dag/__pycache__/graph.cpython-314.pyc +0 -0
- package/dag/graph.py +222 -0
- package/lib/jobs.sh +12 -1
- package/package.json +5 -1
- package/postinstall.sh +1 -1
- package/service/controller.sh +43 -11
- package/web/audit.py +122 -0
- package/web/checkpoint.py +80 -0
- package/web/config.py +2 -5
- package/web/handler.py +464 -26
- package/web/handler_fs.py +15 -14
- package/web/handler_goals.py +203 -0
- package/web/handler_jobs.py +165 -42
- package/web/handler_memory.py +203 -0
- package/web/jobs.py +576 -12
- package/web/personas.py +419 -0
- package/web/pipeline.py +682 -50
- package/web/presets.py +506 -0
- package/web/projects.py +58 -4
- package/web/static/api.js +90 -3
- package/web/static/app.js +8 -0
- package/web/static/base.css +51 -12
- package/web/static/context.js +14 -4
- package/web/static/form.css +3 -2
- package/web/static/goals.css +363 -0
- package/web/static/goals.js +300 -0
- package/web/static/i18n.js +288 -0
- package/web/static/index.html +142 -6
- package/web/static/jobs.css +951 -4
- package/web/static/jobs.js +890 -54
- package/web/static/memoryview.js +117 -0
- package/web/static/personas.js +228 -0
- package/web/static/pipeline.css +308 -1
- package/web/static/pipelines.js +249 -14
- package/web/static/presets.js +244 -0
- package/web/static/send.js +26 -4
- package/web/static/settings-style.css +34 -3
- package/web/static/settings.js +37 -1
- package/web/static/stream.js +242 -19
- package/web/static/utils.js +54 -2
- package/web/webhook.py +210 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════
|
|
2
|
+
Memory View — 메모리 검색/목록/CRUD UI
|
|
3
|
+
═══════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
let _memoryCache = [];
|
|
6
|
+
let _memoryTypeFilter = '';
|
|
7
|
+
let _memorySearchQuery = '';
|
|
8
|
+
let _memorySearchTimer = null;
|
|
9
|
+
|
|
10
|
+
/* ── Fetch & Render ── */
|
|
11
|
+
async function loadMemories() {
|
|
12
|
+
try {
|
|
13
|
+
const params = {};
|
|
14
|
+
if (_memoryTypeFilter) params.type = _memoryTypeFilter;
|
|
15
|
+
if (_memorySearchQuery) params.query = _memorySearchQuery;
|
|
16
|
+
const data = await fetchMemories(params);
|
|
17
|
+
_memoryCache = data.memories || [];
|
|
18
|
+
renderMemories();
|
|
19
|
+
} catch (e) {
|
|
20
|
+
const list = document.getElementById('memoryList');
|
|
21
|
+
if (list) list.innerHTML = `<div class="memory-empty">${t('msg_memory_failed')}: ${e.message}</div>`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderMemories() {
|
|
26
|
+
const list = document.getElementById('memoryList');
|
|
27
|
+
if (!list) return;
|
|
28
|
+
|
|
29
|
+
if (_memoryCache.length === 0) {
|
|
30
|
+
list.innerHTML = `<div class="memory-empty">${t('memory_no_items')}</div>`;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
list.innerHTML = _memoryCache.map(m => _renderMemoryCard(m)).join('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _renderMemoryCard(m) {
|
|
38
|
+
const typeLabel = t(`memory_type_${m.type}`) || m.type;
|
|
39
|
+
const tags = (m.tags || []).map(tag => `<span class="memory-tag">${_esc(tag)}</span>`).join('');
|
|
40
|
+
|
|
41
|
+
return `<div class="memory-card" id="mem-${m.id}">
|
|
42
|
+
<div class="memory-card-header">
|
|
43
|
+
<span class="memory-type-badge ${m.type || ''}">${typeLabel}</span>
|
|
44
|
+
<span class="memory-card-title">${_esc(m.title)}</span>
|
|
45
|
+
<div class="memory-card-actions">
|
|
46
|
+
<button onclick="onDeleteMemory('${m.id}')" title="${t('close')}">
|
|
47
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="memory-card-content">${_esc(m.content)}</div>
|
|
52
|
+
${tags ? `<div class="memory-card-tags">${tags}</div>` : ''}
|
|
53
|
+
</div>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ── Filter ── */
|
|
57
|
+
function setMemoryTypeFilter(type) {
|
|
58
|
+
_memoryTypeFilter = type;
|
|
59
|
+
loadMemories();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onMemorySearch(value) {
|
|
63
|
+
clearTimeout(_memorySearchTimer);
|
|
64
|
+
_memorySearchTimer = setTimeout(() => {
|
|
65
|
+
_memorySearchQuery = value.trim();
|
|
66
|
+
loadMemories();
|
|
67
|
+
}, 300);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ── Create ── */
|
|
71
|
+
function toggleMemoryForm() {
|
|
72
|
+
const form = document.getElementById('memoryCreateForm');
|
|
73
|
+
if (!form) return;
|
|
74
|
+
form.classList.toggle('visible');
|
|
75
|
+
if (form.classList.contains('visible')) {
|
|
76
|
+
const input = document.getElementById('memTitleInput');
|
|
77
|
+
if (input) input.focus();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function submitMemoryForm() {
|
|
82
|
+
const type = document.getElementById('memTypeSelect')?.value;
|
|
83
|
+
const title = (document.getElementById('memTitleInput')?.value || '').trim();
|
|
84
|
+
const content = (document.getElementById('memContentInput')?.value || '').trim();
|
|
85
|
+
const tagsStr = (document.getElementById('memTagsInput')?.value || '').trim();
|
|
86
|
+
|
|
87
|
+
if (!type || !title || !content) {
|
|
88
|
+
return showToast(t('msg_prompt_required'), 'error');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tags = tagsStr ? tagsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await createMemory({ type, title, content, tags });
|
|
95
|
+
showToast(t('msg_memory_created'));
|
|
96
|
+
// Reset form
|
|
97
|
+
document.getElementById('memTitleInput').value = '';
|
|
98
|
+
document.getElementById('memContentInput').value = '';
|
|
99
|
+
document.getElementById('memTagsInput').value = '';
|
|
100
|
+
document.getElementById('memoryCreateForm')?.classList.remove('visible');
|
|
101
|
+
loadMemories();
|
|
102
|
+
} catch (e) {
|
|
103
|
+
showToast(`${t('msg_memory_failed')}: ${e.message}`, 'error');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ── Delete ── */
|
|
108
|
+
async function onDeleteMemory(id) {
|
|
109
|
+
if (!confirm(t('memory_confirm_delete'))) return;
|
|
110
|
+
try {
|
|
111
|
+
await deleteMemory(id);
|
|
112
|
+
showToast(t('msg_memory_deleted'));
|
|
113
|
+
loadMemories();
|
|
114
|
+
} catch (e) {
|
|
115
|
+
showToast(`${t('msg_memory_failed')}: ${e.message}`, 'error');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/* ===================================================
|
|
2
|
+
Personas -- 직군별 전문가 페르소나 관리 UI
|
|
3
|
+
=================================================== */
|
|
4
|
+
|
|
5
|
+
let _personas = [];
|
|
6
|
+
let _selectedPersona = null; // 전송 시 적용할 페르소나 ID
|
|
7
|
+
|
|
8
|
+
const _PERSONA_ICONS = {
|
|
9
|
+
compass: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>',
|
|
10
|
+
server: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>',
|
|
11
|
+
layout: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>',
|
|
12
|
+
palette: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="13.5" cy="6.5" r="0.5" fill="currentColor"/><circle cx="17.5" cy="10.5" r="0.5" fill="currentColor"/><circle cx="8.5" cy="7.5" r="0.5" fill="currentColor"/><circle cx="6.5" cy="12.5" r="0.5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>',
|
|
13
|
+
'check-circle':'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
|
14
|
+
shield: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
|
|
15
|
+
cloud: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg>',
|
|
16
|
+
database: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>',
|
|
17
|
+
user: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function fetchPersonas() {
|
|
21
|
+
try {
|
|
22
|
+
_personas = await apiFetch('/api/personas');
|
|
23
|
+
renderPersonaCards();
|
|
24
|
+
_updatePersonaPicker();
|
|
25
|
+
} catch { /* silent */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderPersonaCards() {
|
|
29
|
+
const container = document.getElementById('personaGrid');
|
|
30
|
+
if (!container) return;
|
|
31
|
+
|
|
32
|
+
if (_personas.length === 0) {
|
|
33
|
+
container.innerHTML = '<div class="empty-state" style="padding:20px;text-align:center;color:var(--text-muted);font-size:0.8rem;">페르소나가 없습니다.</div>';
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
container.innerHTML = _personas.map(p => {
|
|
38
|
+
const icon = _PERSONA_ICONS[p.icon] || _PERSONA_ICONS.user;
|
|
39
|
+
const builtinBadge = p.builtin
|
|
40
|
+
? '<span class="persona-badge persona-badge-builtin">내장</span>'
|
|
41
|
+
: '<span class="persona-badge persona-badge-custom">커스텀</span>';
|
|
42
|
+
|
|
43
|
+
return `<div class="persona-card" data-persona-id="${escapeHtml(p.id)}" style="--persona-color:${escapeHtml(p.color)}">
|
|
44
|
+
<div class="persona-card-avatar" style="background:${escapeHtml(p.color)}20;color:${escapeHtml(p.color)}">${icon}</div>
|
|
45
|
+
<div class="persona-card-body">
|
|
46
|
+
<div class="persona-card-header">
|
|
47
|
+
<span class="persona-card-name">${escapeHtml(p.name)}</span>
|
|
48
|
+
${builtinBadge}
|
|
49
|
+
</div>
|
|
50
|
+
<div class="persona-card-desc">${escapeHtml(p.description)}</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="persona-card-actions">
|
|
53
|
+
<button class="btn btn-sm btn-primary" onclick="selectPersonaForSend('${escapeHtml(p.id)}')" title="이 페르소나로 작업 전송">배정</button>
|
|
54
|
+
<button class="btn btn-sm" onclick="openPersonaDetail('${escapeHtml(p.id)}')" title="상세 보기">상세</button>
|
|
55
|
+
${!p.builtin ? `<button class="btn btn-sm btn-danger" onclick="deletePersona('${escapeHtml(p.id)}')">삭제</button>` : ''}
|
|
56
|
+
</div>
|
|
57
|
+
</div>`;
|
|
58
|
+
}).join('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── 페르소나 선택 (전송 폼 연동) ── */
|
|
62
|
+
|
|
63
|
+
function selectPersonaForSend(personaId) {
|
|
64
|
+
if (_selectedPersona === personaId) {
|
|
65
|
+
_selectedPersona = null;
|
|
66
|
+
} else {
|
|
67
|
+
_selectedPersona = personaId;
|
|
68
|
+
}
|
|
69
|
+
_updatePersonaPicker();
|
|
70
|
+
const persona = _personas.find(p => p.id === personaId);
|
|
71
|
+
if (_selectedPersona && persona) {
|
|
72
|
+
showToast(`${persona.name} 페르소나 배정됨`);
|
|
73
|
+
} else {
|
|
74
|
+
showToast('페르소나 배정 해제됨');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function clearPersonaSelection() {
|
|
79
|
+
_selectedPersona = null;
|
|
80
|
+
_updatePersonaPicker();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _updatePersonaPicker() {
|
|
84
|
+
const badge = document.getElementById('personaBadge');
|
|
85
|
+
if (!badge) return;
|
|
86
|
+
if (_selectedPersona) {
|
|
87
|
+
const p = _personas.find(x => x.id === _selectedPersona);
|
|
88
|
+
if (p) {
|
|
89
|
+
const icon = _PERSONA_ICONS[p.icon] || _PERSONA_ICONS.user;
|
|
90
|
+
badge.innerHTML = `<span class="persona-active-badge" style="--persona-color:${escapeHtml(p.color)}" onclick="clearPersonaSelection()" title="${escapeHtml(p.name)} (클릭하여 해제)">${icon} ${escapeHtml(p.name)}</span>`;
|
|
91
|
+
badge.style.display = '';
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
badge.innerHTML = '';
|
|
96
|
+
badge.style.display = 'none';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ── 페르소나 상세 다이얼로그 ── */
|
|
100
|
+
|
|
101
|
+
async function openPersonaDetail(personaId) {
|
|
102
|
+
let persona;
|
|
103
|
+
try {
|
|
104
|
+
persona = await apiFetch(`/api/personas/${encodeURIComponent(personaId)}`);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
showToast(err.message, 'error');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const icon = _PERSONA_ICONS[persona.icon] || _PERSONA_ICONS.user;
|
|
111
|
+
const overlay = document.createElement('div');
|
|
112
|
+
overlay.className = 'settings-overlay';
|
|
113
|
+
overlay.style.cssText = 'display:flex;align-items:center;justify-content:center;';
|
|
114
|
+
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
|
|
115
|
+
|
|
116
|
+
overlay.innerHTML = `<div class="settings-panel" style="max-width:560px;margin:0;">
|
|
117
|
+
<div class="settings-header">
|
|
118
|
+
<div class="settings-title" style="display:flex;align-items:center;gap:8px;">
|
|
119
|
+
<span style="color:${escapeHtml(persona.color)}">${icon}</span>
|
|
120
|
+
<span>${escapeHtml(persona.name)}</span>
|
|
121
|
+
${persona.builtin ? '<span class="persona-badge persona-badge-builtin">내장</span>' : '<span class="persona-badge persona-badge-custom">커스텀</span>'}
|
|
122
|
+
</div>
|
|
123
|
+
<button class="settings-close" onclick="this.closest('.settings-overlay').remove()">
|
|
124
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="settings-body">
|
|
128
|
+
<div style="margin-bottom:12px;font-size:0.78rem;color:var(--text-secondary);">${escapeHtml(persona.description)}</div>
|
|
129
|
+
<div style="margin-bottom:8px;">
|
|
130
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:6px;">시스템 프롬프트</label>
|
|
131
|
+
<pre class="persona-prompt-preview">${escapeHtml(persona.system_prompt || '(없음)')}</pre>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="settings-footer" style="display:flex;gap:6px;">
|
|
135
|
+
<button class="btn btn-sm btn-primary" onclick="selectPersonaForSend('${escapeHtml(persona.id)}');this.closest('.settings-overlay').remove();">배정</button>
|
|
136
|
+
<button class="btn btn-sm" onclick="this.closest('.settings-overlay').remove()">닫기</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>`;
|
|
139
|
+
document.body.appendChild(overlay);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* ── 커스텀 페르소나 생성 다이얼로그 ── */
|
|
143
|
+
|
|
144
|
+
function openCreatePersonaDialog() {
|
|
145
|
+
const dlgId = 'personaCreate_' + Date.now();
|
|
146
|
+
const overlay = document.createElement('div');
|
|
147
|
+
overlay.className = 'settings-overlay';
|
|
148
|
+
overlay.style.cssText = 'display:flex;align-items:center;justify-content:center;';
|
|
149
|
+
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
|
|
150
|
+
|
|
151
|
+
const roleOptions = ['custom','planner','developer','designer','qa','security','devops','data']
|
|
152
|
+
.map(r => `<option value="${r}">${r}</option>`).join('');
|
|
153
|
+
|
|
154
|
+
overlay.innerHTML = `<div class="settings-panel" style="max-width:520px;margin:0;">
|
|
155
|
+
<div class="settings-header">
|
|
156
|
+
<div class="settings-title" style="display:flex;align-items:center;gap:8px;">
|
|
157
|
+
${_PERSONA_ICONS.user}
|
|
158
|
+
<span>페르소나 만들기</span>
|
|
159
|
+
</div>
|
|
160
|
+
<button class="settings-close" onclick="this.closest('.settings-overlay').remove()">
|
|
161
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="settings-body">
|
|
165
|
+
<div style="margin-bottom:10px;">
|
|
166
|
+
<label class="persona-dlg-label">이름 *</label>
|
|
167
|
+
<input id="${dlgId}_name" type="text" class="persona-dlg-input" placeholder="예: 코드 아키텍트">
|
|
168
|
+
</div>
|
|
169
|
+
<div style="margin-bottom:10px;">
|
|
170
|
+
<label class="persona-dlg-label">역할</label>
|
|
171
|
+
<select id="${dlgId}_role" class="persona-dlg-input">${roleOptions}</select>
|
|
172
|
+
</div>
|
|
173
|
+
<div style="margin-bottom:10px;">
|
|
174
|
+
<label class="persona-dlg-label">설명</label>
|
|
175
|
+
<input id="${dlgId}_desc" type="text" class="persona-dlg-input" placeholder="이 페르소나의 전문 분야를 설명하세요">
|
|
176
|
+
</div>
|
|
177
|
+
<div style="margin-bottom:10px;">
|
|
178
|
+
<label class="persona-dlg-label">시스템 프롬프트 *</label>
|
|
179
|
+
<textarea id="${dlgId}_prompt" class="persona-dlg-input" rows="8" placeholder="당신은 ... 전문가입니다.\n\n## 역할\n- ...\n\n## 원칙\n- ..."></textarea>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="settings-footer" style="display:flex;gap:6px;">
|
|
183
|
+
<button class="btn btn-sm btn-primary" onclick="_doCreatePersona('${dlgId}',this)">생성</button>
|
|
184
|
+
<button class="btn btn-sm" onclick="this.closest('.settings-overlay').remove()">취소</button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>`;
|
|
187
|
+
document.body.appendChild(overlay);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function _doCreatePersona(dlgId, btn) {
|
|
191
|
+
const name = document.getElementById(dlgId + '_name').value.trim();
|
|
192
|
+
const role = document.getElementById(dlgId + '_role').value;
|
|
193
|
+
const desc = document.getElementById(dlgId + '_desc').value.trim();
|
|
194
|
+
const prompt = document.getElementById(dlgId + '_prompt').value.trim();
|
|
195
|
+
|
|
196
|
+
if (!name) { showToast('이름을 입력하세요', 'error'); return; }
|
|
197
|
+
if (!prompt) { showToast('시스템 프롬프트를 입력하세요', 'error'); return; }
|
|
198
|
+
|
|
199
|
+
btn.disabled = true;
|
|
200
|
+
btn.textContent = '생성 중...';
|
|
201
|
+
try {
|
|
202
|
+
await apiFetch('/api/personas', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
body: JSON.stringify({ name, role, description: desc, system_prompt: prompt }),
|
|
205
|
+
});
|
|
206
|
+
showToast(`페르소나 "${name}" 생성됨`);
|
|
207
|
+
btn.closest('.settings-overlay').remove();
|
|
208
|
+
fetchPersonas();
|
|
209
|
+
} catch (err) {
|
|
210
|
+
showToast(`생성 실패: ${err.message}`, 'error');
|
|
211
|
+
btn.disabled = false;
|
|
212
|
+
btn.textContent = '생성';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ── 삭제 ── */
|
|
217
|
+
|
|
218
|
+
async function deletePersona(personaId) {
|
|
219
|
+
if (!confirm('이 페르소나를 삭제하시겠습니까?')) return;
|
|
220
|
+
try {
|
|
221
|
+
await apiFetch(`/api/personas/${encodeURIComponent(personaId)}`, { method: 'DELETE' });
|
|
222
|
+
showToast('페르소나 삭제됨');
|
|
223
|
+
if (_selectedPersona === personaId) clearPersonaSelection();
|
|
224
|
+
fetchPersonas();
|
|
225
|
+
} catch (err) {
|
|
226
|
+
showToast(err.message || '삭제 실패', 'error');
|
|
227
|
+
}
|
|
228
|
+
}
|
package/web/static/pipeline.css
CHANGED
|
@@ -1,4 +1,230 @@
|
|
|
1
|
-
/* ──
|
|
1
|
+
/* ── Preset ── */
|
|
2
|
+
.preset-grid {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
5
|
+
gap: 1px;
|
|
6
|
+
background: var(--border);
|
|
7
|
+
}
|
|
8
|
+
.preset-card {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: 12px;
|
|
11
|
+
padding: 14px 16px;
|
|
12
|
+
background: var(--surface);
|
|
13
|
+
cursor: default;
|
|
14
|
+
transition: background 0.15s;
|
|
15
|
+
}
|
|
16
|
+
.preset-card:hover {
|
|
17
|
+
background: var(--bg-secondary);
|
|
18
|
+
}
|
|
19
|
+
.preset-card-icon {
|
|
20
|
+
flex-shrink: 0;
|
|
21
|
+
width: 36px;
|
|
22
|
+
height: 36px;
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
background: rgba(99, 102, 241, 0.1);
|
|
27
|
+
color: var(--primary);
|
|
28
|
+
border-radius: 8px;
|
|
29
|
+
}
|
|
30
|
+
.preset-card-body {
|
|
31
|
+
flex: 1;
|
|
32
|
+
min-width: 0;
|
|
33
|
+
}
|
|
34
|
+
.preset-card-header {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 6px;
|
|
38
|
+
margin-bottom: 4px;
|
|
39
|
+
}
|
|
40
|
+
.preset-card-name {
|
|
41
|
+
font-size: 0.82rem;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
color: var(--text);
|
|
44
|
+
}
|
|
45
|
+
.preset-card-desc {
|
|
46
|
+
font-size: 0.72rem;
|
|
47
|
+
color: var(--text-secondary);
|
|
48
|
+
line-height: 1.4;
|
|
49
|
+
margin-bottom: 6px;
|
|
50
|
+
}
|
|
51
|
+
.preset-card-pipes {
|
|
52
|
+
font-size: 0.68rem;
|
|
53
|
+
color: var(--text-muted);
|
|
54
|
+
font-family: var(--font-mono, monospace);
|
|
55
|
+
}
|
|
56
|
+
.preset-pipe-count {
|
|
57
|
+
color: var(--text-muted);
|
|
58
|
+
font-size: 0.65rem;
|
|
59
|
+
}
|
|
60
|
+
.preset-card-actions {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 4px;
|
|
64
|
+
align-self: center;
|
|
65
|
+
flex-shrink: 0;
|
|
66
|
+
}
|
|
67
|
+
.preset-badge {
|
|
68
|
+
font-size: 0.6rem;
|
|
69
|
+
padding: 1px 6px;
|
|
70
|
+
border-radius: 3px;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
text-transform: uppercase;
|
|
73
|
+
letter-spacing: 0.3px;
|
|
74
|
+
}
|
|
75
|
+
.preset-badge-builtin {
|
|
76
|
+
background: rgba(59, 130, 246, 0.1);
|
|
77
|
+
color: var(--primary);
|
|
78
|
+
}
|
|
79
|
+
.preset-badge-custom {
|
|
80
|
+
background: rgba(168, 85, 247, 0.1);
|
|
81
|
+
color: #a855f7;
|
|
82
|
+
}
|
|
83
|
+
.preset-pipe-tag {
|
|
84
|
+
display: inline-block;
|
|
85
|
+
padding: 2px 8px;
|
|
86
|
+
background: var(--bg-secondary);
|
|
87
|
+
border: 1px solid var(--border);
|
|
88
|
+
border-radius: 4px;
|
|
89
|
+
font-size: 0.72rem;
|
|
90
|
+
color: var(--text-secondary);
|
|
91
|
+
font-family: var(--font-mono, monospace);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ── Persona ── */
|
|
95
|
+
.persona-grid {
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
98
|
+
gap: 1px;
|
|
99
|
+
background: var(--border);
|
|
100
|
+
}
|
|
101
|
+
.persona-card {
|
|
102
|
+
display: flex;
|
|
103
|
+
gap: 12px;
|
|
104
|
+
padding: 14px 16px;
|
|
105
|
+
background: var(--surface);
|
|
106
|
+
cursor: default;
|
|
107
|
+
transition: background 0.15s;
|
|
108
|
+
}
|
|
109
|
+
.persona-card:hover {
|
|
110
|
+
background: var(--bg-secondary);
|
|
111
|
+
}
|
|
112
|
+
.persona-card-avatar {
|
|
113
|
+
flex-shrink: 0;
|
|
114
|
+
width: 36px;
|
|
115
|
+
height: 36px;
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
border-radius: 50%;
|
|
120
|
+
}
|
|
121
|
+
.persona-card-body {
|
|
122
|
+
flex: 1;
|
|
123
|
+
min-width: 0;
|
|
124
|
+
}
|
|
125
|
+
.persona-card-header {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 6px;
|
|
129
|
+
margin-bottom: 4px;
|
|
130
|
+
}
|
|
131
|
+
.persona-card-name {
|
|
132
|
+
font-size: 0.82rem;
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
color: var(--text);
|
|
135
|
+
}
|
|
136
|
+
.persona-card-desc {
|
|
137
|
+
font-size: 0.72rem;
|
|
138
|
+
color: var(--text-secondary);
|
|
139
|
+
line-height: 1.4;
|
|
140
|
+
}
|
|
141
|
+
.persona-card-actions {
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: column;
|
|
144
|
+
gap: 4px;
|
|
145
|
+
align-self: center;
|
|
146
|
+
flex-shrink: 0;
|
|
147
|
+
}
|
|
148
|
+
.persona-badge {
|
|
149
|
+
font-size: 0.6rem;
|
|
150
|
+
padding: 1px 6px;
|
|
151
|
+
border-radius: 3px;
|
|
152
|
+
font-weight: 600;
|
|
153
|
+
text-transform: uppercase;
|
|
154
|
+
letter-spacing: 0.3px;
|
|
155
|
+
}
|
|
156
|
+
.persona-badge-builtin {
|
|
157
|
+
background: rgba(59, 130, 246, 0.1);
|
|
158
|
+
color: var(--primary);
|
|
159
|
+
}
|
|
160
|
+
.persona-badge-custom {
|
|
161
|
+
background: rgba(168, 85, 247, 0.1);
|
|
162
|
+
color: #a855f7;
|
|
163
|
+
}
|
|
164
|
+
.persona-active-badge {
|
|
165
|
+
display: inline-flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 4px;
|
|
168
|
+
padding: 2px 8px;
|
|
169
|
+
border-radius: 12px;
|
|
170
|
+
font-size: 0.68rem;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
background: color-mix(in srgb, var(--persona-color, #6366f1) 12%, transparent);
|
|
174
|
+
color: var(--persona-color, #6366f1);
|
|
175
|
+
border: 1px solid color-mix(in srgb, var(--persona-color, #6366f1) 25%, transparent);
|
|
176
|
+
transition: opacity 0.15s;
|
|
177
|
+
}
|
|
178
|
+
.persona-active-badge:hover {
|
|
179
|
+
opacity: 0.7;
|
|
180
|
+
}
|
|
181
|
+
.persona-active-badge svg {
|
|
182
|
+
width: 12px;
|
|
183
|
+
height: 12px;
|
|
184
|
+
}
|
|
185
|
+
.persona-prompt-preview {
|
|
186
|
+
font-size: 0.72rem;
|
|
187
|
+
line-height: 1.5;
|
|
188
|
+
color: var(--text-secondary);
|
|
189
|
+
background: var(--bg-secondary);
|
|
190
|
+
border: 1px solid var(--border);
|
|
191
|
+
border-radius: 6px;
|
|
192
|
+
padding: 12px;
|
|
193
|
+
max-height: 300px;
|
|
194
|
+
overflow-y: auto;
|
|
195
|
+
white-space: pre-wrap;
|
|
196
|
+
word-break: break-word;
|
|
197
|
+
font-family: var(--font-mono, monospace);
|
|
198
|
+
margin: 0;
|
|
199
|
+
}
|
|
200
|
+
.persona-dlg-label {
|
|
201
|
+
font-size: 0.72rem;
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
color: var(--text-secondary);
|
|
204
|
+
display: block;
|
|
205
|
+
margin-bottom: 4px;
|
|
206
|
+
}
|
|
207
|
+
.persona-dlg-input {
|
|
208
|
+
width: 100%;
|
|
209
|
+
padding: 6px 10px;
|
|
210
|
+
border: 1px solid var(--border);
|
|
211
|
+
border-radius: 6px;
|
|
212
|
+
background: var(--bg-secondary);
|
|
213
|
+
color: var(--text-primary);
|
|
214
|
+
font-size: 0.8rem;
|
|
215
|
+
box-sizing: border-box;
|
|
216
|
+
font-family: inherit;
|
|
217
|
+
}
|
|
218
|
+
.persona-dlg-input:focus {
|
|
219
|
+
outline: none;
|
|
220
|
+
border-color: var(--accent);
|
|
221
|
+
}
|
|
222
|
+
textarea.persona-dlg-input {
|
|
223
|
+
font-family: var(--font-mono, monospace);
|
|
224
|
+
font-size: 0.75rem;
|
|
225
|
+
resize: vertical;
|
|
226
|
+
}
|
|
227
|
+
|
|
2
228
|
/* ── Pipeline ── */
|
|
3
229
|
.pipeline-list {
|
|
4
230
|
display: flex;
|
|
@@ -29,3 +255,84 @@
|
|
|
29
255
|
margin-top: 4px;
|
|
30
256
|
line-height: 1.4;
|
|
31
257
|
}
|
|
258
|
+
|
|
259
|
+
/* ── Running state ── */
|
|
260
|
+
.pipeline-card.is-running {
|
|
261
|
+
border-left: 2px solid var(--yellow);
|
|
262
|
+
}
|
|
263
|
+
.pipe-running-badge {
|
|
264
|
+
display: inline-flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: 4px;
|
|
267
|
+
font-size: 0.65rem;
|
|
268
|
+
padding: 1px 6px;
|
|
269
|
+
border-radius: 3px;
|
|
270
|
+
background: var(--yellow-dim);
|
|
271
|
+
color: var(--yellow);
|
|
272
|
+
font-weight: 600;
|
|
273
|
+
}
|
|
274
|
+
.pipe-running-dot {
|
|
275
|
+
width: 6px;
|
|
276
|
+
height: 6px;
|
|
277
|
+
border-radius: 50%;
|
|
278
|
+
background: var(--yellow);
|
|
279
|
+
animation: pulse 1.5s infinite;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ── History table ── */
|
|
283
|
+
.pipe-hist-table-wrap {
|
|
284
|
+
overflow-x: auto;
|
|
285
|
+
max-height: 360px;
|
|
286
|
+
overflow-y: auto;
|
|
287
|
+
}
|
|
288
|
+
.pipe-hist-table {
|
|
289
|
+
width: 100%;
|
|
290
|
+
border-collapse: collapse;
|
|
291
|
+
font-size: 0.72rem;
|
|
292
|
+
}
|
|
293
|
+
.pipe-hist-table th {
|
|
294
|
+
text-align: left;
|
|
295
|
+
padding: 6px 8px;
|
|
296
|
+
font-weight: 600;
|
|
297
|
+
color: var(--text-secondary);
|
|
298
|
+
border-bottom: 1px solid var(--border);
|
|
299
|
+
position: sticky;
|
|
300
|
+
top: 0;
|
|
301
|
+
background: var(--surface);
|
|
302
|
+
}
|
|
303
|
+
.pipe-hist-table td {
|
|
304
|
+
padding: 6px 8px;
|
|
305
|
+
border-bottom: 1px solid var(--border);
|
|
306
|
+
color: var(--text-primary);
|
|
307
|
+
vertical-align: top;
|
|
308
|
+
}
|
|
309
|
+
.pipe-hist-table tr:hover td {
|
|
310
|
+
background: var(--bg-secondary);
|
|
311
|
+
}
|
|
312
|
+
.pipe-hist-result {
|
|
313
|
+
max-width: 280px;
|
|
314
|
+
overflow: hidden;
|
|
315
|
+
text-overflow: ellipsis;
|
|
316
|
+
white-space: nowrap;
|
|
317
|
+
font-size: 0.68rem;
|
|
318
|
+
color: var(--text-muted);
|
|
319
|
+
}
|
|
320
|
+
.pipe-hist-badge {
|
|
321
|
+
display: inline-block;
|
|
322
|
+
font-size: 0.62rem;
|
|
323
|
+
padding: 1px 6px;
|
|
324
|
+
border-radius: 3px;
|
|
325
|
+
font-weight: 600;
|
|
326
|
+
}
|
|
327
|
+
.pipe-hist-change {
|
|
328
|
+
background: rgba(34, 197, 94, 0.1);
|
|
329
|
+
color: #22c55e;
|
|
330
|
+
}
|
|
331
|
+
.pipe-hist-nochange {
|
|
332
|
+
background: rgba(107, 114, 128, 0.1);
|
|
333
|
+
color: var(--text-muted);
|
|
334
|
+
}
|
|
335
|
+
.pipe-hist-unknown {
|
|
336
|
+
background: rgba(168, 85, 247, 0.1);
|
|
337
|
+
color: #a855f7;
|
|
338
|
+
}
|