clay-server 2.14.0-beta.9 → 2.14.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/lib/crisis-safety.js +60 -0
- package/lib/mates.js +3 -0
- package/lib/project.js +27 -4
- package/lib/public/app.js +80 -7
- package/lib/public/css/input.css +1 -1
- package/lib/public/css/mates.css +468 -202
- package/lib/public/css/messages.css +1 -1
- package/lib/public/index.html +43 -28
- package/lib/public/modules/input.js +1 -1
- package/lib/public/modules/mate-knowledge.js +390 -75
- package/lib/public/modules/mate-sidebar.js +100 -2
- package/lib/public/modules/sidebar.js +6 -0
- package/lib/public/modules/tools.js +106 -65
- package/lib/sdk-bridge.js +4 -7
- package/package.json +1 -1
|
@@ -1,59 +1,89 @@
|
|
|
1
1
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
2
|
import { hideNotes } from './sticky-notes.js';
|
|
3
|
+
import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
|
|
3
4
|
|
|
4
5
|
var getMateWs = null;
|
|
5
6
|
var containerEl = null;
|
|
6
|
-
var
|
|
7
|
-
var closeBtn = null;
|
|
7
|
+
var filesEl = null;
|
|
8
8
|
var sidebarBtn = null;
|
|
9
9
|
var countBadge = null;
|
|
10
10
|
var visible = false;
|
|
11
11
|
var cachedFiles = [];
|
|
12
12
|
|
|
13
|
+
// Sidebar panels
|
|
14
|
+
var conversationsPanel = null;
|
|
15
|
+
var knowledgePanel = null;
|
|
16
|
+
var knowledgeBackBtn = null;
|
|
17
|
+
var knowledgeAddSidebarBtn = null;
|
|
18
|
+
|
|
13
19
|
// Editor elements
|
|
14
|
-
var
|
|
20
|
+
var activeNameEl = null;
|
|
15
21
|
var editorNameEl = null;
|
|
16
22
|
var editorContentEl = null;
|
|
17
23
|
var editorSaveBtn = null;
|
|
18
24
|
var editorDeleteBtn = null;
|
|
19
|
-
var
|
|
20
|
-
var
|
|
25
|
+
var editorPreviewEl = null;
|
|
26
|
+
var editorHighlightEl = null;
|
|
27
|
+
var editorHighlightPre = null;
|
|
28
|
+
var previewTimer = null;
|
|
29
|
+
var editorExtEl = null;
|
|
30
|
+
var nameGroupEl = null;
|
|
21
31
|
var editingFile = null;
|
|
32
|
+
var dirty = false;
|
|
22
33
|
|
|
23
34
|
export function initMateKnowledge(mateWsGetter) {
|
|
24
35
|
getMateWs = mateWsGetter;
|
|
25
36
|
containerEl = document.getElementById("mate-knowledge-container");
|
|
26
|
-
|
|
27
|
-
closeBtn = document.getElementById("mate-knowledge-close-btn");
|
|
37
|
+
filesEl = document.getElementById("mate-knowledge-files");
|
|
28
38
|
sidebarBtn = document.getElementById("mate-knowledge-btn");
|
|
29
39
|
countBadge = document.getElementById("mate-knowledge-count");
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
// Sidebar panels
|
|
42
|
+
conversationsPanel = document.getElementById("mate-sidebar-conversations");
|
|
43
|
+
knowledgePanel = document.getElementById("mate-sidebar-knowledge");
|
|
44
|
+
knowledgeBackBtn = document.getElementById("mate-knowledge-back-btn");
|
|
45
|
+
knowledgeAddSidebarBtn = document.getElementById("mate-knowledge-add-sidebar-btn");
|
|
46
|
+
|
|
47
|
+
// Editor
|
|
48
|
+
activeNameEl = document.getElementById("mate-knowledge-active-name");
|
|
32
49
|
editorNameEl = document.getElementById("mate-knowledge-editor-name");
|
|
33
50
|
editorContentEl = document.getElementById("mate-knowledge-editor-content");
|
|
34
51
|
editorSaveBtn = document.getElementById("mate-knowledge-editor-save");
|
|
35
52
|
editorDeleteBtn = document.getElementById("mate-knowledge-editor-delete");
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
editorPreviewEl = document.getElementById("mate-knowledge-editor-preview");
|
|
54
|
+
editorHighlightEl = document.getElementById("mate-knowledge-editor-highlight");
|
|
55
|
+
editorHighlightPre = editorHighlightEl ? editorHighlightEl.parentElement : null;
|
|
56
|
+
editorExtEl = document.getElementById("mate-knowledge-editor-ext");
|
|
57
|
+
nameGroupEl = document.getElementById("mate-knowledge-name-group");
|
|
38
58
|
|
|
39
59
|
if (sidebarBtn) {
|
|
40
60
|
sidebarBtn.addEventListener("click", function () {
|
|
41
|
-
if (visible) {
|
|
42
|
-
hideKnowledge();
|
|
43
|
-
} else {
|
|
44
|
-
showKnowledge();
|
|
45
|
-
}
|
|
61
|
+
if (visible) { hideKnowledge(); } else { showKnowledge(); }
|
|
46
62
|
});
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
if (knowledgeBackBtn) {
|
|
66
|
+
knowledgeBackBtn.addEventListener("click", hideKnowledge);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var closeBtn = document.getElementById("mate-knowledge-close-btn");
|
|
49
70
|
if (closeBtn) {
|
|
50
|
-
closeBtn.addEventListener("click",
|
|
71
|
+
closeBtn.addEventListener("click", function () {
|
|
72
|
+
// Close editor, keep sidebar file list open
|
|
73
|
+
if (containerEl) containerEl.classList.add("hidden");
|
|
74
|
+
editingFile = null;
|
|
75
|
+
renderFileList();
|
|
76
|
+
});
|
|
51
77
|
}
|
|
52
78
|
|
|
53
|
-
if (
|
|
54
|
-
|
|
79
|
+
if (knowledgeAddSidebarBtn) {
|
|
80
|
+
knowledgeAddSidebarBtn.addEventListener("click", function () {
|
|
81
|
+
selectFile(null, "");
|
|
82
|
+
});
|
|
55
83
|
}
|
|
56
84
|
|
|
85
|
+
if (editorSaveBtn) editorSaveBtn.addEventListener("click", saveKnowledge);
|
|
86
|
+
|
|
57
87
|
if (editorDeleteBtn) {
|
|
58
88
|
editorDeleteBtn.addEventListener("click", function () {
|
|
59
89
|
if (editingFile) {
|
|
@@ -61,45 +91,74 @@ export function initMateKnowledge(mateWsGetter) {
|
|
|
61
91
|
if (ws && ws.readyState === 1) {
|
|
62
92
|
ws.send(JSON.stringify({ type: "knowledge_delete", name: editingFile }));
|
|
63
93
|
}
|
|
64
|
-
|
|
94
|
+
selectFile(null, "");
|
|
65
95
|
}
|
|
66
96
|
});
|
|
67
97
|
}
|
|
68
98
|
|
|
69
|
-
if (editorCloseBtn) {
|
|
70
|
-
editorCloseBtn.addEventListener("click", closeEditor);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (editorBackdrop) {
|
|
74
|
-
editorBackdrop.addEventListener("click", closeEditor);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
99
|
// Stop keyboard events from leaking
|
|
78
|
-
var stopProp = function (e) {
|
|
100
|
+
var stopProp = function (e) {
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
};
|
|
103
|
+
var editorKeydown = function (e) {
|
|
104
|
+
e.stopPropagation();
|
|
105
|
+
// Keep Cmd+Z / Cmd+Shift+Z inside the textarea only
|
|
106
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
|
|
107
|
+
e.stopImmediatePropagation();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
79
110
|
if (editorNameEl) {
|
|
80
111
|
editorNameEl.addEventListener("keydown", stopProp);
|
|
81
112
|
editorNameEl.addEventListener("keyup", stopProp);
|
|
82
113
|
editorNameEl.addEventListener("keypress", stopProp);
|
|
83
114
|
}
|
|
84
115
|
if (editorContentEl) {
|
|
85
|
-
editorContentEl.addEventListener("keydown",
|
|
116
|
+
editorContentEl.addEventListener("keydown", editorKeydown);
|
|
86
117
|
editorContentEl.addEventListener("keyup", stopProp);
|
|
87
118
|
editorContentEl.addEventListener("keypress", stopProp);
|
|
119
|
+
editorContentEl.addEventListener("input", function () {
|
|
120
|
+
dirty = true;
|
|
121
|
+
if (editorSaveBtn) editorSaveBtn.disabled = false;
|
|
122
|
+
updateHighlight();
|
|
123
|
+
if (previewTimer) clearTimeout(previewTimer);
|
|
124
|
+
previewTimer = setTimeout(updatePreview, 150);
|
|
125
|
+
});
|
|
126
|
+
editorContentEl.addEventListener("scroll", syncHighlightScroll);
|
|
127
|
+
initFormatPopover(editorContentEl);
|
|
88
128
|
}
|
|
89
129
|
}
|
|
90
130
|
|
|
91
131
|
export function showKnowledge() {
|
|
92
132
|
visible = true;
|
|
93
133
|
hideNotes();
|
|
94
|
-
|
|
134
|
+
|
|
135
|
+
// Toggle sidebar panels: hide conversations, show knowledge file list
|
|
136
|
+
if (conversationsPanel) conversationsPanel.classList.add("hidden");
|
|
137
|
+
if (knowledgePanel) knowledgePanel.classList.remove("hidden");
|
|
95
138
|
if (sidebarBtn) sidebarBtn.classList.add("active");
|
|
139
|
+
|
|
140
|
+
// Don't show editor yet, only when a file is selected
|
|
96
141
|
requestKnowledgeList();
|
|
97
142
|
}
|
|
98
143
|
|
|
99
144
|
export function hideKnowledge() {
|
|
100
145
|
visible = false;
|
|
101
|
-
|
|
146
|
+
|
|
147
|
+
// Toggle sidebar panels: show conversations, hide knowledge file list
|
|
148
|
+
if (conversationsPanel) conversationsPanel.classList.remove("hidden");
|
|
149
|
+
if (knowledgePanel) knowledgePanel.classList.add("hidden");
|
|
102
150
|
if (sidebarBtn) sidebarBtn.classList.remove("active");
|
|
151
|
+
|
|
152
|
+
// Hide editor/preview and reset state
|
|
153
|
+
if (containerEl) containerEl.classList.add("hidden");
|
|
154
|
+
editingFile = null;
|
|
155
|
+
cachedFiles = [];
|
|
156
|
+
|
|
157
|
+
// Reset badge
|
|
158
|
+
if (countBadge) {
|
|
159
|
+
countBadge.textContent = "";
|
|
160
|
+
countBadge.classList.add("hidden");
|
|
161
|
+
}
|
|
103
162
|
}
|
|
104
163
|
|
|
105
164
|
export function isKnowledgeVisible() {
|
|
@@ -116,7 +175,7 @@ export function requestKnowledgeList() {
|
|
|
116
175
|
export function renderKnowledgeList(files) {
|
|
117
176
|
cachedFiles = files || [];
|
|
118
177
|
|
|
119
|
-
// Update
|
|
178
|
+
// Update badge
|
|
120
179
|
if (countBadge) {
|
|
121
180
|
if (cachedFiles.length > 0) {
|
|
122
181
|
countBadge.textContent = String(cachedFiles.length);
|
|
@@ -125,73 +184,169 @@ export function renderKnowledgeList(files) {
|
|
|
125
184
|
countBadge.classList.add("hidden");
|
|
126
185
|
}
|
|
127
186
|
}
|
|
128
|
-
var headerCount = document.getElementById("mate-knowledge-header-count");
|
|
129
|
-
if (headerCount) {
|
|
130
|
-
headerCount.textContent = cachedFiles.length > 0 ? cachedFiles.length + " files" : "";
|
|
131
|
-
}
|
|
132
187
|
|
|
133
|
-
|
|
134
|
-
|
|
188
|
+
renderFileList();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderFileList() {
|
|
192
|
+
if (!filesEl) return;
|
|
193
|
+
filesEl.innerHTML = "";
|
|
135
194
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
195
|
+
if (cachedFiles.length === 0) {
|
|
196
|
+
var empty = document.createElement("div");
|
|
197
|
+
empty.className = "mate-knowledge-empty";
|
|
198
|
+
empty.textContent = "No knowledge files yet";
|
|
199
|
+
filesEl.appendChild(empty);
|
|
200
|
+
}
|
|
142
201
|
|
|
143
202
|
for (var i = 0; i < cachedFiles.length; i++) {
|
|
144
|
-
|
|
203
|
+
filesEl.appendChild(renderFileItem(cachedFiles[i]));
|
|
145
204
|
}
|
|
146
205
|
refreshIcons();
|
|
147
206
|
}
|
|
148
207
|
|
|
149
|
-
function
|
|
150
|
-
var
|
|
151
|
-
|
|
208
|
+
function renderFileItem(file) {
|
|
209
|
+
var item = document.createElement("div");
|
|
210
|
+
item.className = "mate-knowledge-file-item";
|
|
211
|
+
if (editingFile === file.name) item.classList.add("active");
|
|
152
212
|
|
|
153
|
-
var
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
213
|
+
var icon = document.createElement("span");
|
|
214
|
+
icon.className = "mate-knowledge-file-icon";
|
|
215
|
+
icon.innerHTML = iconHtml(file.name.endsWith(".jsonl") ? "database" : "file-text");
|
|
216
|
+
item.appendChild(icon);
|
|
157
217
|
|
|
158
|
-
var
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
tile.appendChild(preview);
|
|
218
|
+
var name = document.createElement("span");
|
|
219
|
+
name.className = "mate-knowledge-file-name";
|
|
220
|
+
var isJsonl = file.name.endsWith(".jsonl");
|
|
221
|
+
name.textContent = file.name.replace(/\.(md|jsonl)$/, "");
|
|
222
|
+
item.appendChild(name);
|
|
164
223
|
|
|
165
|
-
|
|
224
|
+
if (isJsonl) {
|
|
225
|
+
var badge = document.createElement("span");
|
|
226
|
+
badge.className = "mate-knowledge-file-badge";
|
|
227
|
+
badge.textContent = "data";
|
|
228
|
+
item.appendChild(badge);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
item.addEventListener("click", (function (fname) {
|
|
166
232
|
return function () {
|
|
167
233
|
var ws = getMateWs ? getMateWs() : null;
|
|
168
234
|
if (ws && ws.readyState === 1) {
|
|
169
|
-
ws.send(JSON.stringify({ type: "knowledge_read", name:
|
|
235
|
+
ws.send(JSON.stringify({ type: "knowledge_read", name: fname }));
|
|
170
236
|
}
|
|
171
237
|
};
|
|
172
238
|
})(file.name));
|
|
173
239
|
|
|
174
|
-
return
|
|
240
|
+
return item;
|
|
175
241
|
}
|
|
176
242
|
|
|
177
243
|
export function handleKnowledgeContent(msg) {
|
|
178
|
-
|
|
244
|
+
if (msg.name && msg.name.endsWith(".jsonl")) {
|
|
245
|
+
selectJsonlFile(msg.name, msg.content || "");
|
|
246
|
+
} else {
|
|
247
|
+
selectFile(msg.name, msg.content || "");
|
|
248
|
+
}
|
|
179
249
|
}
|
|
180
250
|
|
|
181
|
-
function
|
|
182
|
-
|
|
251
|
+
function selectJsonlFile(fileName, content) {
|
|
252
|
+
editingFile = fileName;
|
|
253
|
+
dirty = false;
|
|
254
|
+
|
|
255
|
+
if (containerEl) containerEl.classList.remove("hidden");
|
|
256
|
+
|
|
257
|
+
// Show name, hide name input group
|
|
258
|
+
if (activeNameEl) { activeNameEl.textContent = fileName.replace(/\.jsonl$/, ""); activeNameEl.classList.remove("hidden"); }
|
|
259
|
+
if (nameGroupEl) nameGroupEl.classList.add("hidden");
|
|
260
|
+
|
|
261
|
+
// Hide editor pane and controls
|
|
262
|
+
if (editorContentEl) editorContentEl.value = "";
|
|
263
|
+
if (editorSaveBtn) editorSaveBtn.style.display = "none";
|
|
264
|
+
if (editorDeleteBtn) editorDeleteBtn.style.display = "";
|
|
265
|
+
if (editorHighlightPre) editorHighlightPre.style.display = "none";
|
|
266
|
+
if (editorContentEl) editorContentEl.style.display = "none";
|
|
267
|
+
|
|
268
|
+
// Build table from JSONL
|
|
269
|
+
if (editorPreviewEl) {
|
|
270
|
+
var lines = content.trim().split("\n").filter(function (l) { return l.trim(); });
|
|
271
|
+
if (lines.length === 0) {
|
|
272
|
+
editorPreviewEl.innerHTML = "<p style=\"opacity:0.5\">No data entries yet</p>";
|
|
273
|
+
} else {
|
|
274
|
+
var rows = [];
|
|
275
|
+
var allKeys = [];
|
|
276
|
+
var keySet = {};
|
|
277
|
+
for (var i = 0; i < lines.length; i++) {
|
|
278
|
+
try {
|
|
279
|
+
var obj = JSON.parse(lines[i]);
|
|
280
|
+
rows.push(obj);
|
|
281
|
+
var keys = Object.keys(obj);
|
|
282
|
+
for (var k = 0; k < keys.length; k++) {
|
|
283
|
+
if (!keySet[keys[k]]) { keySet[keys[k]] = true; allKeys.push(keys[k]); }
|
|
284
|
+
}
|
|
285
|
+
} catch (e) { /* skip malformed lines */ }
|
|
286
|
+
}
|
|
287
|
+
var html = "<table class=\"mate-knowledge-jsonl-table\"><thead><tr>";
|
|
288
|
+
for (var c = 0; c < allKeys.length; c++) {
|
|
289
|
+
html += "<th>" + escapeHtml(allKeys[c]) + "</th>";
|
|
290
|
+
}
|
|
291
|
+
html += "</tr></thead><tbody>";
|
|
292
|
+
for (var r = 0; r < rows.length; r++) {
|
|
293
|
+
html += "<tr>";
|
|
294
|
+
for (var c = 0; c < allKeys.length; c++) {
|
|
295
|
+
var val = rows[r][allKeys[c]];
|
|
296
|
+
var cell = val === undefined ? "" : typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
297
|
+
html += "<td>" + escapeHtml(cell) + "</td>";
|
|
298
|
+
}
|
|
299
|
+
html += "</tr>";
|
|
300
|
+
}
|
|
301
|
+
html += "</tbody></table>";
|
|
302
|
+
editorPreviewEl.innerHTML = html;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
renderFileList();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function escapeHtml(str) {
|
|
310
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function selectFile(fileName, content) {
|
|
183
314
|
editingFile = fileName || null;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
315
|
+
dirty = false;
|
|
316
|
+
|
|
317
|
+
// Restore editor pane (may have been hidden by JSONL viewer)
|
|
318
|
+
if (editorContentEl) editorContentEl.style.display = "";
|
|
319
|
+
if (editorHighlightPre) editorHighlightPre.style.display = "";
|
|
320
|
+
if (editorSaveBtn) editorSaveBtn.style.display = "";
|
|
321
|
+
|
|
322
|
+
// Show editor when a file is selected or new file created
|
|
323
|
+
if (containerEl) containerEl.classList.remove("hidden");
|
|
324
|
+
|
|
325
|
+
// Update active name / name input group
|
|
326
|
+
if (fileName) {
|
|
327
|
+
if (activeNameEl) { activeNameEl.textContent = fileName.replace(/\.md$/, ""); activeNameEl.classList.remove("hidden"); }
|
|
328
|
+
if (nameGroupEl) nameGroupEl.classList.add("hidden");
|
|
329
|
+
} else {
|
|
330
|
+
if (activeNameEl) activeNameEl.classList.add("hidden");
|
|
331
|
+
if (nameGroupEl) nameGroupEl.classList.remove("hidden");
|
|
332
|
+
if (editorNameEl) editorNameEl.value = "";
|
|
187
333
|
}
|
|
334
|
+
|
|
188
335
|
if (editorContentEl) {
|
|
189
336
|
editorContentEl.value = content || "";
|
|
337
|
+
editorContentEl.placeholder = fileName ? "" : "Start writing...";
|
|
190
338
|
}
|
|
191
339
|
if (editorDeleteBtn) {
|
|
192
340
|
editorDeleteBtn.style.display = fileName ? "" : "none";
|
|
193
341
|
}
|
|
194
|
-
|
|
342
|
+
if (editorSaveBtn) {
|
|
343
|
+
editorSaveBtn.disabled = true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
updateHighlight();
|
|
347
|
+
updatePreview();
|
|
348
|
+
renderFileList();
|
|
349
|
+
|
|
195
350
|
if (!fileName && editorNameEl) {
|
|
196
351
|
editorNameEl.focus();
|
|
197
352
|
} else if (editorContentEl) {
|
|
@@ -199,24 +354,184 @@ function openEditor(fileName, content) {
|
|
|
199
354
|
}
|
|
200
355
|
}
|
|
201
356
|
|
|
202
|
-
function
|
|
203
|
-
if (
|
|
204
|
-
|
|
357
|
+
function updateHighlight() {
|
|
358
|
+
if (!editorHighlightEl || !editorContentEl) return;
|
|
359
|
+
var text = editorContentEl.value + "\n";
|
|
360
|
+
// Reset completely: hljs skips elements it already processed
|
|
361
|
+
editorHighlightEl.className = "language-markdown";
|
|
362
|
+
editorHighlightEl.removeAttribute("data-highlighted");
|
|
363
|
+
editorHighlightEl.textContent = text;
|
|
364
|
+
if (window.hljs) {
|
|
365
|
+
window.hljs.highlightElement(editorHighlightEl);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function syncHighlightScroll() {
|
|
370
|
+
if (!editorHighlightPre || !editorContentEl) return;
|
|
371
|
+
editorHighlightPre.scrollTop = editorContentEl.scrollTop;
|
|
372
|
+
editorHighlightPre.scrollLeft = editorContentEl.scrollLeft;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function updatePreview() {
|
|
376
|
+
if (!editorPreviewEl || !editorContentEl) return;
|
|
377
|
+
var text = editorContentEl.value;
|
|
378
|
+
if (!text.trim()) {
|
|
379
|
+
editorPreviewEl.innerHTML = "";
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
editorPreviewEl.innerHTML = renderMarkdown(text);
|
|
383
|
+
highlightCodeBlocks(editorPreviewEl);
|
|
205
384
|
}
|
|
206
385
|
|
|
207
386
|
function saveKnowledge() {
|
|
208
387
|
if (!editorNameEl || !editorContentEl) return;
|
|
209
|
-
var name = editorNameEl.value.trim();
|
|
388
|
+
var name = (editingFile || editorNameEl.value.trim().replace(/\.md$/i, "") + ".md");
|
|
210
389
|
var content = editorContentEl.value;
|
|
211
|
-
if (!name) {
|
|
390
|
+
if (!name || name === ".md") {
|
|
212
391
|
editorNameEl.style.outline = "2px solid var(--error, #ff5555)";
|
|
213
392
|
setTimeout(function () { editorNameEl.style.outline = ""; }, 1500);
|
|
214
393
|
return;
|
|
215
394
|
}
|
|
216
|
-
if (!name.endsWith(".md")) name += ".md";
|
|
217
395
|
var ws = getMateWs ? getMateWs() : null;
|
|
218
396
|
if (ws && ws.readyState === 1) {
|
|
219
397
|
ws.send(JSON.stringify({ type: "knowledge_save", name: name, content: content }));
|
|
220
398
|
}
|
|
221
|
-
|
|
399
|
+
editingFile = name;
|
|
400
|
+
dirty = false;
|
|
401
|
+
if (editorSaveBtn) editorSaveBtn.disabled = true;
|
|
402
|
+
if (activeNameEl) { activeNameEl.textContent = name.replace(/\.md$/, ""); activeNameEl.classList.remove("hidden"); }
|
|
403
|
+
if (nameGroupEl) nameGroupEl.classList.add("hidden");
|
|
404
|
+
if (editorDeleteBtn) editorDeleteBtn.style.display = "";
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// --- Format Popover ---
|
|
408
|
+
|
|
409
|
+
var formatPopover = null;
|
|
410
|
+
var popoverHideTimer = null;
|
|
411
|
+
|
|
412
|
+
var FORMAT_ACTIONS = [
|
|
413
|
+
{ icon: "bold", label: "Bold", wrap: ["**", "**"] },
|
|
414
|
+
{ icon: "italic", label: "Italic", wrap: ["*", "*"] },
|
|
415
|
+
{ icon: "strikethrough", label: "Strikethrough", wrap: ["~~", "~~"] },
|
|
416
|
+
{ icon: "code", label: "Code", wrap: ["`", "`"] },
|
|
417
|
+
{ icon: "link", label: "Link", wrap: ["[", "](url)"] },
|
|
418
|
+
{ icon: "heading-1", label: "Heading", prefix: "# " },
|
|
419
|
+
{ icon: "list", label: "List", prefix: "- " },
|
|
420
|
+
{ icon: "quote", label: "Quote", prefix: "> " },
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
function initFormatPopover(textarea) {
|
|
424
|
+
// Create popover element
|
|
425
|
+
formatPopover = document.createElement("div");
|
|
426
|
+
formatPopover.className = "mate-format-popover";
|
|
427
|
+
formatPopover.style.display = "none";
|
|
428
|
+
|
|
429
|
+
for (var i = 0; i < FORMAT_ACTIONS.length; i++) {
|
|
430
|
+
var action = FORMAT_ACTIONS[i];
|
|
431
|
+
var btn = document.createElement("button");
|
|
432
|
+
btn.className = "mate-format-btn";
|
|
433
|
+
btn.title = action.label;
|
|
434
|
+
btn.innerHTML = iconHtml(action.icon);
|
|
435
|
+
btn.dataset.index = String(i);
|
|
436
|
+
btn.addEventListener("mousedown", function (e) {
|
|
437
|
+
e.preventDefault(); // prevent textarea blur
|
|
438
|
+
var idx = parseInt(this.dataset.index);
|
|
439
|
+
applyFormat(textarea, FORMAT_ACTIONS[idx]);
|
|
440
|
+
});
|
|
441
|
+
formatPopover.appendChild(btn);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
document.body.appendChild(formatPopover);
|
|
445
|
+
refreshIcons(formatPopover);
|
|
446
|
+
|
|
447
|
+
textarea.addEventListener("mouseup", function () {
|
|
448
|
+
setTimeout(function () { checkSelection(textarea); }, 10);
|
|
449
|
+
});
|
|
450
|
+
textarea.addEventListener("keyup", function (e) {
|
|
451
|
+
if (e.shiftKey || e.key === "Shift") {
|
|
452
|
+
checkSelection(textarea);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
textarea.addEventListener("blur", function () {
|
|
457
|
+
popoverHideTimer = setTimeout(hidePopover, 150);
|
|
458
|
+
});
|
|
459
|
+
textarea.addEventListener("focus", function () {
|
|
460
|
+
if (popoverHideTimer) { clearTimeout(popoverHideTimer); popoverHideTimer = null; }
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
document.addEventListener("scroll", hidePopover, true);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function checkSelection(textarea) {
|
|
467
|
+
var start = textarea.selectionStart;
|
|
468
|
+
var end = textarea.selectionEnd;
|
|
469
|
+
if (start === end || !formatPopover) {
|
|
470
|
+
hidePopover();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
showPopover(textarea);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function showPopover(textarea) {
|
|
477
|
+
if (!formatPopover) return;
|
|
478
|
+
|
|
479
|
+
// Position above the textarea selection
|
|
480
|
+
// We approximate position using a mirror div technique
|
|
481
|
+
var rect = textarea.getBoundingClientRect();
|
|
482
|
+
var lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 20;
|
|
483
|
+
var paddingTop = parseInt(getComputedStyle(textarea).paddingTop) || 0;
|
|
484
|
+
var paddingLeft = parseInt(getComputedStyle(textarea).paddingLeft) || 0;
|
|
485
|
+
var fontSize = parseInt(getComputedStyle(textarea).fontSize) || 13;
|
|
486
|
+
|
|
487
|
+
// Get text before selection to calculate approximate position
|
|
488
|
+
var text = textarea.value;
|
|
489
|
+
var start = textarea.selectionStart;
|
|
490
|
+
var textBefore = text.substring(0, start);
|
|
491
|
+
var lines = textBefore.split("\n");
|
|
492
|
+
var currentLine = lines.length - 1;
|
|
493
|
+
var charInLine = lines[lines.length - 1].length;
|
|
494
|
+
|
|
495
|
+
// Approximate character width (monospace)
|
|
496
|
+
var charWidth = fontSize * 0.6;
|
|
497
|
+
|
|
498
|
+
var scrollTop = textarea.scrollTop;
|
|
499
|
+
var x = rect.left + paddingLeft + (charWidth * Math.min(charInLine, 40));
|
|
500
|
+
var y = rect.top + paddingTop + (currentLine * lineHeight) - scrollTop - 8;
|
|
501
|
+
|
|
502
|
+
// Clamp to viewport
|
|
503
|
+
var popoverWidth = 280;
|
|
504
|
+
if (x + popoverWidth / 2 > window.innerWidth) x = window.innerWidth - popoverWidth / 2 - 8;
|
|
505
|
+
if (x - popoverWidth / 2 < 8) x = popoverWidth / 2 + 8;
|
|
506
|
+
if (y < 40) y = rect.top + paddingTop + ((currentLine + 1) * lineHeight) - scrollTop + 28;
|
|
507
|
+
|
|
508
|
+
formatPopover.style.display = "flex";
|
|
509
|
+
formatPopover.style.left = x + "px";
|
|
510
|
+
formatPopover.style.top = y + "px";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function hidePopover() {
|
|
514
|
+
if (formatPopover) formatPopover.style.display = "none";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function applyFormat(textarea, action) {
|
|
518
|
+
var start = textarea.selectionStart;
|
|
519
|
+
var end = textarea.selectionEnd;
|
|
520
|
+
var selected = textarea.value.substring(start, end);
|
|
521
|
+
|
|
522
|
+
var replacement;
|
|
523
|
+
|
|
524
|
+
if (action.wrap) {
|
|
525
|
+
replacement = action.wrap[0] + selected + action.wrap[1];
|
|
526
|
+
} else if (action.prefix) {
|
|
527
|
+
var lines = selected.split("\n");
|
|
528
|
+
replacement = lines.map(function (line) { return action.prefix + line; }).join("\n");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Use execCommand to preserve native undo/redo stack
|
|
532
|
+
textarea.focus();
|
|
533
|
+
textarea.setSelectionRange(start, end);
|
|
534
|
+
document.execCommand("insertText", false, replacement);
|
|
535
|
+
|
|
536
|
+
hidePopover();
|
|
222
537
|
}
|