@yemi33/minions 0.1.2194 → 0.1.2196
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/dashboard/js/command-center.js +2 -0
- package/dashboard/js/fre.js +125 -1
- package/dashboard/js/modal.js +35 -1
- package/dashboard/js/refresh.js +7 -1
- package/dashboard/js/render-agents.js +18 -0
- package/dashboard/js/render-kb.js +27 -2
- package/dashboard/js/render-meetings.js +36 -3
- package/dashboard/js/render-pipelines.js +15 -10
- package/dashboard/js/render-plans.js +73 -3
- package/dashboard/js/render-prd.js +29 -5
- package/dashboard/js/render-prs.js +174 -3
- package/dashboard/js/render-schedules.js +12 -0
- package/dashboard/js/render-utils.js +151 -1
- package/dashboard/js/render-watches.js +61 -7
- package/dashboard/js/render-work-items.js +161 -20
- package/dashboard/js/state.js +110 -1
- package/dashboard/js/utils.js +246 -14
- package/dashboard/layout.html +2 -0
- package/dashboard/slim/body.html +23 -13
- package/dashboard/slim/js/knowledge.js +576 -0
- package/dashboard/slim/js/members.js +43 -0
- package/dashboard/slim/js/modals-tiles.js +6 -4
- package/dashboard/slim/js/pinned.js +7 -19
- package/dashboard/slim/js/status.js +17 -21
- package/dashboard/slim/styles.css +94 -36
- package/dashboard/styles.css +34 -0
- package/dashboard-build.js +1 -1
- package/dashboard.js +50 -2
- package/engine/lifecycle.js +6 -0
- package/engine/pipeline.js +10 -0
- package/engine/queries.js +81 -0
- package/engine/scheduler.js +19 -1
- package/package.json +1 -1
package/dashboard/layout.html
CHANGED
|
@@ -127,6 +127,8 @@
|
|
|
127
127
|
<div class="modal-header">
|
|
128
128
|
<h3 id="modal-title">—</h3>
|
|
129
129
|
<div class="modal-header-actions">
|
|
130
|
+
<button class="modal-copy modal-back-btn" id="modal-back-btn" onclick="popModalFrame()" title="Back to previous modal" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M9.78 12.78a.75.75 0 01-1.06 0L4.47 8.53a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L6.06 8l3.72 3.72a.75.75 0 010 1.06z"/></svg> Back</button>
|
|
131
|
+
<span class="modal-stack-chip" id="modal-stack-chip" title="Modal stack depth" style="display:none">1/5</span>
|
|
130
132
|
<button class="modal-copy" id="modal-edit-btn" onclick="modalToggleEdit()" title="Edit" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61zM12.9 2.97L4.288 11.58l-.537 1.878 1.878-.537L14.242 4.31 12.9 2.97z"/></svg> Edit</button>
|
|
131
133
|
<button class="modal-copy is-success" id="modal-save-btn" onclick="modalSaveEdit()" title="Save" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Save</button>
|
|
132
134
|
<button class="modal-copy" id="modal-cancel-edit-btn" onclick="modalCancelEdit()" title="Cancel edit" style="display:none">Cancel</button>
|
package/dashboard/slim/body.html
CHANGED
|
@@ -106,11 +106,11 @@
|
|
|
106
106
|
<div class="cockpit-value dim">0</div>
|
|
107
107
|
<div class="cockpit-detail">no watches set</div>
|
|
108
108
|
</div>
|
|
109
|
-
<div class="cockpit-tile" data-tile="
|
|
109
|
+
<div class="cockpit-tile" data-tile="knowledge">
|
|
110
110
|
<button id="slim-tile-pin-chip" class="linkpr-chip on-tile" type="button" title="Pin content">+ Pin</button>
|
|
111
|
-
<div class="cockpit-label"><span class="cockpit-dot"></span>
|
|
111
|
+
<div class="cockpit-label"><span class="cockpit-dot"></span> Knowledge</div>
|
|
112
112
|
<div class="cockpit-value dim">0</div>
|
|
113
|
-
<div class="cockpit-detail">
|
|
113
|
+
<div class="cockpit-detail">pinned context, notes & KB</div>
|
|
114
114
|
</div>
|
|
115
115
|
</div>
|
|
116
116
|
</div>
|
|
@@ -187,18 +187,28 @@
|
|
|
187
187
|
</div>
|
|
188
188
|
</div>
|
|
189
189
|
|
|
190
|
-
<!--
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
<!-- Knowledge control panel — opened from the "Knowledge" status tile. One
|
|
191
|
+
unified box surfacing all three knowledge surfaces via tabs: Pinned
|
|
192
|
+
Context, Notes, and KB (knowledge base). Each tab is rendered lazily by
|
|
193
|
+
dashboard/slim/js/knowledge.js into #slim-knowledge-body. Backed by the
|
|
194
|
+
existing endpoints (no new server routes):
|
|
195
|
+
Pinned — /api/pinned (+ update/remove), surfaced via /api/status.
|
|
196
|
+
Notes — /api/notes-full (read), /api/notes-save (edit notes.md),
|
|
197
|
+
/api/notes (add an inbox note).
|
|
198
|
+
KB — /api/knowledge (list), /api/knowledge/:cat/:file (read),
|
|
199
|
+
/api/knowledge POST (create), /api/kb-pins(+/toggle). -->
|
|
200
|
+
<div class="modal-bg" id="slim-knowledge-modal">
|
|
201
|
+
<div class="modal slim-knowledge-modal-inner">
|
|
196
202
|
<div class="modal-header">
|
|
197
|
-
<h3>
|
|
198
|
-
<
|
|
199
|
-
|
|
203
|
+
<h3>Knowledge</h3>
|
|
204
|
+
<div class="kn-tabs" id="slim-kn-tabs" role="tablist">
|
|
205
|
+
<button class="kn-tab active" id="slim-kn-tab-pinned" data-kn-tab="pinned" type="button" role="tab" aria-selected="true" aria-controls="slim-knowledge-body">Pinned Context</button>
|
|
206
|
+
<button class="kn-tab" id="slim-kn-tab-notes" data-kn-tab="notes" type="button" role="tab" aria-selected="false" aria-controls="slim-knowledge-body">Notes</button>
|
|
207
|
+
<button class="kn-tab" id="slim-kn-tab-kb" data-kn-tab="kb" type="button" role="tab" aria-selected="false" aria-controls="slim-knowledge-body">KB</button>
|
|
208
|
+
</div>
|
|
209
|
+
<button id="slim-knowledge-close" class="icon-btn" title="Close" style="margin-left:auto">×</button>
|
|
200
210
|
</div>
|
|
201
|
-
<div class="modal-body" id="slim-
|
|
211
|
+
<div class="modal-body" id="slim-knowledge-body" role="tabpanel" tabindex="0" aria-labelledby="slim-kn-tab-pinned"></div>
|
|
202
212
|
</div>
|
|
203
213
|
</div>
|
|
204
214
|
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
|
|
2
|
+
// ── Knowledge control panel (Pinned Context + Notes + KB) ──────────
|
|
3
|
+
// One unified cockpit box over the three knowledge surfaces, replacing the
|
|
4
|
+
// old single "Pinned context" tile. Reuses the classic dashboard's existing
|
|
5
|
+
// endpoints (no new server routes):
|
|
6
|
+
// Pinned — /api/pinned (+ update/remove); the Pinned-Context tab reuses the
|
|
7
|
+
// editor + list renderer in pinned.js.
|
|
8
|
+
// Notes — /api/notes-full (read notes.md), /api/notes-save (edit notes.md),
|
|
9
|
+
// /api/notes (add an inbox note for consolidation).
|
|
10
|
+
// KB — /api/knowledge (list), /api/knowledge/:cat/:file (read entry),
|
|
11
|
+
// /api/knowledge POST (create), /api/kb-pins(+/toggle) (pin state).
|
|
12
|
+
//
|
|
13
|
+
// Counts: pinned is live from the 5s status poll (renderKnowledgeTile is fed
|
|
14
|
+
// by applyStatus). Notes + KB counts are fetched lazily — once shortly after
|
|
15
|
+
// load and again after any create/save — so the heavier /api/notes-full and
|
|
16
|
+
// /api/knowledge reads stay off the recurring poll.
|
|
17
|
+
|
|
18
|
+
var _knowledgeCounts = { pinned: 0, notes: null, kb: null };
|
|
19
|
+
var _knActiveTab = 'pinned';
|
|
20
|
+
var _kbData = null; // cached /api/knowledge payload
|
|
21
|
+
var _kbPins = []; // cached pinned KB keys ('knowledge/<cat>/<file>')
|
|
22
|
+
|
|
23
|
+
var KB_CAT_LABELS = {
|
|
24
|
+
architecture: 'Architecture', conventions: 'Conventions',
|
|
25
|
+
'project-notes': 'Project Notes', 'build-reports': 'Build Reports',
|
|
26
|
+
reviews: 'Reviews', learnings: 'Learnings', decisions: 'Decisions',
|
|
27
|
+
incidents: 'Incidents', 'api-notes': 'API Notes',
|
|
28
|
+
};
|
|
29
|
+
// Categories offered in the KB create form (mirrors the classic dashboard).
|
|
30
|
+
var KB_CREATE_CATS = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
|
|
31
|
+
var KB_LIST_CAP = 60; // cap rendered rows; the KB can hold thousands
|
|
32
|
+
|
|
33
|
+
// ── Cockpit tile ───────────────────────────────────────────────────
|
|
34
|
+
// Called from applyStatus with the live pinned count each poll, and from the
|
|
35
|
+
// lazy count loader (no arg) to repaint when notes/KB counts arrive.
|
|
36
|
+
function renderKnowledgeTile(pinnedCount) {
|
|
37
|
+
if (typeof pinnedCount === 'number') _knowledgeCounts.pinned = pinnedCount;
|
|
38
|
+
var p = _knowledgeCounts.pinned || 0;
|
|
39
|
+
var n = _knowledgeCounts.notes;
|
|
40
|
+
var k = _knowledgeCounts.kb;
|
|
41
|
+
var total = p + (n || 0) + (k || 0);
|
|
42
|
+
var detail = p + ' pinned · ' + (n == null ? '…' : n) + ' notes · ' + (k == null ? '…' : k) + ' KB';
|
|
43
|
+
updateTile('knowledge', total, detail, total ? 'blue' : null);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _countNotesEntries(text) {
|
|
47
|
+
if (!text || text === 'No notes file found.') return 0;
|
|
48
|
+
var m = text.match(/^###\s/gm);
|
|
49
|
+
return m ? m.length : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _countKbEntries(data) {
|
|
53
|
+
if (!data || typeof data !== 'object') return 0;
|
|
54
|
+
var total = 0;
|
|
55
|
+
Object.keys(data).forEach(function(k) {
|
|
56
|
+
if (Array.isArray(data[k])) total += data[k].length;
|
|
57
|
+
});
|
|
58
|
+
return total;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Best-effort lazy fetch of the notes + KB counts. Failures leave the cached
|
|
62
|
+
// value untouched so a transient error doesn't blank the tile detail.
|
|
63
|
+
async function loadKnowledgeCounts() {
|
|
64
|
+
try {
|
|
65
|
+
var res = await fetch('/api/notes-full', { headers: { 'Accept': 'text/plain' } });
|
|
66
|
+
if (res.ok) _knowledgeCounts.notes = _countNotesEntries(await res.text());
|
|
67
|
+
} catch (e) { /* keep last-known */ }
|
|
68
|
+
try {
|
|
69
|
+
var kres = await fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } });
|
|
70
|
+
if (kres.ok) {
|
|
71
|
+
_kbData = await kres.json();
|
|
72
|
+
_knowledgeCounts.kb = _countKbEntries(_kbData);
|
|
73
|
+
}
|
|
74
|
+
} catch (e) { /* keep last-known */ }
|
|
75
|
+
renderKnowledgeTile();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function loadKbPins() {
|
|
79
|
+
try {
|
|
80
|
+
var res = await fetch('/api/kb-pins', { headers: { 'Accept': 'application/json' } });
|
|
81
|
+
if (!res.ok) return;
|
|
82
|
+
var d = await res.json();
|
|
83
|
+
_kbPins = Array.isArray(d.pins) ? d.pins : [];
|
|
84
|
+
} catch (e) { _kbPins = _kbPins || []; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function kbPinKey(cat, file) { return 'knowledge/' + cat + '/' + file; }
|
|
88
|
+
function isKbPinned(key) { return _kbPins.indexOf(key) !== -1; }
|
|
89
|
+
|
|
90
|
+
// ── Modal open/close + tab switching ───────────────────────────────
|
|
91
|
+
function openKnowledgeModal() {
|
|
92
|
+
var modal = document.getElementById('slim-knowledge-modal');
|
|
93
|
+
if (!modal) return;
|
|
94
|
+
renderKnowledgeTab();
|
|
95
|
+
modal.classList.add('open');
|
|
96
|
+
}
|
|
97
|
+
function closeKnowledgeModal() {
|
|
98
|
+
var modal = document.getElementById('slim-knowledge-modal');
|
|
99
|
+
if (modal) modal.classList.remove('open');
|
|
100
|
+
}
|
|
101
|
+
function setKnowledgeTab(tab) {
|
|
102
|
+
_knActiveTab = tab;
|
|
103
|
+
var tabs = document.querySelectorAll('#slim-kn-tabs .kn-tab');
|
|
104
|
+
var activeTabId = null;
|
|
105
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
106
|
+
var isActive = tabs[i].getAttribute('data-kn-tab') === tab;
|
|
107
|
+
tabs[i].classList.toggle('active', isActive);
|
|
108
|
+
tabs[i].setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
109
|
+
if (isActive) activeTabId = tabs[i].id;
|
|
110
|
+
}
|
|
111
|
+
var body = document.getElementById('slim-knowledge-body');
|
|
112
|
+
if (body && activeTabId) body.setAttribute('aria-labelledby', activeTabId);
|
|
113
|
+
renderKnowledgeTab();
|
|
114
|
+
}
|
|
115
|
+
function renderKnowledgeTab() {
|
|
116
|
+
var body = document.getElementById('slim-knowledge-body');
|
|
117
|
+
if (!body) return;
|
|
118
|
+
body.textContent = '';
|
|
119
|
+
if (_knActiveTab === 'notes') renderKnowledgeNotesTab(body);
|
|
120
|
+
else if (_knActiveTab === 'kb') renderKnowledgeKbTab(body);
|
|
121
|
+
else renderKnowledgePinnedTab(body);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Tab: Pinned Context ────────────────────────────────────────────
|
|
125
|
+
function renderKnowledgePinnedTab(body) {
|
|
126
|
+
var intro = document.createElement('p');
|
|
127
|
+
intro.textContent = 'Context all agents see, prepended to every prompt as “read first”.';
|
|
128
|
+
body.appendChild(intro);
|
|
129
|
+
|
|
130
|
+
var toolbar = document.createElement('div');
|
|
131
|
+
toolbar.className = 'kn-toolbar';
|
|
132
|
+
var addBtn = document.createElement('button');
|
|
133
|
+
addBtn.className = 'linkpr-chip';
|
|
134
|
+
addBtn.type = 'button';
|
|
135
|
+
addBtn.textContent = '+ Pin content';
|
|
136
|
+
addBtn.addEventListener('click', function() { openSlimPinEditor(null); });
|
|
137
|
+
toolbar.appendChild(addBtn);
|
|
138
|
+
body.appendChild(toolbar);
|
|
139
|
+
|
|
140
|
+
var list = document.createElement('div');
|
|
141
|
+
list.id = 'slim-pinned-list';
|
|
142
|
+
body.appendChild(list);
|
|
143
|
+
renderSlimPinnedList(); // defined in pinned.js; renders into #slim-pinned-list
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Tab: Notes ─────────────────────────────────────────────────────
|
|
147
|
+
function renderKnowledgeNotesTab(body) {
|
|
148
|
+
var intro = document.createElement('p');
|
|
149
|
+
intro.textContent = 'Consolidated team notes (notes.md). Edit and save, or add a note for the next consolidation sweep.';
|
|
150
|
+
body.appendChild(intro);
|
|
151
|
+
|
|
152
|
+
var msg = document.createElement('div');
|
|
153
|
+
msg.id = 'slim-notes-msg';
|
|
154
|
+
msg.className = 'kn-msg';
|
|
155
|
+
body.appendChild(msg);
|
|
156
|
+
|
|
157
|
+
var ta = document.createElement('textarea');
|
|
158
|
+
ta.id = 'slim-notes-content';
|
|
159
|
+
ta.className = 'linkpr-input kn-notes-textarea';
|
|
160
|
+
ta.rows = 12;
|
|
161
|
+
ta.value = 'Loading notes…';
|
|
162
|
+
ta.disabled = true;
|
|
163
|
+
body.appendChild(ta);
|
|
164
|
+
|
|
165
|
+
var saveRow = document.createElement('div');
|
|
166
|
+
saveRow.className = 'kn-toolbar kn-toolbar-end';
|
|
167
|
+
var saveBtn = document.createElement('button');
|
|
168
|
+
saveBtn.className = 'btn-primary';
|
|
169
|
+
saveBtn.type = 'button';
|
|
170
|
+
saveBtn.textContent = 'Save notes.md';
|
|
171
|
+
saveBtn.disabled = true;
|
|
172
|
+
saveBtn.addEventListener('click', saveKnowledgeNotes);
|
|
173
|
+
saveRow.appendChild(saveBtn);
|
|
174
|
+
body.appendChild(saveRow);
|
|
175
|
+
|
|
176
|
+
// Add-a-note (inbox) sub-form.
|
|
177
|
+
var addHead = document.createElement('div');
|
|
178
|
+
addHead.className = 'kn-section-head';
|
|
179
|
+
addHead.textContent = 'Add a note for consolidation';
|
|
180
|
+
body.appendChild(addHead);
|
|
181
|
+
|
|
182
|
+
var titleLabel = document.createElement('label');
|
|
183
|
+
titleLabel.className = 'linkpr-label';
|
|
184
|
+
titleLabel.textContent = 'Title';
|
|
185
|
+
var titleInput = document.createElement('input');
|
|
186
|
+
titleInput.id = 'slim-note-title';
|
|
187
|
+
titleInput.className = 'linkpr-input';
|
|
188
|
+
titleInput.type = 'text';
|
|
189
|
+
titleInput.placeholder = 'Short summary';
|
|
190
|
+
titleLabel.appendChild(titleInput);
|
|
191
|
+
body.appendChild(titleLabel);
|
|
192
|
+
|
|
193
|
+
var whatLabel = document.createElement('label');
|
|
194
|
+
whatLabel.className = 'linkpr-label';
|
|
195
|
+
whatLabel.textContent = 'Note';
|
|
196
|
+
var whatInput = document.createElement('textarea');
|
|
197
|
+
whatInput.id = 'slim-note-what';
|
|
198
|
+
whatInput.className = 'linkpr-input';
|
|
199
|
+
whatInput.rows = 3;
|
|
200
|
+
whatInput.placeholder = 'What did you learn / decide?';
|
|
201
|
+
whatLabel.appendChild(whatInput);
|
|
202
|
+
body.appendChild(whatLabel);
|
|
203
|
+
|
|
204
|
+
var addRow = document.createElement('div');
|
|
205
|
+
addRow.className = 'kn-toolbar kn-toolbar-end';
|
|
206
|
+
var addNoteMsg = document.createElement('span');
|
|
207
|
+
addNoteMsg.id = 'slim-note-msg';
|
|
208
|
+
addNoteMsg.className = 'kn-msg kn-msg-inline';
|
|
209
|
+
addRow.appendChild(addNoteMsg);
|
|
210
|
+
var addNoteBtn = document.createElement('button');
|
|
211
|
+
addNoteBtn.className = 'btn-secondary';
|
|
212
|
+
addNoteBtn.type = 'button';
|
|
213
|
+
addNoteBtn.textContent = 'Add note';
|
|
214
|
+
addNoteBtn.addEventListener('click', submitKnowledgeNote);
|
|
215
|
+
addRow.appendChild(addNoteBtn);
|
|
216
|
+
body.appendChild(addRow);
|
|
217
|
+
|
|
218
|
+
// Load notes.md into the textarea.
|
|
219
|
+
fetch('/api/notes-full', { headers: { 'Accept': 'text/plain' } })
|
|
220
|
+
.then(function(r) { return r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status)); })
|
|
221
|
+
.then(function(text) {
|
|
222
|
+
if (document.getElementById('slim-notes-content') !== ta) return; // tab switched away
|
|
223
|
+
ta.value = (text === 'No notes file found.') ? '' : text;
|
|
224
|
+
ta.disabled = false;
|
|
225
|
+
saveBtn.disabled = false;
|
|
226
|
+
_knowledgeCounts.notes = _countNotesEntries(text);
|
|
227
|
+
renderKnowledgeTile();
|
|
228
|
+
})
|
|
229
|
+
.catch(function(e) {
|
|
230
|
+
ta.value = '';
|
|
231
|
+
ta.disabled = false;
|
|
232
|
+
saveBtn.disabled = false;
|
|
233
|
+
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Could not load notes: ' + (e.message || e); }
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function saveKnowledgeNotes() {
|
|
238
|
+
var ta = document.getElementById('slim-notes-content');
|
|
239
|
+
var msg = document.getElementById('slim-notes-msg');
|
|
240
|
+
if (!ta) return;
|
|
241
|
+
if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Saving…'; }
|
|
242
|
+
try {
|
|
243
|
+
var res = await fetch('/api/notes-save', {
|
|
244
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
245
|
+
body: JSON.stringify({ content: ta.value, file: 'notes.md' }),
|
|
246
|
+
});
|
|
247
|
+
var d = await res.json().catch(function() { return {}; });
|
|
248
|
+
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
249
|
+
if (msg) { msg.style.color = 'var(--green)'; msg.textContent = 'Saved.'; }
|
|
250
|
+
_knowledgeCounts.notes = _countNotesEntries(ta.value);
|
|
251
|
+
renderKnowledgeTile();
|
|
252
|
+
} catch (e) {
|
|
253
|
+
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function submitKnowledgeNote() {
|
|
258
|
+
var titleEl = document.getElementById('slim-note-title');
|
|
259
|
+
var whatEl = document.getElementById('slim-note-what');
|
|
260
|
+
var msg = document.getElementById('slim-note-msg');
|
|
261
|
+
var title = titleEl ? (titleEl.value || '').trim() : '';
|
|
262
|
+
var what = whatEl ? (whatEl.value || '').trim() : '';
|
|
263
|
+
if (!title) {
|
|
264
|
+
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Title is required.'; }
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Adding…'; }
|
|
268
|
+
try {
|
|
269
|
+
var res = await fetch('/api/notes', {
|
|
270
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
271
|
+
body: JSON.stringify({ title: title, what: what }),
|
|
272
|
+
});
|
|
273
|
+
var d = await res.json().catch(function() { return {}; });
|
|
274
|
+
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
275
|
+
if (titleEl) titleEl.value = '';
|
|
276
|
+
if (whatEl) whatEl.value = '';
|
|
277
|
+
if (msg) { msg.style.color = 'var(--green)'; msg.textContent = 'Note added for consolidation.'; }
|
|
278
|
+
} catch (e) {
|
|
279
|
+
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Tab: KB ────────────────────────────────────────────────────────
|
|
284
|
+
function renderKnowledgeKbTab(body) {
|
|
285
|
+
var intro = document.createElement('p');
|
|
286
|
+
intro.textContent = 'Knowledge base — categorized entries written by consolidation. Click an entry to read it.';
|
|
287
|
+
body.appendChild(intro);
|
|
288
|
+
|
|
289
|
+
var toolbar = document.createElement('div');
|
|
290
|
+
toolbar.className = 'kn-toolbar';
|
|
291
|
+
var newBtn = document.createElement('button');
|
|
292
|
+
newBtn.className = 'linkpr-chip';
|
|
293
|
+
newBtn.type = 'button';
|
|
294
|
+
newBtn.textContent = '+ New entry';
|
|
295
|
+
newBtn.addEventListener('click', function() { toggleKbCreateForm(body); });
|
|
296
|
+
toolbar.appendChild(newBtn);
|
|
297
|
+
body.appendChild(toolbar);
|
|
298
|
+
|
|
299
|
+
var listMsg = document.createElement('div');
|
|
300
|
+
listMsg.id = 'slim-kb-list-msg';
|
|
301
|
+
listMsg.className = 'kn-msg';
|
|
302
|
+
body.appendChild(listMsg);
|
|
303
|
+
|
|
304
|
+
var listWrap = document.createElement('div');
|
|
305
|
+
listWrap.id = 'slim-kb-list';
|
|
306
|
+
body.appendChild(listWrap);
|
|
307
|
+
|
|
308
|
+
if (_kbData) renderKbList(listWrap);
|
|
309
|
+
else {
|
|
310
|
+
var loading = document.createElement('div');
|
|
311
|
+
loading.className = 'tile-empty';
|
|
312
|
+
loading.textContent = 'Loading knowledge base…';
|
|
313
|
+
listWrap.appendChild(loading);
|
|
314
|
+
}
|
|
315
|
+
// Always refresh list + pins on open so newly-swept entries appear.
|
|
316
|
+
Promise.all([
|
|
317
|
+
fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } }).then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }),
|
|
318
|
+
loadKbPins(),
|
|
319
|
+
]).then(function(results) {
|
|
320
|
+
if (results[0]) {
|
|
321
|
+
_kbData = results[0];
|
|
322
|
+
_knowledgeCounts.kb = _countKbEntries(_kbData);
|
|
323
|
+
renderKnowledgeTile();
|
|
324
|
+
}
|
|
325
|
+
if (_knActiveTab === 'kb') {
|
|
326
|
+
var lw = document.getElementById('slim-kb-list');
|
|
327
|
+
if (lw) renderKbList(lw);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function _kbAllItems() {
|
|
333
|
+
var items = [];
|
|
334
|
+
if (!_kbData) return items;
|
|
335
|
+
Object.keys(_kbData).forEach(function(cat) {
|
|
336
|
+
if (!Array.isArray(_kbData[cat])) return;
|
|
337
|
+
_kbData[cat].forEach(function(item) {
|
|
338
|
+
items.push({ file: item.file, category: cat, title: item.title, agent: item.agent, date: item.date, sortTs: item.sortTs, preview: item.preview });
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
return items;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function renderKbList(listWrap) {
|
|
345
|
+
listWrap.textContent = '';
|
|
346
|
+
var items = _kbAllItems();
|
|
347
|
+
if (!items.length) {
|
|
348
|
+
var empty = document.createElement('div');
|
|
349
|
+
empty.className = 'tile-empty';
|
|
350
|
+
empty.textContent = 'No knowledge entries yet. Notes are classified here after consolidation.';
|
|
351
|
+
listWrap.appendChild(empty);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
// Pinned first, then newest.
|
|
355
|
+
items.sort(function(a, b) {
|
|
356
|
+
var ap = isKbPinned(kbPinKey(a.category, a.file));
|
|
357
|
+
var bp = isKbPinned(kbPinKey(b.category, b.file));
|
|
358
|
+
if (ap !== bp) return ap ? -1 : 1;
|
|
359
|
+
return (b.sortTs || 0) - (a.sortTs || 0) || (b.date || '').localeCompare(a.date || '');
|
|
360
|
+
});
|
|
361
|
+
var shown = items.slice(0, KB_LIST_CAP);
|
|
362
|
+
shown.forEach(function(item) {
|
|
363
|
+
listWrap.appendChild(buildKbRow(item));
|
|
364
|
+
});
|
|
365
|
+
if (items.length > shown.length) {
|
|
366
|
+
var more = document.createElement('div');
|
|
367
|
+
more.className = 'tile-empty';
|
|
368
|
+
more.textContent = 'Showing ' + shown.length + ' of ' + items.length + ' entries (newest + pinned first).';
|
|
369
|
+
listWrap.appendChild(more);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function buildKbRow(item) {
|
|
374
|
+
var key = kbPinKey(item.category, item.file);
|
|
375
|
+
var pinned = isKbPinned(key);
|
|
376
|
+
var row = document.createElement('div');
|
|
377
|
+
row.className = 'kb-row' + (pinned ? ' kb-row-pinned' : '');
|
|
378
|
+
|
|
379
|
+
var top = document.createElement('div');
|
|
380
|
+
top.className = 'kb-row-top';
|
|
381
|
+
|
|
382
|
+
var pinBtn = document.createElement('button');
|
|
383
|
+
pinBtn.className = 'kb-pin-btn' + (pinned ? ' on' : '');
|
|
384
|
+
pinBtn.type = 'button';
|
|
385
|
+
pinBtn.title = pinned ? 'Unpin from KB' : 'Pin in KB';
|
|
386
|
+
pinBtn.textContent = pinned ? '★' : '☆';
|
|
387
|
+
pinBtn.addEventListener('click', function(ev) { ev.stopPropagation(); toggleKbPin(key); });
|
|
388
|
+
top.appendChild(pinBtn);
|
|
389
|
+
|
|
390
|
+
var title = document.createElement('span');
|
|
391
|
+
title.className = 'kb-row-title';
|
|
392
|
+
title.textContent = item.title || item.file || '(untitled)';
|
|
393
|
+
title.title = item.title || '';
|
|
394
|
+
top.appendChild(title);
|
|
395
|
+
row.appendChild(top);
|
|
396
|
+
|
|
397
|
+
var meta = document.createElement('div');
|
|
398
|
+
meta.className = 'kb-row-meta';
|
|
399
|
+
meta.textContent = [KB_CAT_LABELS[item.category] || item.category, item.agent || null, item.date || null]
|
|
400
|
+
.filter(Boolean).join(' · ');
|
|
401
|
+
row.appendChild(meta);
|
|
402
|
+
|
|
403
|
+
if (item.preview) {
|
|
404
|
+
var prev = document.createElement('div');
|
|
405
|
+
prev.className = 'kb-row-preview';
|
|
406
|
+
var t = String(item.preview);
|
|
407
|
+
prev.textContent = t.length > 200 ? t.slice(0, 200) + '…' : t;
|
|
408
|
+
row.appendChild(prev);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
row.addEventListener('click', function() { openKbItem(item.category, item.file); });
|
|
412
|
+
return row;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function toggleKbPin(key) {
|
|
416
|
+
var listMsg = document.getElementById('slim-kb-list-msg');
|
|
417
|
+
if (listMsg) { listMsg.style.color = ''; listMsg.textContent = ''; }
|
|
418
|
+
try {
|
|
419
|
+
var res = await fetch('/api/kb-pins/toggle', {
|
|
420
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
421
|
+
body: JSON.stringify({ key: key }),
|
|
422
|
+
});
|
|
423
|
+
var d = await res.json().catch(function() { return {}; });
|
|
424
|
+
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
425
|
+
if (d.pinned) { if (_kbPins.indexOf(key) === -1) _kbPins.push(key); }
|
|
426
|
+
else { _kbPins = _kbPins.filter(function(k) { return k !== key; }); }
|
|
427
|
+
var lw = document.getElementById('slim-kb-list');
|
|
428
|
+
if (lw) renderKbList(lw);
|
|
429
|
+
} catch (e) {
|
|
430
|
+
if (listMsg) { listMsg.style.color = 'var(--red)'; listMsg.textContent = 'Pin toggle failed: ' + (e && e.message ? e.message : e); }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function openKbItem(category, file) {
|
|
435
|
+
var body = document.getElementById('slim-knowledge-body');
|
|
436
|
+
if (!body) return;
|
|
437
|
+
body.textContent = '';
|
|
438
|
+
var backRow = document.createElement('div');
|
|
439
|
+
backRow.className = 'kn-toolbar';
|
|
440
|
+
var backBtn = document.createElement('button');
|
|
441
|
+
backBtn.className = 'btn-secondary';
|
|
442
|
+
backBtn.type = 'button';
|
|
443
|
+
backBtn.textContent = '← Back to list';
|
|
444
|
+
backBtn.addEventListener('click', function() { renderKnowledgeTab(); });
|
|
445
|
+
backRow.appendChild(backBtn);
|
|
446
|
+
body.appendChild(backRow);
|
|
447
|
+
|
|
448
|
+
var head = document.createElement('div');
|
|
449
|
+
head.className = 'kn-section-head';
|
|
450
|
+
head.textContent = file;
|
|
451
|
+
body.appendChild(head);
|
|
452
|
+
|
|
453
|
+
var pre = document.createElement('pre');
|
|
454
|
+
pre.className = 'kb-entry-content';
|
|
455
|
+
pre.textContent = 'Loading…';
|
|
456
|
+
body.appendChild(pre);
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
var res = await fetch('/api/knowledge/' + encodeURIComponent(category) + '/' + encodeURIComponent(file));
|
|
460
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
461
|
+
var text = await res.text();
|
|
462
|
+
pre.textContent = text.replace(/^---[\s\S]*?---\n*/m, '');
|
|
463
|
+
} catch (e) {
|
|
464
|
+
pre.textContent = 'Failed to load entry: ' + (e && e.message ? e.message : e);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function toggleKbCreateForm(body) {
|
|
469
|
+
var existing = document.getElementById('slim-kb-create');
|
|
470
|
+
if (existing) { existing.parentNode.removeChild(existing); return; }
|
|
471
|
+
var form = document.createElement('div');
|
|
472
|
+
form.id = 'slim-kb-create';
|
|
473
|
+
form.className = 'kn-create-form';
|
|
474
|
+
|
|
475
|
+
var catLabel = document.createElement('label');
|
|
476
|
+
catLabel.className = 'linkpr-label';
|
|
477
|
+
catLabel.textContent = 'Category';
|
|
478
|
+
var catSel = document.createElement('select');
|
|
479
|
+
catSel.id = 'slim-kb-category';
|
|
480
|
+
catSel.className = 'linkpr-input';
|
|
481
|
+
KB_CREATE_CATS.forEach(function(c) {
|
|
482
|
+
var opt = document.createElement('option');
|
|
483
|
+
opt.value = c;
|
|
484
|
+
opt.textContent = KB_CAT_LABELS[c] || c;
|
|
485
|
+
catSel.appendChild(opt);
|
|
486
|
+
});
|
|
487
|
+
catLabel.appendChild(catSel);
|
|
488
|
+
form.appendChild(catLabel);
|
|
489
|
+
|
|
490
|
+
var titleLabel = document.createElement('label');
|
|
491
|
+
titleLabel.className = 'linkpr-label';
|
|
492
|
+
titleLabel.textContent = 'Title';
|
|
493
|
+
var titleInput = document.createElement('input');
|
|
494
|
+
titleInput.id = 'slim-kb-title';
|
|
495
|
+
titleInput.className = 'linkpr-input';
|
|
496
|
+
titleInput.type = 'text';
|
|
497
|
+
titleInput.placeholder = 'Entry title';
|
|
498
|
+
titleLabel.appendChild(titleInput);
|
|
499
|
+
form.appendChild(titleLabel);
|
|
500
|
+
|
|
501
|
+
var contentLabel = document.createElement('label');
|
|
502
|
+
contentLabel.className = 'linkpr-label';
|
|
503
|
+
contentLabel.textContent = 'Content';
|
|
504
|
+
var contentInput = document.createElement('textarea');
|
|
505
|
+
contentInput.id = 'slim-kb-content';
|
|
506
|
+
contentInput.className = 'linkpr-input';
|
|
507
|
+
contentInput.rows = 6;
|
|
508
|
+
contentInput.placeholder = 'Write your knowledge entry…';
|
|
509
|
+
contentLabel.appendChild(contentInput);
|
|
510
|
+
form.appendChild(contentLabel);
|
|
511
|
+
|
|
512
|
+
var row = document.createElement('div');
|
|
513
|
+
row.className = 'kn-toolbar kn-toolbar-end';
|
|
514
|
+
var msg = document.createElement('span');
|
|
515
|
+
msg.id = 'slim-kb-create-msg';
|
|
516
|
+
msg.className = 'kn-msg kn-msg-inline';
|
|
517
|
+
row.appendChild(msg);
|
|
518
|
+
var saveBtn = document.createElement('button');
|
|
519
|
+
saveBtn.className = 'btn-primary';
|
|
520
|
+
saveBtn.type = 'button';
|
|
521
|
+
saveBtn.textContent = 'Save entry';
|
|
522
|
+
saveBtn.addEventListener('click', submitKbEntry);
|
|
523
|
+
row.appendChild(saveBtn);
|
|
524
|
+
form.appendChild(row);
|
|
525
|
+
|
|
526
|
+
// Insert the form directly after the toolbar (before the list).
|
|
527
|
+
var listWrap = document.getElementById('slim-kb-list');
|
|
528
|
+
if (listWrap && listWrap.parentNode) listWrap.parentNode.insertBefore(form, listWrap);
|
|
529
|
+
else body.appendChild(form);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function submitKbEntry() {
|
|
533
|
+
var catEl = document.getElementById('slim-kb-category');
|
|
534
|
+
var titleEl = document.getElementById('slim-kb-title');
|
|
535
|
+
var contentEl = document.getElementById('slim-kb-content');
|
|
536
|
+
var msg = document.getElementById('slim-kb-create-msg');
|
|
537
|
+
var category = catEl ? catEl.value : '';
|
|
538
|
+
var title = titleEl ? (titleEl.value || '').trim() : '';
|
|
539
|
+
var content = contentEl ? contentEl.value : '';
|
|
540
|
+
if (!title || !content.trim()) {
|
|
541
|
+
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Title and content are required.'; }
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Saving…'; }
|
|
545
|
+
try {
|
|
546
|
+
var res = await fetch('/api/knowledge', {
|
|
547
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
548
|
+
body: JSON.stringify({ category: category, title: title, content: content }),
|
|
549
|
+
});
|
|
550
|
+
var d = await res.json().catch(function() { return {}; });
|
|
551
|
+
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
552
|
+
// Refresh the list + count, then re-render the KB tab.
|
|
553
|
+
var kres = await fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } });
|
|
554
|
+
if (kres.ok) { _kbData = await kres.json(); _knowledgeCounts.kb = _countKbEntries(_kbData); renderKnowledgeTile(); }
|
|
555
|
+
if (_knActiveTab === 'kb') renderKnowledgeTab();
|
|
556
|
+
} catch (e) {
|
|
557
|
+
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ── Wiring ─────────────────────────────────────────────────────────
|
|
562
|
+
bindModalClose('slim-knowledge-modal', 'slim-knowledge-close');
|
|
563
|
+
(function bindKnowledgeUi() {
|
|
564
|
+
var tabBar = document.getElementById('slim-kn-tabs');
|
|
565
|
+
if (tabBar) {
|
|
566
|
+
tabBar.addEventListener('click', function(ev) {
|
|
567
|
+
var btn = ev.target && ev.target.closest ? ev.target.closest('.kn-tab') : null;
|
|
568
|
+
if (!btn) return;
|
|
569
|
+
var tab = btn.getAttribute('data-kn-tab');
|
|
570
|
+
if (tab) setKnowledgeTab(tab);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
// Populate the notes + KB counts shortly after first paint so the tile
|
|
574
|
+
// detail reads "N pinned · M notes · K KB" without waiting for a modal open.
|
|
575
|
+
setTimeout(loadKnowledgeCounts, 1500);
|
|
576
|
+
})();
|