living-ai-documentation 2.0.0 → 2.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/dist/src/frontend/agents.js +288 -0
- package/dist/src/frontend/config.js +3 -2
- package/dist/src/frontend/diagram/color-picker.js +72 -0
- package/dist/src/frontend/diagram/defaults-modal.js +302 -0
- package/dist/src/frontend/diagram/edge-panel.js +110 -11
- package/dist/src/frontend/diagram/main.js +5 -8
- package/dist/src/frontend/diagram/network.js +16 -7
- package/dist/src/frontend/diagram/node-panel.js +104 -2
- package/dist/src/frontend/diagram.html +52 -29
- package/dist/src/frontend/i18n/en.json +43 -1
- package/dist/src/frontend/i18n/fr.json +43 -1
- package/dist/src/frontend/index.html +156 -33
- package/dist/src/frontend/workspace/app.js +439 -67
- package/dist/src/frontend/workspace/app.js.map +1 -1
- package/dist/src/frontend/workspace/app.ts +582 -84
- package/dist/src/frontend/workspace/index.html +232 -82
- package/dist/src/frontend/workspace/persistence.js +61 -0
- package/dist/src/frontend/workspace/persistence.js.map +1 -1
- package/dist/src/frontend/workspace/persistence.ts +111 -0
- package/dist/src/frontend/workspace/styles.css +293 -34
- package/dist/src/lib/config.d.ts +23 -0
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +16 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/routes/config.d.ts.map +1 -1
- package/dist/src/routes/config.js +13 -0
- package/dist/src/routes/config.js.map +1 -1
- package/dist/src/routes/workspace.d.ts.map +1 -1
- package/dist/src/routes/workspace.js +528 -6
- package/dist/src/routes/workspace.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadWorkspaceState, saveWorkspaceState, } from "./persistence.js";
|
|
1
|
+
import { loadWorkspaceState, saveWorkspaceState, testLlmConnection, listLlmModels, runAgentPrompt, } from "./persistence.js";
|
|
2
2
|
const LEAF_WIDTH = 164;
|
|
3
3
|
const LEAF_HEIGHT = 64;
|
|
4
4
|
const ROOT_RADIUS = 132;
|
|
@@ -6,10 +6,10 @@ const PROVIDER_RADIUS = 78;
|
|
|
6
6
|
const MIN_POLYGON_SIDES = 6;
|
|
7
7
|
const GRID_MINOR = 24;
|
|
8
8
|
const GRID_MAJOR = 96;
|
|
9
|
-
const PANEL_MIN_WIDTH =
|
|
10
|
-
const PANEL_MAX_WIDTH =
|
|
11
|
-
const PANEL_VIEWPORT_RATIO = 1 /
|
|
12
|
-
const PANEL_HEIGHT =
|
|
9
|
+
const PANEL_MIN_WIDTH = 540;
|
|
10
|
+
const PANEL_MAX_WIDTH = 780;
|
|
11
|
+
const PANEL_VIEWPORT_RATIO = 1 / 2;
|
|
12
|
+
const PANEL_HEIGHT = 9999;
|
|
13
13
|
const PANEL_GAP = 44;
|
|
14
14
|
const ROOT_CHILD_DISTANCE = 255;
|
|
15
15
|
const PROVIDER_CHILD_DISTANCE = 178;
|
|
@@ -43,41 +43,70 @@ const ROOT_CLUSTER_RELAXATION_STEP = 48;
|
|
|
43
43
|
const ROOT_CLUSTER_RELAXATION_ATTEMPTS = 7;
|
|
44
44
|
const ROOT_CLUSTER_MAX_DISTANCE = ROOT_CHILD_DISTANCE + ROOT_CHILD_MAX_EXTRA_DISTANCE + 260;
|
|
45
45
|
const SAVE_DEBOUNCE_MS = 450;
|
|
46
|
+
const SAVE_TOAST_MS = 2600;
|
|
46
47
|
const canvas = document.getElementById("workspaceCanvas");
|
|
47
48
|
const ctx = canvas.getContext("2d");
|
|
48
49
|
const apiStatus = document.getElementById("apiStatus");
|
|
49
50
|
const addButton = document.getElementById("addButton");
|
|
50
51
|
const fitButton = document.getElementById("fitButton");
|
|
51
|
-
const
|
|
52
|
+
const testNodeButton = document.getElementById("testNodeButton");
|
|
53
|
+
const loadModelsButton = document.getElementById("loadModelsButton");
|
|
54
|
+
const closePanelButton = document.getElementById("closePanelButton");
|
|
52
55
|
const deleteNodeButton = document.getElementById("deleteNodeButton");
|
|
56
|
+
const renameConfirmOverlay = document.getElementById("renameConfirmOverlay");
|
|
57
|
+
const renameConfirmMessage = document.getElementById("renameConfirmMessage");
|
|
58
|
+
const cancelRenameButton = document.getElementById("cancelRenameButton");
|
|
59
|
+
const confirmRenameButton = document.getElementById("confirmRenameButton");
|
|
53
60
|
const deleteConfirmOverlay = document.getElementById("deleteConfirmOverlay");
|
|
54
61
|
const deleteConfirmMessage = document.getElementById("deleteConfirmMessage");
|
|
55
62
|
const cancelDeleteButton = document.getElementById("cancelDeleteButton");
|
|
56
63
|
const confirmDeleteButton = document.getElementById("confirmDeleteButton");
|
|
64
|
+
const agentRunOverlay = document.getElementById("agentRunOverlay");
|
|
65
|
+
const agentRunTitle = document.getElementById("agentRunTitle");
|
|
66
|
+
const agentRunHint = document.getElementById("agentRunHint");
|
|
67
|
+
const agentRunInputField = document.getElementById("agentRunInputField");
|
|
68
|
+
const agentRunInput = document.getElementById("agentRunInput");
|
|
69
|
+
const agentRunResult = document.getElementById("agentRunResult");
|
|
70
|
+
const cancelAgentRunButton = document.getElementById("cancelAgentRunButton");
|
|
71
|
+
const executeAgentRunButton = document.getElementById("executeAgentRunButton");
|
|
57
72
|
const fallbackPanelHost = document.getElementById("fallbackPanelHost");
|
|
58
73
|
const configSurface = document.getElementById("configSurface");
|
|
59
74
|
const form = configSurface;
|
|
75
|
+
const workspaceToast = document.getElementById("workspaceToast");
|
|
76
|
+
const workspaceToastIcon = document.getElementById("workspaceToastIcon");
|
|
77
|
+
const workspaceToastMessage = document.getElementById("workspaceToastMessage");
|
|
78
|
+
let toastTimerId = null;
|
|
79
|
+
let savedAgentLabel = null;
|
|
80
|
+
let savedAgentFolder = null;
|
|
81
|
+
let agentRunTargetId = null;
|
|
60
82
|
const fields = {
|
|
61
83
|
name: document.getElementById("nodeName"),
|
|
62
84
|
kind: document.getElementById("nodeKind"),
|
|
63
|
-
environment: document.getElementById("nodeEnvironment"),
|
|
64
85
|
endpoint: document.getElementById("nodeEndpoint"),
|
|
65
86
|
token: document.getElementById("nodeToken"),
|
|
87
|
+
model: document.getElementById("nodeModel"),
|
|
88
|
+
workspaceField: document.getElementById("agentWorkspaceField"),
|
|
89
|
+
workspaceFolder: document.getElementById("nodeWorkspaceFolder"),
|
|
66
90
|
timeout: document.getElementById("nodeTimeout"),
|
|
67
|
-
retryPolicy: document.getElementById("nodeRetryPolicy"),
|
|
68
91
|
description: document.getElementById("nodeDescription"),
|
|
69
92
|
typeBadge: document.getElementById("nodeTypeBadge"),
|
|
70
|
-
|
|
93
|
+
llmFields: document.getElementById("llmFields"),
|
|
71
94
|
mcpInventory: document.getElementById("mcpInventory"),
|
|
95
|
+
agentSection: document.getElementById("agentSection"),
|
|
96
|
+
systemPrompt: document.getElementById("nodeSystemPrompt"),
|
|
97
|
+
requiresUserInput: document.getElementById("nodeRequiresUserInput"),
|
|
98
|
+
userInputDescriptionField: document.getElementById("agentUserInputDescriptionField"),
|
|
99
|
+
userInputDescription: document.getElementById("nodeUserInputDescription"),
|
|
100
|
+
expectedOutputMarker: document.getElementById("nodeExpectedOutputMarker"),
|
|
72
101
|
};
|
|
73
102
|
const initialEntities = [
|
|
74
103
|
createEntity("root", "Living AI Documentation", "system", null),
|
|
75
|
-
createEntity("devstral", "DevStral2 LLM API", "llm", "root"),
|
|
76
|
-
createEntity("qwen", "Qwen2 LLM API", "llm", "root"),
|
|
77
104
|
createEntity("mcp", "MCP", "mcp", "root"),
|
|
78
|
-
createEntity("
|
|
79
|
-
createEntity("
|
|
80
|
-
createEntity("
|
|
105
|
+
//createEntity("devstral", "DevStral2 LLM API", "llm", "root"),
|
|
106
|
+
//createEntity("qwen", "Qwen2 LLM API", "llm", "root"),
|
|
107
|
+
//createEntity("review-agent", "Code Review Agent", "agent", "devstral"),
|
|
108
|
+
//createEntity("sonar-agent", "Sonarqube Metrics Agent", "agent", "devstral"),
|
|
109
|
+
//createEntity("doc-agent", "Documentation Agent", "agent", "qwen"),
|
|
81
110
|
];
|
|
82
111
|
const state = {
|
|
83
112
|
entities: cloneEntities(initialEntities),
|
|
@@ -135,16 +164,6 @@ fitButton.addEventListener("click", () => {
|
|
|
135
164
|
scheduleWorkspaceSave();
|
|
136
165
|
scheduleRender();
|
|
137
166
|
});
|
|
138
|
-
resetButton.addEventListener("click", () => {
|
|
139
|
-
state.entities = cloneEntities(initialEntities);
|
|
140
|
-
state.selectedId = null;
|
|
141
|
-
state.lastSaveAt = null;
|
|
142
|
-
layoutGraph();
|
|
143
|
-
resetView(true, true);
|
|
144
|
-
syncPanelFromSelection();
|
|
145
|
-
scheduleWorkspaceSave();
|
|
146
|
-
scheduleRender();
|
|
147
|
-
});
|
|
148
167
|
canvas.addEventListener("pointerdown", (event) => {
|
|
149
168
|
if (event.target !== canvas) {
|
|
150
169
|
return;
|
|
@@ -155,7 +174,8 @@ canvas.addEventListener("pointerdown", (event) => {
|
|
|
155
174
|
.reverse()
|
|
156
175
|
.find((entity) => isEntityHit(entity, worldPoint));
|
|
157
176
|
if (hitEntity) {
|
|
158
|
-
|
|
177
|
+
if (hitEntity.id !== "root")
|
|
178
|
+
selectEntity(hitEntity.id);
|
|
159
179
|
return;
|
|
160
180
|
}
|
|
161
181
|
state.isPanning = true;
|
|
@@ -212,18 +232,23 @@ form.addEventListener("submit", (event) => {
|
|
|
212
232
|
event.preventDefault();
|
|
213
233
|
syncSelectedFromForm();
|
|
214
234
|
state.lastSaveAt = new Date();
|
|
215
|
-
fields.healthBadge.textContent = "Applied";
|
|
216
235
|
layoutGraph();
|
|
217
|
-
void saveWorkspaceNow();
|
|
236
|
+
void saveWorkspaceNow(true);
|
|
218
237
|
scheduleRender();
|
|
219
238
|
});
|
|
220
|
-
form.addEventListener("input", () => {
|
|
239
|
+
form.addEventListener("input", (event) => {
|
|
221
240
|
syncSelectedFromForm();
|
|
222
|
-
fields.healthBadge.textContent = "Draft";
|
|
223
241
|
layoutGraph();
|
|
224
|
-
|
|
242
|
+
// Don't auto-save while editing the name of an agent — wait for blur + popup
|
|
243
|
+
const isAgentNameEdit = event.target === fields.name && selectedEntity()?.kind === "agent";
|
|
244
|
+
if (!isAgentNameEdit) {
|
|
245
|
+
scheduleWorkspaceSave();
|
|
246
|
+
}
|
|
225
247
|
scheduleRender();
|
|
226
248
|
});
|
|
249
|
+
closePanelButton.addEventListener("click", () => {
|
|
250
|
+
selectEntity(null);
|
|
251
|
+
});
|
|
227
252
|
deleteNodeButton.addEventListener("click", (event) => {
|
|
228
253
|
event.preventDefault();
|
|
229
254
|
event.stopPropagation();
|
|
@@ -246,9 +271,75 @@ confirmDeleteButton.addEventListener("click", () => {
|
|
|
246
271
|
}
|
|
247
272
|
closeDeleteConfirmation();
|
|
248
273
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
274
|
+
testNodeButton.addEventListener("click", () => {
|
|
275
|
+
const selected = selectedEntity();
|
|
276
|
+
if (selected?.kind === "agent") {
|
|
277
|
+
openAgentRunDialog();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
void testSelectedLlmConnection();
|
|
281
|
+
});
|
|
282
|
+
loadModelsButton.addEventListener("click", () => {
|
|
283
|
+
void loadModelsForSelect();
|
|
284
|
+
});
|
|
285
|
+
fields.name.addEventListener("blur", () => {
|
|
286
|
+
const selected = selectedEntity();
|
|
287
|
+
if (!selected || selected.kind !== "agent")
|
|
288
|
+
return;
|
|
289
|
+
const newName = fields.name.value.trim();
|
|
290
|
+
if (!newName ||
|
|
291
|
+
!savedAgentLabel ||
|
|
292
|
+
!savedAgentFolder ||
|
|
293
|
+
newName === savedAgentLabel)
|
|
294
|
+
return;
|
|
295
|
+
const newFolder = workspaceFolderForProvider(newName);
|
|
296
|
+
if (newFolder === savedAgentFolder)
|
|
297
|
+
return; // slug identique, pas de renommage nécessaire
|
|
298
|
+
renameConfirmMessage.textContent = `Renommer le dossier "${savedAgentFolder}" en "${newFolder}" ?`;
|
|
299
|
+
renameConfirmOverlay.hidden = false;
|
|
300
|
+
cancelRenameButton.focus();
|
|
301
|
+
});
|
|
302
|
+
cancelRenameButton.addEventListener("click", () => {
|
|
303
|
+
renameConfirmOverlay.hidden = true;
|
|
304
|
+
const selected = selectedEntity();
|
|
305
|
+
if (selected && savedAgentLabel) {
|
|
306
|
+
selected.label = savedAgentLabel;
|
|
307
|
+
fields.name.value = savedAgentLabel;
|
|
308
|
+
layoutGraph();
|
|
309
|
+
scheduleRender();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
confirmRenameButton.addEventListener("click", () => {
|
|
313
|
+
renameConfirmOverlay.hidden = true;
|
|
314
|
+
const selected = selectedEntity();
|
|
315
|
+
if (!selected || !savedAgentFolder)
|
|
316
|
+
return;
|
|
317
|
+
// Remettre l'ancien chemin pour que le backend sache quoi renommer
|
|
318
|
+
selected.config.workspaceFolder = savedAgentFolder;
|
|
319
|
+
fields.workspaceFolder.value = savedAgentFolder;
|
|
320
|
+
const newName = fields.name.value.trim();
|
|
321
|
+
if (newName)
|
|
322
|
+
savedAgentLabel = newName;
|
|
323
|
+
savedAgentFolder = workspaceFolderForProvider(newName);
|
|
324
|
+
scheduleWorkspaceSave();
|
|
325
|
+
});
|
|
326
|
+
fields.model.addEventListener("change", () => {
|
|
327
|
+
testNodeButton.disabled = !fields.model.value;
|
|
328
|
+
const selected = selectedEntity();
|
|
329
|
+
if (selected) {
|
|
330
|
+
selected.config.model = fields.model.value;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
cancelAgentRunButton.addEventListener("click", () => {
|
|
334
|
+
closeAgentRunDialog();
|
|
335
|
+
});
|
|
336
|
+
executeAgentRunButton.addEventListener("click", () => {
|
|
337
|
+
void executeAgentRunFromDialog();
|
|
338
|
+
});
|
|
339
|
+
agentRunOverlay.addEventListener("click", (event) => {
|
|
340
|
+
if (event.target === agentRunOverlay) {
|
|
341
|
+
closeAgentRunDialog();
|
|
342
|
+
}
|
|
252
343
|
});
|
|
253
344
|
void initializeWorkspace();
|
|
254
345
|
function isolatePanelEvents() {
|
|
@@ -260,6 +351,8 @@ function isolatePanelEvents() {
|
|
|
260
351
|
function isolateConfirmEvents() {
|
|
261
352
|
for (const type of PANEL_EVENT_TYPES) {
|
|
262
353
|
deleteConfirmOverlay.addEventListener(type, stopPanelEventPropagation);
|
|
354
|
+
renameConfirmOverlay.addEventListener(type, stopPanelEventPropagation);
|
|
355
|
+
agentRunOverlay.addEventListener(type, stopPanelEventPropagation);
|
|
263
356
|
}
|
|
264
357
|
}
|
|
265
358
|
function stopPanelEventPropagation(event) {
|
|
@@ -272,7 +365,9 @@ async function initializeWorkspace() {
|
|
|
272
365
|
state.cameraX = finiteNumber(persisted.camera?.x, state.cameraX);
|
|
273
366
|
state.cameraY = finiteNumber(persisted.camera?.y, state.cameraY);
|
|
274
367
|
state.zoom = clamp(finiteNumber(persisted.camera?.zoom, state.zoom), ZOOM_MIN, ZOOM_MAX);
|
|
275
|
-
state.lastSaveAt = persisted.updatedAt
|
|
368
|
+
state.lastSaveAt = persisted.updatedAt
|
|
369
|
+
? new Date(persisted.updatedAt)
|
|
370
|
+
: null;
|
|
276
371
|
}
|
|
277
372
|
state.isHydrated = true;
|
|
278
373
|
layoutGraph();
|
|
@@ -289,13 +384,19 @@ function hydrateEntities(entities) {
|
|
|
289
384
|
const hasRoot = hydrated.some((entity) => entity.id === "root");
|
|
290
385
|
const hasMcp = hydrated.some((entity) => entity.id === "mcp");
|
|
291
386
|
const withRequiredNodes = [
|
|
292
|
-
...(hasRoot
|
|
387
|
+
...(hasRoot
|
|
388
|
+
? []
|
|
389
|
+
: [createEntity("root", "Living AI Documentation", "system", null)]),
|
|
293
390
|
...hydrated,
|
|
294
391
|
...(hasMcp ? [] : [createEntity("mcp", "MCP", "mcp", "root")]),
|
|
295
392
|
];
|
|
296
393
|
const ids = new Set(withRequiredNodes.map((entity) => entity.id));
|
|
297
394
|
return withRequiredNodes.map((entity) => {
|
|
298
|
-
const normalizedKind = entity.id === "root"
|
|
395
|
+
const normalizedKind = entity.id === "root"
|
|
396
|
+
? "system"
|
|
397
|
+
: entity.id === "mcp"
|
|
398
|
+
? "mcp"
|
|
399
|
+
: entity.kind;
|
|
299
400
|
const normalizedParentId = entity.id === "root"
|
|
300
401
|
? null
|
|
301
402
|
: entity.id === "mcp"
|
|
@@ -335,12 +436,17 @@ function hydrateEntity(input) {
|
|
|
335
436
|
y: finiteNumber(input.y, 0),
|
|
336
437
|
angle: finiteNumber(input.angle, 0),
|
|
337
438
|
config: {
|
|
338
|
-
environment: stringValue(config.environment) || defaultEnvironment(kind),
|
|
339
439
|
endpoint: stringValue(config.endpoint) || defaultEndpoint(id, kind),
|
|
340
440
|
token: stringValue(config.token),
|
|
341
|
-
|
|
342
|
-
|
|
441
|
+
model: stringValue(config.model),
|
|
442
|
+
timeout: finiteNumber(config.timeout, 180),
|
|
343
443
|
description: stringValue(config.description) || defaultDescription(kind),
|
|
444
|
+
workspaceFolder: stringValue(config.workspaceFolder) ||
|
|
445
|
+
(kind === "agent" ? workspaceFolderForProvider(label) : ""),
|
|
446
|
+
systemPrompt: stringValue(config.systemPrompt),
|
|
447
|
+
requiresUserInput: config.requiresUserInput === true,
|
|
448
|
+
userInputDescription: stringValue(config.userInputDescription),
|
|
449
|
+
expectedOutputMarker: stringValue(config.expectedOutputMarker),
|
|
344
450
|
},
|
|
345
451
|
};
|
|
346
452
|
}
|
|
@@ -353,10 +459,10 @@ function scheduleWorkspaceSave() {
|
|
|
353
459
|
}
|
|
354
460
|
state.saveTimerId = window.setTimeout(() => {
|
|
355
461
|
state.saveTimerId = null;
|
|
356
|
-
void saveWorkspaceNow();
|
|
462
|
+
void saveWorkspaceNow(false);
|
|
357
463
|
}, SAVE_DEBOUNCE_MS);
|
|
358
464
|
}
|
|
359
|
-
async function saveWorkspaceNow() {
|
|
465
|
+
async function saveWorkspaceNow(notify = false) {
|
|
360
466
|
if (!state.isHydrated) {
|
|
361
467
|
return;
|
|
362
468
|
}
|
|
@@ -366,11 +472,41 @@ async function saveWorkspaceNow() {
|
|
|
366
472
|
}
|
|
367
473
|
const saved = await saveWorkspaceState(serializeWorkspace());
|
|
368
474
|
if (saved) {
|
|
475
|
+
state.entities = hydrateEntities(saved.entities);
|
|
369
476
|
state.lastSaveAt = new Date(saved.updatedAt);
|
|
370
477
|
if (selectedEntity()) {
|
|
371
|
-
|
|
478
|
+
syncPanelFromSelection();
|
|
479
|
+
}
|
|
480
|
+
if (notify) {
|
|
481
|
+
showSaveToast("Workspace saved");
|
|
372
482
|
}
|
|
373
483
|
}
|
|
484
|
+
else if (notify) {
|
|
485
|
+
showSaveToast("Save failed");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function showLoadingToast(message) {
|
|
489
|
+
if (toastTimerId) {
|
|
490
|
+
clearTimeout(toastTimerId);
|
|
491
|
+
toastTimerId = null;
|
|
492
|
+
}
|
|
493
|
+
workspaceToast.dataset.state = "loading";
|
|
494
|
+
workspaceToastIcon.textContent = "↻";
|
|
495
|
+
workspaceToastMessage.textContent = message;
|
|
496
|
+
workspaceToast.hidden = false;
|
|
497
|
+
}
|
|
498
|
+
function showSaveToast(message, state = "success") {
|
|
499
|
+
if (toastTimerId) {
|
|
500
|
+
clearTimeout(toastTimerId);
|
|
501
|
+
}
|
|
502
|
+
workspaceToast.dataset.state = state;
|
|
503
|
+
workspaceToastIcon.textContent = state === "error" ? "✕" : "✓";
|
|
504
|
+
workspaceToastMessage.textContent = message;
|
|
505
|
+
workspaceToast.hidden = false;
|
|
506
|
+
toastTimerId = window.setTimeout(() => {
|
|
507
|
+
workspaceToast.hidden = true;
|
|
508
|
+
toastTimerId = null;
|
|
509
|
+
}, SAVE_TOAST_MS);
|
|
374
510
|
}
|
|
375
511
|
function serializeWorkspace() {
|
|
376
512
|
return {
|
|
@@ -400,7 +536,10 @@ function stringValue(input) {
|
|
|
400
536
|
return typeof input === "string" ? input : "";
|
|
401
537
|
}
|
|
402
538
|
function nodeKindValue(input) {
|
|
403
|
-
return input === "system" ||
|
|
539
|
+
return input === "system" ||
|
|
540
|
+
input === "llm" ||
|
|
541
|
+
input === "agent" ||
|
|
542
|
+
input === "mcp"
|
|
404
543
|
? input
|
|
405
544
|
: null;
|
|
406
545
|
}
|
|
@@ -417,12 +556,16 @@ function createEntity(id, label, kind, parentId) {
|
|
|
417
556
|
y: 0,
|
|
418
557
|
angle: 0,
|
|
419
558
|
config: {
|
|
420
|
-
environment: defaultEnvironment(kind),
|
|
421
559
|
endpoint: defaultEndpoint(id, kind),
|
|
422
560
|
token: "",
|
|
423
|
-
|
|
424
|
-
|
|
561
|
+
model: "",
|
|
562
|
+
timeout: 180,
|
|
425
563
|
description: defaultDescription(kind),
|
|
564
|
+
workspaceFolder: kind === "agent" ? workspaceFolderForProvider(label) : "",
|
|
565
|
+
systemPrompt: "",
|
|
566
|
+
requiresUserInput: false,
|
|
567
|
+
userInputDescription: "",
|
|
568
|
+
expectedOutputMarker: "",
|
|
426
569
|
},
|
|
427
570
|
};
|
|
428
571
|
}
|
|
@@ -432,21 +575,27 @@ function cloneEntities(entities) {
|
|
|
432
575
|
config: { ...entity.config },
|
|
433
576
|
}));
|
|
434
577
|
}
|
|
435
|
-
function defaultEnvironment(kind) {
|
|
436
|
-
if (kind === "llm" || kind === "mcp") {
|
|
437
|
-
return "production";
|
|
438
|
-
}
|
|
439
|
-
return "local";
|
|
440
|
-
}
|
|
441
578
|
function defaultEndpoint(id, kind) {
|
|
442
579
|
if (kind === "llm") {
|
|
443
|
-
return
|
|
580
|
+
return "http://localhost:11434";
|
|
444
581
|
}
|
|
445
582
|
if (kind === "mcp") {
|
|
446
583
|
return "http://localhost:4321/mcp";
|
|
447
584
|
}
|
|
448
585
|
return "";
|
|
449
586
|
}
|
|
587
|
+
function workspaceFolderForProvider(label) {
|
|
588
|
+
return `AI/WORKSPACE/${slugify(label)}`;
|
|
589
|
+
}
|
|
590
|
+
function slugify(value) {
|
|
591
|
+
const slug = value
|
|
592
|
+
.normalize("NFKD")
|
|
593
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
594
|
+
.toLowerCase()
|
|
595
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
596
|
+
.replace(/^_+|_+$/g, "");
|
|
597
|
+
return slug || "llm_provider";
|
|
598
|
+
}
|
|
450
599
|
function defaultDescription(kind) {
|
|
451
600
|
const copy = {
|
|
452
601
|
system: "Owns the documentation workspace, MCP access, providers, and agent topology.",
|
|
@@ -481,8 +630,7 @@ function addProvider() {
|
|
|
481
630
|
}
|
|
482
631
|
function addAgent(parentId) {
|
|
483
632
|
const hadSelection = Boolean(state.selectedId);
|
|
484
|
-
const index = childrenOf(parentId).filter((entity) => entity.kind === "agent").length +
|
|
485
|
-
1;
|
|
633
|
+
const index = childrenOf(parentId).filter((entity) => entity.kind === "agent").length + 1;
|
|
486
634
|
const id = `agent-${Date.now().toString(36)}`;
|
|
487
635
|
const agent = createEntity(id, `Agent ${index}`, "agent", parentId);
|
|
488
636
|
state.entities = [...state.entities, agent];
|
|
@@ -650,6 +798,13 @@ function collisionRadius(entity) {
|
|
|
650
798
|
function selectEntity(id) {
|
|
651
799
|
const hadSelection = Boolean(state.selectedId);
|
|
652
800
|
state.selectedId = id;
|
|
801
|
+
const entity = id ? entityById(id) : null;
|
|
802
|
+
savedAgentLabel = entity?.kind === "agent" ? entity.label : null;
|
|
803
|
+
savedAgentFolder =
|
|
804
|
+
entity?.kind === "agent"
|
|
805
|
+
? entity.config.workspaceFolder ||
|
|
806
|
+
workspaceFolderForProvider(entity.label)
|
|
807
|
+
: null;
|
|
653
808
|
layoutGraph();
|
|
654
809
|
syncCameraForPanelChange(hadSelection, Boolean(id), true);
|
|
655
810
|
syncPanelFromSelection();
|
|
@@ -693,21 +848,38 @@ function syncPanelFromSelection() {
|
|
|
693
848
|
fallbackPanelHost.hidden = !hasSelection || supportsHtmlInCanvas;
|
|
694
849
|
if (!selected) {
|
|
695
850
|
addButton.title = "Add LLM provider";
|
|
851
|
+
testNodeButton.textContent = "Test";
|
|
852
|
+
testNodeButton.hidden = true;
|
|
853
|
+
testNodeButton.disabled = true;
|
|
696
854
|
return;
|
|
697
855
|
}
|
|
698
856
|
fields.name.value = selected.label;
|
|
699
857
|
fields.kind.value = selected.kind;
|
|
700
|
-
fields.environment.value = selected.config.environment;
|
|
701
858
|
fields.endpoint.value = selected.config.endpoint;
|
|
702
859
|
fields.token.value = selected.config.token;
|
|
860
|
+
restoreModelSelect(selected.config.model);
|
|
861
|
+
fields.workspaceFolder.value = selected.config.workspaceFolder;
|
|
862
|
+
fields.workspaceField.hidden = selected.kind !== "agent";
|
|
703
863
|
fields.timeout.value = String(selected.config.timeout);
|
|
704
|
-
fields.retryPolicy.value = selected.config.retryPolicy;
|
|
705
864
|
fields.description.value = selected.config.description;
|
|
706
865
|
fields.typeBadge.textContent = labelForBadge(selected.kind);
|
|
707
|
-
fields.healthBadge.textContent = state.lastSaveAt ? "Applied" : "Ready";
|
|
708
866
|
fields.kind.disabled = true;
|
|
867
|
+
fields.llmFields.hidden =
|
|
868
|
+
selected.kind === "mcp" || selected.kind === "agent";
|
|
709
869
|
fields.mcpInventory.hidden = selected.kind !== "mcp";
|
|
710
|
-
|
|
870
|
+
fields.agentSection.hidden = selected.kind !== "agent";
|
|
871
|
+
fields.systemPrompt.value = selected.config.systemPrompt;
|
|
872
|
+
fields.requiresUserInput.checked = selected.config.requiresUserInput;
|
|
873
|
+
syncUserInputDescriptionVisibility();
|
|
874
|
+
fields.userInputDescription.value = selected.config.userInputDescription;
|
|
875
|
+
fields.expectedOutputMarker.value = selected.config.expectedOutputMarker;
|
|
876
|
+
deleteNodeButton.hidden = isProtectedEntity(selected.id);
|
|
877
|
+
testNodeButton.textContent = "Test";
|
|
878
|
+
testNodeButton.hidden = selected.kind !== "llm" && selected.kind !== "agent";
|
|
879
|
+
testNodeButton.disabled =
|
|
880
|
+
selected.kind === "llm"
|
|
881
|
+
? !selected.config.model
|
|
882
|
+
: selected.kind !== "agent";
|
|
711
883
|
addButton.title =
|
|
712
884
|
selected.kind === "llm" || entityById(selected.parentId)?.kind === "llm"
|
|
713
885
|
? "Add agent"
|
|
@@ -720,13 +892,198 @@ function syncSelectedFromForm() {
|
|
|
720
892
|
}
|
|
721
893
|
selected.label = fields.name.value.trim() || "Untitled node";
|
|
722
894
|
selected.kind = fields.kind.value;
|
|
723
|
-
selected.config.environment = fields.environment.value;
|
|
724
895
|
selected.config.endpoint = fields.endpoint.value;
|
|
725
896
|
selected.config.token = fields.token.value;
|
|
897
|
+
selected.config.model = fields.model.value;
|
|
726
898
|
selected.config.timeout = Number(fields.timeout.value || 30);
|
|
727
|
-
selected.config.retryPolicy = fields.retryPolicy.value;
|
|
728
899
|
selected.config.description = fields.description.value;
|
|
900
|
+
selected.config.systemPrompt = fields.systemPrompt.value;
|
|
901
|
+
selected.config.requiresUserInput = fields.requiresUserInput.checked;
|
|
902
|
+
selected.config.userInputDescription = fields.userInputDescription.value;
|
|
903
|
+
selected.config.expectedOutputMarker = fields.expectedOutputMarker.value;
|
|
904
|
+
syncUserInputDescriptionVisibility();
|
|
729
905
|
fields.typeBadge.textContent = labelForBadge(selected.kind);
|
|
906
|
+
// Propagate model + timeout to child agents
|
|
907
|
+
if (selected.kind === "llm") {
|
|
908
|
+
for (const entity of state.entities) {
|
|
909
|
+
if (entity.parentId === selected.id && entity.kind === "agent") {
|
|
910
|
+
entity.config.model = selected.config.model;
|
|
911
|
+
entity.config.timeout = selected.config.timeout;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function syncUserInputDescriptionVisibility() {
|
|
917
|
+
const shouldShow = selectedEntity()?.kind === "agent" && fields.requiresUserInput.checked;
|
|
918
|
+
fields.userInputDescriptionField.hidden = !shouldShow;
|
|
919
|
+
fields.userInputDescription.required = shouldShow;
|
|
920
|
+
}
|
|
921
|
+
function restoreModelSelect(savedModel) {
|
|
922
|
+
const existing = Array.from(fields.model.options).map((o) => o.value);
|
|
923
|
+
if (savedModel && !existing.includes(savedModel)) {
|
|
924
|
+
fields.model.innerHTML = "";
|
|
925
|
+
const opt = document.createElement("option");
|
|
926
|
+
opt.value = savedModel;
|
|
927
|
+
opt.textContent = savedModel;
|
|
928
|
+
fields.model.appendChild(opt);
|
|
929
|
+
}
|
|
930
|
+
fields.model.value = savedModel || (existing[0] ?? "");
|
|
931
|
+
}
|
|
932
|
+
function openAgentRunDialog() {
|
|
933
|
+
const selected = selectedEntity();
|
|
934
|
+
if (!selected || selected.kind !== "agent")
|
|
935
|
+
return;
|
|
936
|
+
syncSelectedFromForm();
|
|
937
|
+
const llm = entityById(selected.parentId ?? "");
|
|
938
|
+
if (!llm || llm.kind !== "llm") {
|
|
939
|
+
showSaveToast("No parent LLM found for this agent.", "error");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (!llm.config.model) {
|
|
943
|
+
showSaveToast("Parent LLM has no model selected.", "error");
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (!selected.config.systemPrompt.trim()) {
|
|
947
|
+
showSaveToast("Write a system prompt first.", "error");
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
if (selected.config.requiresUserInput &&
|
|
951
|
+
!selected.config.userInputDescription.trim()) {
|
|
952
|
+
showSaveToast("Describe the required user input first.", "error");
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
agentRunTargetId = selected.id;
|
|
956
|
+
agentRunTitle.textContent = `Test ${selected.label}`;
|
|
957
|
+
agentRunHint.textContent = selected.config.requiresUserInput
|
|
958
|
+
? selected.config.userInputDescription
|
|
959
|
+
: "Run this agent with its configured system prompt.";
|
|
960
|
+
agentRunInputField.hidden = !selected.config.requiresUserInput;
|
|
961
|
+
agentRunInput.required = selected.config.requiresUserInput;
|
|
962
|
+
agentRunInput.value = "";
|
|
963
|
+
agentRunResult.hidden = true;
|
|
964
|
+
agentRunResult.textContent = "";
|
|
965
|
+
executeAgentRunButton.disabled = false;
|
|
966
|
+
executeAgentRunButton.textContent = "Run";
|
|
967
|
+
agentRunOverlay.hidden = false;
|
|
968
|
+
if (selected.config.requiresUserInput) {
|
|
969
|
+
window.setTimeout(() => agentRunInput.focus(), 0);
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
window.setTimeout(() => executeAgentRunButton.focus(), 0);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function closeAgentRunDialog() {
|
|
976
|
+
agentRunOverlay.hidden = true;
|
|
977
|
+
agentRunTargetId = null;
|
|
978
|
+
}
|
|
979
|
+
async function executeAgentRunFromDialog() {
|
|
980
|
+
if (agentRunInput.required && !agentRunInput.value.trim()) {
|
|
981
|
+
agentRunInput.reportValidity();
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const selected = agentRunTargetId ? entityById(agentRunTargetId) : null;
|
|
985
|
+
if (!selected || selected.kind !== "agent") {
|
|
986
|
+
showAgentRunResult("error", "The selected agent is no longer available.");
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const llm = entityById(selected.parentId ?? "");
|
|
990
|
+
if (!llm || llm.kind !== "llm") {
|
|
991
|
+
showAgentRunResult("error", "No parent LLM found for this agent.");
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
executeAgentRunButton.disabled = true;
|
|
995
|
+
executeAgentRunButton.textContent = "Running…";
|
|
996
|
+
showAgentRunResult("loading", "Running agent…");
|
|
997
|
+
const mcpEntity = state.entities.find((e) => e.kind === "mcp");
|
|
998
|
+
const result = await runAgentPrompt({
|
|
999
|
+
endpoint: llm.config.endpoint,
|
|
1000
|
+
token: llm.config.token,
|
|
1001
|
+
model: llm.config.model,
|
|
1002
|
+
systemPrompt: selected.config.systemPrompt,
|
|
1003
|
+
userInput: agentRunInput.value.trim() || undefined,
|
|
1004
|
+
timeout: llm.config.timeout,
|
|
1005
|
+
mcpEndpoint: mcpEntity?.config.endpoint || undefined,
|
|
1006
|
+
expectedOutputMarker: selected.config.expectedOutputMarker || undefined,
|
|
1007
|
+
});
|
|
1008
|
+
executeAgentRunButton.disabled = false;
|
|
1009
|
+
executeAgentRunButton.textContent = "Run";
|
|
1010
|
+
if (result.ok && result.content) {
|
|
1011
|
+
showAgentRunResult("success", result.content);
|
|
1012
|
+
showSaveToast("Agent responded.", "success");
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
showAgentRunResult("error", result.error ?? "Agent failed.");
|
|
1016
|
+
showSaveToast(result.error ?? "Agent failed.", "error");
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function showAgentRunResult(state, message) {
|
|
1020
|
+
agentRunResult.hidden = false;
|
|
1021
|
+
agentRunResult.dataset.state = state;
|
|
1022
|
+
agentRunResult.textContent = message;
|
|
1023
|
+
}
|
|
1024
|
+
async function loadModelsForSelect() {
|
|
1025
|
+
const selected = selectedEntity();
|
|
1026
|
+
if (!selected || selected.kind !== "llm")
|
|
1027
|
+
return;
|
|
1028
|
+
syncSelectedFromForm();
|
|
1029
|
+
const endpoint = selected.config.endpoint;
|
|
1030
|
+
if (!endpoint) {
|
|
1031
|
+
showSaveToast("Set an endpoint first.");
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
loadModelsButton.classList.add("loading");
|
|
1035
|
+
loadModelsButton.disabled = true;
|
|
1036
|
+
fields.model.disabled = true;
|
|
1037
|
+
const result = await listLlmModels({
|
|
1038
|
+
endpoint,
|
|
1039
|
+
token: selected.config.token,
|
|
1040
|
+
});
|
|
1041
|
+
loadModelsButton.classList.remove("loading");
|
|
1042
|
+
loadModelsButton.disabled = false;
|
|
1043
|
+
fields.model.disabled = false;
|
|
1044
|
+
if (!result.ok || !result.models?.length) {
|
|
1045
|
+
showSaveToast(result.error ?? "No models found.");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const previousValue = fields.model.value;
|
|
1049
|
+
fields.model.innerHTML = "";
|
|
1050
|
+
for (const id of result.models) {
|
|
1051
|
+
const opt = document.createElement("option");
|
|
1052
|
+
opt.value = id;
|
|
1053
|
+
opt.textContent = id;
|
|
1054
|
+
fields.model.appendChild(opt);
|
|
1055
|
+
}
|
|
1056
|
+
fields.model.value = result.models.includes(previousValue)
|
|
1057
|
+
? previousValue
|
|
1058
|
+
: result.models[0];
|
|
1059
|
+
selected.config.model = fields.model.value;
|
|
1060
|
+
testNodeButton.disabled = !fields.model.value;
|
|
1061
|
+
showSaveToast(`${result.models.length} model${result.models.length === 1 ? "" : "s"} loaded.`);
|
|
1062
|
+
scheduleRender();
|
|
1063
|
+
}
|
|
1064
|
+
async function testSelectedLlmConnection() {
|
|
1065
|
+
const selected = selectedEntity();
|
|
1066
|
+
if (!selected || selected.kind !== "llm") {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
syncSelectedFromForm();
|
|
1070
|
+
testNodeButton.disabled = true;
|
|
1071
|
+
showLoadingToast(`Testing ${selected.config.model}…`);
|
|
1072
|
+
scheduleRender();
|
|
1073
|
+
const result = await testLlmConnection({
|
|
1074
|
+
endpoint: selected.config.endpoint,
|
|
1075
|
+
token: selected.config.token,
|
|
1076
|
+
model: selected.config.model,
|
|
1077
|
+
timeout: selected.config.timeout,
|
|
1078
|
+
});
|
|
1079
|
+
testNodeButton.disabled = false;
|
|
1080
|
+
if (result.ok) {
|
|
1081
|
+
showSaveToast(result.detail ?? "Connection OK", "success");
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
showSaveToast(result.error || `Connection failed (${result.status || "no status"})`, "error");
|
|
1085
|
+
}
|
|
1086
|
+
scheduleRender();
|
|
730
1087
|
}
|
|
731
1088
|
function labelForBadge(kind) {
|
|
732
1089
|
const labels = {
|
|
@@ -901,7 +1258,7 @@ function drawConfigSurface(width, height) {
|
|
|
901
1258
|
return;
|
|
902
1259
|
}
|
|
903
1260
|
const panelWidth = panelWidthForViewport(width);
|
|
904
|
-
const panelHeight = Math.min(PANEL_HEIGHT, height -
|
|
1261
|
+
const panelHeight = Math.min(PANEL_HEIGHT, height - 118);
|
|
905
1262
|
const panelX = width - panelWidth - 24;
|
|
906
1263
|
const panelY = Math.max(24, (height - panelHeight) / 2);
|
|
907
1264
|
try {
|
|
@@ -939,9 +1296,25 @@ function resetView(animate, resetZoom) {
|
|
|
939
1296
|
}
|
|
940
1297
|
function syncCameraForPanelChange(hadSelection, hasSelection, animate) {
|
|
941
1298
|
if (hasSelection) {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1299
|
+
if (!hadSelection) {
|
|
1300
|
+
// Panel was closed → center selected node in the left area
|
|
1301
|
+
const selected = selectedEntity();
|
|
1302
|
+
if (selected) {
|
|
1303
|
+
const panelReserve = panelWidthForViewport(canvas.clientWidth) + PANEL_GAP;
|
|
1304
|
+
const availableWidth = canvas.clientWidth - panelReserve;
|
|
1305
|
+
const targetX = availableWidth / 2 - selected.x * state.zoom;
|
|
1306
|
+
const appliedDelta = targetX - state.cameraX;
|
|
1307
|
+
// Store negative delta so deselect reverses the shift
|
|
1308
|
+
state.panelShiftX = -appliedDelta;
|
|
1309
|
+
startViewAnimation(targetX, state.cameraY, state.zoom);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
// Panel already open → avoid occlusion only if needed
|
|
1314
|
+
const deltaX = selectedPanelAvoidanceDelta();
|
|
1315
|
+
state.panelShiftX += Math.max(0, -deltaX);
|
|
1316
|
+
translateCamera(deltaX, animate);
|
|
1317
|
+
}
|
|
945
1318
|
return;
|
|
946
1319
|
}
|
|
947
1320
|
if (!hadSelection) {
|
|
@@ -1121,8 +1494,7 @@ function rectangularBoundaryPoint(entity, angle) {
|
|
|
1121
1494
|
function isEntityHit(entity, point) {
|
|
1122
1495
|
if (entity.kind === "system" || entity.kind === "llm") {
|
|
1123
1496
|
const radius = entity.kind === "system" ? ROOT_RADIUS : PROVIDER_RADIUS;
|
|
1124
|
-
return (Math.hypot(point.x - entity.x, point.y - entity.y) <=
|
|
1125
|
-
radius + HIT_PADDING);
|
|
1497
|
+
return (Math.hypot(point.x - entity.x, point.y - entity.y) <= radius + HIT_PADDING);
|
|
1126
1498
|
}
|
|
1127
1499
|
const { width, height } = leafSize(entity);
|
|
1128
1500
|
return (Math.abs(point.x - entity.x) <= width / 2 + HIT_PADDING &&
|