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,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadWorkspaceState,
|
|
3
3
|
saveWorkspaceState,
|
|
4
|
+
testLlmConnection,
|
|
5
|
+
listLlmModels,
|
|
6
|
+
runAgentPrompt,
|
|
4
7
|
type PersistedWorkspaceDocument,
|
|
5
8
|
} from "./persistence.js";
|
|
6
9
|
|
|
@@ -22,12 +25,16 @@ type WorkspaceCanvasElement = globalThis.HTMLCanvasElement & {
|
|
|
22
25
|
type NodeKind = "system" | "llm" | "agent" | "mcp";
|
|
23
26
|
|
|
24
27
|
interface EntityConfig {
|
|
25
|
-
environment: string;
|
|
26
28
|
endpoint: string;
|
|
27
29
|
token: string;
|
|
30
|
+
model: string;
|
|
28
31
|
timeout: number;
|
|
29
|
-
retryPolicy: string;
|
|
30
32
|
description: string;
|
|
33
|
+
workspaceFolder: string;
|
|
34
|
+
systemPrompt: string;
|
|
35
|
+
requiresUserInput: boolean;
|
|
36
|
+
userInputDescription: string;
|
|
37
|
+
expectedOutputMarker: string;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
interface Entity {
|
|
@@ -87,10 +94,10 @@ const PROVIDER_RADIUS = 78;
|
|
|
87
94
|
const MIN_POLYGON_SIDES = 6;
|
|
88
95
|
const GRID_MINOR = 24;
|
|
89
96
|
const GRID_MAJOR = 96;
|
|
90
|
-
const PANEL_MIN_WIDTH =
|
|
91
|
-
const PANEL_MAX_WIDTH =
|
|
92
|
-
const PANEL_VIEWPORT_RATIO = 1 /
|
|
93
|
-
const PANEL_HEIGHT =
|
|
97
|
+
const PANEL_MIN_WIDTH = 540;
|
|
98
|
+
const PANEL_MAX_WIDTH = 780;
|
|
99
|
+
const PANEL_VIEWPORT_RATIO = 1 / 2;
|
|
100
|
+
const PANEL_HEIGHT = 9999;
|
|
94
101
|
const PANEL_GAP = 44;
|
|
95
102
|
const ROOT_CHILD_DISTANCE = 255;
|
|
96
103
|
const PROVIDER_CHILD_DISTANCE = 178;
|
|
@@ -125,44 +132,131 @@ const ROOT_CLUSTER_RELAXATION_ATTEMPTS = 7;
|
|
|
125
132
|
const ROOT_CLUSTER_MAX_DISTANCE =
|
|
126
133
|
ROOT_CHILD_DISTANCE + ROOT_CHILD_MAX_EXTRA_DISTANCE + 260;
|
|
127
134
|
const SAVE_DEBOUNCE_MS = 450;
|
|
135
|
+
const SAVE_TOAST_MS = 2600;
|
|
128
136
|
|
|
129
|
-
const canvas = document.getElementById(
|
|
137
|
+
const canvas = document.getElementById(
|
|
138
|
+
"workspaceCanvas",
|
|
139
|
+
) as WorkspaceCanvasElement;
|
|
130
140
|
const ctx = canvas.getContext("2d") as WorkspaceCanvasContext;
|
|
131
141
|
const apiStatus = document.getElementById("apiStatus") as HTMLElement;
|
|
132
142
|
const addButton = document.getElementById("addButton") as HTMLButtonElement;
|
|
133
143
|
const fitButton = document.getElementById("fitButton") as HTMLButtonElement;
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
144
|
+
const testNodeButton = document.getElementById(
|
|
145
|
+
"testNodeButton",
|
|
146
|
+
) as HTMLButtonElement;
|
|
147
|
+
const loadModelsButton = document.getElementById(
|
|
148
|
+
"loadModelsButton",
|
|
149
|
+
) as HTMLButtonElement;
|
|
150
|
+
const closePanelButton = document.getElementById(
|
|
151
|
+
"closePanelButton",
|
|
152
|
+
) as HTMLButtonElement;
|
|
153
|
+
const deleteNodeButton = document.getElementById(
|
|
154
|
+
"deleteNodeButton",
|
|
155
|
+
) as HTMLButtonElement;
|
|
156
|
+
const renameConfirmOverlay = document.getElementById(
|
|
157
|
+
"renameConfirmOverlay",
|
|
158
|
+
) as HTMLElement;
|
|
159
|
+
const renameConfirmMessage = document.getElementById(
|
|
160
|
+
"renameConfirmMessage",
|
|
161
|
+
) as HTMLElement;
|
|
162
|
+
const cancelRenameButton = document.getElementById(
|
|
163
|
+
"cancelRenameButton",
|
|
164
|
+
) as HTMLButtonElement;
|
|
165
|
+
const confirmRenameButton = document.getElementById(
|
|
166
|
+
"confirmRenameButton",
|
|
167
|
+
) as HTMLButtonElement;
|
|
168
|
+
const deleteConfirmOverlay = document.getElementById(
|
|
169
|
+
"deleteConfirmOverlay",
|
|
170
|
+
) as HTMLElement;
|
|
171
|
+
const deleteConfirmMessage = document.getElementById(
|
|
172
|
+
"deleteConfirmMessage",
|
|
173
|
+
) as HTMLElement;
|
|
174
|
+
const cancelDeleteButton = document.getElementById(
|
|
175
|
+
"cancelDeleteButton",
|
|
176
|
+
) as HTMLButtonElement;
|
|
177
|
+
const confirmDeleteButton = document.getElementById(
|
|
178
|
+
"confirmDeleteButton",
|
|
179
|
+
) as HTMLButtonElement;
|
|
180
|
+
const agentRunOverlay = document.getElementById(
|
|
181
|
+
"agentRunOverlay",
|
|
182
|
+
) as HTMLElement;
|
|
183
|
+
const agentRunTitle = document.getElementById("agentRunTitle") as HTMLElement;
|
|
184
|
+
const agentRunHint = document.getElementById("agentRunHint") as HTMLElement;
|
|
185
|
+
const agentRunInputField = document.getElementById(
|
|
186
|
+
"agentRunInputField",
|
|
187
|
+
) as HTMLElement;
|
|
188
|
+
const agentRunInput = document.getElementById(
|
|
189
|
+
"agentRunInput",
|
|
190
|
+
) as HTMLTextAreaElement;
|
|
191
|
+
const agentRunResult = document.getElementById("agentRunResult") as HTMLElement;
|
|
192
|
+
const cancelAgentRunButton = document.getElementById(
|
|
193
|
+
"cancelAgentRunButton",
|
|
194
|
+
) as HTMLButtonElement;
|
|
195
|
+
const executeAgentRunButton = document.getElementById(
|
|
196
|
+
"executeAgentRunButton",
|
|
197
|
+
) as HTMLButtonElement;
|
|
198
|
+
const fallbackPanelHost = document.getElementById(
|
|
199
|
+
"fallbackPanelHost",
|
|
200
|
+
) as HTMLElement;
|
|
201
|
+
const configSurface = document.getElementById(
|
|
202
|
+
"configSurface",
|
|
203
|
+
) as HTMLFormElement;
|
|
142
204
|
const form = configSurface;
|
|
205
|
+
const workspaceToast = document.getElementById("workspaceToast") as HTMLElement;
|
|
206
|
+
const workspaceToastIcon = document.getElementById(
|
|
207
|
+
"workspaceToastIcon",
|
|
208
|
+
) as HTMLElement;
|
|
209
|
+
const workspaceToastMessage = document.getElementById(
|
|
210
|
+
"workspaceToastMessage",
|
|
211
|
+
) as HTMLElement;
|
|
212
|
+
let toastTimerId: number | null = null;
|
|
213
|
+
let savedAgentLabel: string | null = null;
|
|
214
|
+
let savedAgentFolder: string | null = null;
|
|
215
|
+
let agentRunTargetId: string | null = null;
|
|
143
216
|
|
|
144
217
|
const fields = {
|
|
145
218
|
name: document.getElementById("nodeName") as HTMLInputElement,
|
|
146
219
|
kind: document.getElementById("nodeKind") as HTMLSelectElement,
|
|
147
|
-
environment: document.getElementById("nodeEnvironment") as HTMLSelectElement,
|
|
148
220
|
endpoint: document.getElementById("nodeEndpoint") as HTMLInputElement,
|
|
149
221
|
token: document.getElementById("nodeToken") as HTMLInputElement,
|
|
222
|
+
model: document.getElementById("nodeModel") as HTMLSelectElement,
|
|
223
|
+
workspaceField: document.getElementById("agentWorkspaceField") as HTMLElement,
|
|
224
|
+
workspaceFolder: document.getElementById(
|
|
225
|
+
"nodeWorkspaceFolder",
|
|
226
|
+
) as HTMLInputElement,
|
|
150
227
|
timeout: document.getElementById("nodeTimeout") as HTMLInputElement,
|
|
151
|
-
|
|
152
|
-
|
|
228
|
+
description: document.getElementById(
|
|
229
|
+
"nodeDescription",
|
|
230
|
+
) as HTMLTextAreaElement,
|
|
153
231
|
typeBadge: document.getElementById("nodeTypeBadge") as HTMLElement,
|
|
154
|
-
|
|
232
|
+
llmFields: document.getElementById("llmFields") as HTMLElement,
|
|
155
233
|
mcpInventory: document.getElementById("mcpInventory") as HTMLElement,
|
|
234
|
+
agentSection: document.getElementById("agentSection") as HTMLElement,
|
|
235
|
+
systemPrompt: document.getElementById(
|
|
236
|
+
"nodeSystemPrompt",
|
|
237
|
+
) as HTMLTextAreaElement,
|
|
238
|
+
requiresUserInput: document.getElementById(
|
|
239
|
+
"nodeRequiresUserInput",
|
|
240
|
+
) as HTMLInputElement,
|
|
241
|
+
userInputDescriptionField: document.getElementById(
|
|
242
|
+
"agentUserInputDescriptionField",
|
|
243
|
+
) as HTMLElement,
|
|
244
|
+
userInputDescription: document.getElementById(
|
|
245
|
+
"nodeUserInputDescription",
|
|
246
|
+
) as HTMLTextAreaElement,
|
|
247
|
+
expectedOutputMarker: document.getElementById(
|
|
248
|
+
"nodeExpectedOutputMarker",
|
|
249
|
+
) as HTMLInputElement,
|
|
156
250
|
};
|
|
157
251
|
|
|
158
252
|
const initialEntities: Entity[] = [
|
|
159
253
|
createEntity("root", "Living AI Documentation", "system", null),
|
|
160
|
-
createEntity("devstral", "DevStral2 LLM API", "llm", "root"),
|
|
161
|
-
createEntity("qwen", "Qwen2 LLM API", "llm", "root"),
|
|
162
254
|
createEntity("mcp", "MCP", "mcp", "root"),
|
|
163
|
-
createEntity("
|
|
164
|
-
createEntity("
|
|
165
|
-
createEntity("
|
|
255
|
+
//createEntity("devstral", "DevStral2 LLM API", "llm", "root"),
|
|
256
|
+
//createEntity("qwen", "Qwen2 LLM API", "llm", "root"),
|
|
257
|
+
//createEntity("review-agent", "Code Review Agent", "agent", "devstral"),
|
|
258
|
+
//createEntity("sonar-agent", "Sonarqube Metrics Agent", "agent", "devstral"),
|
|
259
|
+
//createEntity("doc-agent", "Documentation Agent", "agent", "qwen"),
|
|
166
260
|
];
|
|
167
261
|
|
|
168
262
|
const state: WorkspaceState = {
|
|
@@ -230,17 +324,6 @@ fitButton.addEventListener("click", () => {
|
|
|
230
324
|
scheduleRender();
|
|
231
325
|
});
|
|
232
326
|
|
|
233
|
-
resetButton.addEventListener("click", () => {
|
|
234
|
-
state.entities = cloneEntities(initialEntities);
|
|
235
|
-
state.selectedId = null;
|
|
236
|
-
state.lastSaveAt = null;
|
|
237
|
-
layoutGraph();
|
|
238
|
-
resetView(true, true);
|
|
239
|
-
syncPanelFromSelection();
|
|
240
|
-
scheduleWorkspaceSave();
|
|
241
|
-
scheduleRender();
|
|
242
|
-
});
|
|
243
|
-
|
|
244
327
|
canvas.addEventListener("pointerdown", (event) => {
|
|
245
328
|
if (event.target !== canvas) {
|
|
246
329
|
return;
|
|
@@ -253,7 +336,7 @@ canvas.addEventListener("pointerdown", (event) => {
|
|
|
253
336
|
.find((entity) => isEntityHit(entity, worldPoint));
|
|
254
337
|
|
|
255
338
|
if (hitEntity) {
|
|
256
|
-
selectEntity(hitEntity.id);
|
|
339
|
+
if (hitEntity.id !== "root") selectEntity(hitEntity.id);
|
|
257
340
|
return;
|
|
258
341
|
}
|
|
259
342
|
|
|
@@ -327,20 +410,28 @@ form.addEventListener("submit", (event) => {
|
|
|
327
410
|
event.preventDefault();
|
|
328
411
|
syncSelectedFromForm();
|
|
329
412
|
state.lastSaveAt = new Date();
|
|
330
|
-
|
|
413
|
+
|
|
331
414
|
layoutGraph();
|
|
332
|
-
void saveWorkspaceNow();
|
|
415
|
+
void saveWorkspaceNow(true);
|
|
333
416
|
scheduleRender();
|
|
334
417
|
});
|
|
335
418
|
|
|
336
|
-
form.addEventListener("input", () => {
|
|
419
|
+
form.addEventListener("input", (event) => {
|
|
337
420
|
syncSelectedFromForm();
|
|
338
|
-
fields.healthBadge.textContent = "Draft";
|
|
339
421
|
layoutGraph();
|
|
340
|
-
|
|
422
|
+
// Don't auto-save while editing the name of an agent — wait for blur + popup
|
|
423
|
+
const isAgentNameEdit =
|
|
424
|
+
event.target === fields.name && selectedEntity()?.kind === "agent";
|
|
425
|
+
if (!isAgentNameEdit) {
|
|
426
|
+
scheduleWorkspaceSave();
|
|
427
|
+
}
|
|
341
428
|
scheduleRender();
|
|
342
429
|
});
|
|
343
430
|
|
|
431
|
+
closePanelButton.addEventListener("click", () => {
|
|
432
|
+
selectEntity(null);
|
|
433
|
+
});
|
|
434
|
+
|
|
344
435
|
deleteNodeButton.addEventListener("click", (event) => {
|
|
345
436
|
event.preventDefault();
|
|
346
437
|
event.stopPropagation();
|
|
@@ -367,9 +458,84 @@ confirmDeleteButton.addEventListener("click", () => {
|
|
|
367
458
|
closeDeleteConfirmation();
|
|
368
459
|
});
|
|
369
460
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
461
|
+
testNodeButton.addEventListener("click", () => {
|
|
462
|
+
const selected = selectedEntity();
|
|
463
|
+
if (selected?.kind === "agent") {
|
|
464
|
+
openAgentRunDialog();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
void testSelectedLlmConnection();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
loadModelsButton.addEventListener("click", () => {
|
|
472
|
+
void loadModelsForSelect();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
fields.name.addEventListener("blur", () => {
|
|
476
|
+
const selected = selectedEntity();
|
|
477
|
+
if (!selected || selected.kind !== "agent") return;
|
|
478
|
+
const newName = fields.name.value.trim();
|
|
479
|
+
if (
|
|
480
|
+
!newName ||
|
|
481
|
+
!savedAgentLabel ||
|
|
482
|
+
!savedAgentFolder ||
|
|
483
|
+
newName === savedAgentLabel
|
|
484
|
+
)
|
|
485
|
+
return;
|
|
486
|
+
|
|
487
|
+
const newFolder = workspaceFolderForProvider(newName);
|
|
488
|
+
if (newFolder === savedAgentFolder) return; // slug identique, pas de renommage nécessaire
|
|
489
|
+
|
|
490
|
+
renameConfirmMessage.textContent = `Renommer le dossier "${savedAgentFolder}" en "${newFolder}" ?`;
|
|
491
|
+
renameConfirmOverlay.hidden = false;
|
|
492
|
+
cancelRenameButton.focus();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
cancelRenameButton.addEventListener("click", () => {
|
|
496
|
+
renameConfirmOverlay.hidden = true;
|
|
497
|
+
const selected = selectedEntity();
|
|
498
|
+
if (selected && savedAgentLabel) {
|
|
499
|
+
selected.label = savedAgentLabel;
|
|
500
|
+
fields.name.value = savedAgentLabel;
|
|
501
|
+
layoutGraph();
|
|
502
|
+
scheduleRender();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
confirmRenameButton.addEventListener("click", () => {
|
|
507
|
+
renameConfirmOverlay.hidden = true;
|
|
508
|
+
const selected = selectedEntity();
|
|
509
|
+
if (!selected || !savedAgentFolder) return;
|
|
510
|
+
// Remettre l'ancien chemin pour que le backend sache quoi renommer
|
|
511
|
+
selected.config.workspaceFolder = savedAgentFolder;
|
|
512
|
+
fields.workspaceFolder.value = savedAgentFolder;
|
|
513
|
+
const newName = fields.name.value.trim();
|
|
514
|
+
if (newName) savedAgentLabel = newName;
|
|
515
|
+
savedAgentFolder = workspaceFolderForProvider(newName);
|
|
516
|
+
scheduleWorkspaceSave();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
fields.model.addEventListener("change", () => {
|
|
520
|
+
testNodeButton.disabled = !fields.model.value;
|
|
521
|
+
const selected = selectedEntity();
|
|
522
|
+
if (selected) {
|
|
523
|
+
selected.config.model = fields.model.value;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
cancelAgentRunButton.addEventListener("click", () => {
|
|
528
|
+
closeAgentRunDialog();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
executeAgentRunButton.addEventListener("click", () => {
|
|
532
|
+
void executeAgentRunFromDialog();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
agentRunOverlay.addEventListener("click", (event) => {
|
|
536
|
+
if (event.target === agentRunOverlay) {
|
|
537
|
+
closeAgentRunDialog();
|
|
538
|
+
}
|
|
373
539
|
});
|
|
374
540
|
|
|
375
541
|
void initializeWorkspace();
|
|
@@ -384,6 +550,8 @@ function isolatePanelEvents() {
|
|
|
384
550
|
function isolateConfirmEvents() {
|
|
385
551
|
for (const type of PANEL_EVENT_TYPES) {
|
|
386
552
|
deleteConfirmOverlay.addEventListener(type, stopPanelEventPropagation);
|
|
553
|
+
renameConfirmOverlay.addEventListener(type, stopPanelEventPropagation);
|
|
554
|
+
agentRunOverlay.addEventListener(type, stopPanelEventPropagation);
|
|
387
555
|
}
|
|
388
556
|
}
|
|
389
557
|
|
|
@@ -397,8 +565,14 @@ async function initializeWorkspace() {
|
|
|
397
565
|
state.entities = hydrateEntities(persisted.entities);
|
|
398
566
|
state.cameraX = finiteNumber(persisted.camera?.x, state.cameraX);
|
|
399
567
|
state.cameraY = finiteNumber(persisted.camera?.y, state.cameraY);
|
|
400
|
-
state.zoom = clamp(
|
|
401
|
-
|
|
568
|
+
state.zoom = clamp(
|
|
569
|
+
finiteNumber(persisted.camera?.zoom, state.zoom),
|
|
570
|
+
ZOOM_MIN,
|
|
571
|
+
ZOOM_MAX,
|
|
572
|
+
);
|
|
573
|
+
state.lastSaveAt = persisted.updatedAt
|
|
574
|
+
? new Date(persisted.updatedAt)
|
|
575
|
+
: null;
|
|
402
576
|
}
|
|
403
577
|
|
|
404
578
|
state.isHydrated = true;
|
|
@@ -417,14 +591,21 @@ function hydrateEntities(entities: unknown[]) {
|
|
|
417
591
|
const hasRoot = hydrated.some((entity) => entity.id === "root");
|
|
418
592
|
const hasMcp = hydrated.some((entity) => entity.id === "mcp");
|
|
419
593
|
const withRequiredNodes = [
|
|
420
|
-
...(hasRoot
|
|
594
|
+
...(hasRoot
|
|
595
|
+
? []
|
|
596
|
+
: [createEntity("root", "Living AI Documentation", "system", null)]),
|
|
421
597
|
...hydrated,
|
|
422
598
|
...(hasMcp ? [] : [createEntity("mcp", "MCP", "mcp", "root")]),
|
|
423
599
|
];
|
|
424
600
|
const ids = new Set(withRequiredNodes.map((entity) => entity.id));
|
|
425
601
|
|
|
426
602
|
return withRequiredNodes.map((entity) => {
|
|
427
|
-
const normalizedKind =
|
|
603
|
+
const normalizedKind =
|
|
604
|
+
entity.id === "root"
|
|
605
|
+
? "system"
|
|
606
|
+
: entity.id === "mcp"
|
|
607
|
+
? "mcp"
|
|
608
|
+
: entity.kind;
|
|
428
609
|
const normalizedParentId =
|
|
429
610
|
entity.id === "root"
|
|
430
611
|
? null
|
|
@@ -439,7 +620,12 @@ function hydrateEntities(entities: unknown[]) {
|
|
|
439
620
|
kind: normalizedKind,
|
|
440
621
|
parentId: normalizedParentId,
|
|
441
622
|
config: {
|
|
442
|
-
...createEntity(
|
|
623
|
+
...createEntity(
|
|
624
|
+
entity.id,
|
|
625
|
+
entity.label,
|
|
626
|
+
normalizedKind,
|
|
627
|
+
normalizedParentId,
|
|
628
|
+
).config,
|
|
443
629
|
...entity.config,
|
|
444
630
|
},
|
|
445
631
|
};
|
|
@@ -468,12 +654,18 @@ function hydrateEntity(input: unknown): Entity | null {
|
|
|
468
654
|
y: finiteNumber(input.y, 0),
|
|
469
655
|
angle: finiteNumber(input.angle, 0),
|
|
470
656
|
config: {
|
|
471
|
-
environment: stringValue(config.environment) || defaultEnvironment(kind),
|
|
472
657
|
endpoint: stringValue(config.endpoint) || defaultEndpoint(id, kind),
|
|
473
658
|
token: stringValue(config.token),
|
|
474
|
-
|
|
475
|
-
|
|
659
|
+
model: stringValue(config.model),
|
|
660
|
+
timeout: finiteNumber(config.timeout, 180),
|
|
476
661
|
description: stringValue(config.description) || defaultDescription(kind),
|
|
662
|
+
workspaceFolder:
|
|
663
|
+
stringValue(config.workspaceFolder) ||
|
|
664
|
+
(kind === "agent" ? workspaceFolderForProvider(label) : ""),
|
|
665
|
+
systemPrompt: stringValue(config.systemPrompt),
|
|
666
|
+
requiresUserInput: config.requiresUserInput === true,
|
|
667
|
+
userInputDescription: stringValue(config.userInputDescription),
|
|
668
|
+
expectedOutputMarker: stringValue(config.expectedOutputMarker),
|
|
477
669
|
},
|
|
478
670
|
};
|
|
479
671
|
}
|
|
@@ -487,11 +679,11 @@ function scheduleWorkspaceSave() {
|
|
|
487
679
|
}
|
|
488
680
|
state.saveTimerId = window.setTimeout(() => {
|
|
489
681
|
state.saveTimerId = null;
|
|
490
|
-
void saveWorkspaceNow();
|
|
682
|
+
void saveWorkspaceNow(false);
|
|
491
683
|
}, SAVE_DEBOUNCE_MS);
|
|
492
684
|
}
|
|
493
685
|
|
|
494
|
-
async function saveWorkspaceNow() {
|
|
686
|
+
async function saveWorkspaceNow(notify = false) {
|
|
495
687
|
if (!state.isHydrated) {
|
|
496
688
|
return;
|
|
497
689
|
}
|
|
@@ -502,11 +694,45 @@ async function saveWorkspaceNow() {
|
|
|
502
694
|
|
|
503
695
|
const saved = await saveWorkspaceState(serializeWorkspace());
|
|
504
696
|
if (saved) {
|
|
697
|
+
state.entities = hydrateEntities(saved.entities);
|
|
505
698
|
state.lastSaveAt = new Date(saved.updatedAt);
|
|
506
699
|
if (selectedEntity()) {
|
|
507
|
-
|
|
700
|
+
syncPanelFromSelection();
|
|
508
701
|
}
|
|
702
|
+
if (notify) {
|
|
703
|
+
showSaveToast("Workspace saved");
|
|
704
|
+
}
|
|
705
|
+
} else if (notify) {
|
|
706
|
+
showSaveToast("Save failed");
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function showLoadingToast(message: string) {
|
|
711
|
+
if (toastTimerId) {
|
|
712
|
+
clearTimeout(toastTimerId);
|
|
713
|
+
toastTimerId = null;
|
|
714
|
+
}
|
|
715
|
+
workspaceToast.dataset.state = "loading";
|
|
716
|
+
workspaceToastIcon.textContent = "↻";
|
|
717
|
+
workspaceToastMessage.textContent = message;
|
|
718
|
+
workspaceToast.hidden = false;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function showSaveToast(
|
|
722
|
+
message: string,
|
|
723
|
+
state: "success" | "error" = "success",
|
|
724
|
+
) {
|
|
725
|
+
if (toastTimerId) {
|
|
726
|
+
clearTimeout(toastTimerId);
|
|
509
727
|
}
|
|
728
|
+
workspaceToast.dataset.state = state;
|
|
729
|
+
workspaceToastIcon.textContent = state === "error" ? "✕" : "✓";
|
|
730
|
+
workspaceToastMessage.textContent = message;
|
|
731
|
+
workspaceToast.hidden = false;
|
|
732
|
+
toastTimerId = window.setTimeout(() => {
|
|
733
|
+
workspaceToast.hidden = true;
|
|
734
|
+
toastTimerId = null;
|
|
735
|
+
}, SAVE_TOAST_MS);
|
|
510
736
|
}
|
|
511
737
|
|
|
512
738
|
function serializeWorkspace(): PersistedWorkspaceDocument {
|
|
@@ -540,7 +766,10 @@ function stringValue(input: unknown) {
|
|
|
540
766
|
}
|
|
541
767
|
|
|
542
768
|
function nodeKindValue(input: unknown): NodeKind | null {
|
|
543
|
-
return input === "system" ||
|
|
769
|
+
return input === "system" ||
|
|
770
|
+
input === "llm" ||
|
|
771
|
+
input === "agent" ||
|
|
772
|
+
input === "mcp"
|
|
544
773
|
? input
|
|
545
774
|
: null;
|
|
546
775
|
}
|
|
@@ -559,12 +788,17 @@ function createEntity(id, label, kind, parentId) {
|
|
|
559
788
|
y: 0,
|
|
560
789
|
angle: 0,
|
|
561
790
|
config: {
|
|
562
|
-
environment: defaultEnvironment(kind),
|
|
563
791
|
endpoint: defaultEndpoint(id, kind),
|
|
564
792
|
token: "",
|
|
565
|
-
|
|
566
|
-
|
|
793
|
+
model: "",
|
|
794
|
+
timeout: 180,
|
|
567
795
|
description: defaultDescription(kind),
|
|
796
|
+
workspaceFolder:
|
|
797
|
+
kind === "agent" ? workspaceFolderForProvider(label) : "",
|
|
798
|
+
systemPrompt: "",
|
|
799
|
+
requiresUserInput: false,
|
|
800
|
+
userInputDescription: "",
|
|
801
|
+
expectedOutputMarker: "",
|
|
568
802
|
},
|
|
569
803
|
};
|
|
570
804
|
}
|
|
@@ -576,16 +810,9 @@ function cloneEntities(entities) {
|
|
|
576
810
|
}));
|
|
577
811
|
}
|
|
578
812
|
|
|
579
|
-
function defaultEnvironment(kind) {
|
|
580
|
-
if (kind === "llm" || kind === "mcp") {
|
|
581
|
-
return "production";
|
|
582
|
-
}
|
|
583
|
-
return "local";
|
|
584
|
-
}
|
|
585
|
-
|
|
586
813
|
function defaultEndpoint(id, kind) {
|
|
587
814
|
if (kind === "llm") {
|
|
588
|
-
return
|
|
815
|
+
return "http://localhost:11434";
|
|
589
816
|
}
|
|
590
817
|
if (kind === "mcp") {
|
|
591
818
|
return "http://localhost:4321/mcp";
|
|
@@ -593,6 +820,20 @@ function defaultEndpoint(id, kind) {
|
|
|
593
820
|
return "";
|
|
594
821
|
}
|
|
595
822
|
|
|
823
|
+
function workspaceFolderForProvider(label) {
|
|
824
|
+
return `AI/WORKSPACE/${slugify(label)}`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function slugify(value) {
|
|
828
|
+
const slug = value
|
|
829
|
+
.normalize("NFKD")
|
|
830
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
831
|
+
.toLowerCase()
|
|
832
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
833
|
+
.replace(/^_+|_+$/g, "");
|
|
834
|
+
return slug || "llm_provider";
|
|
835
|
+
}
|
|
836
|
+
|
|
596
837
|
function defaultDescription(kind) {
|
|
597
838
|
const copy = {
|
|
598
839
|
system:
|
|
@@ -637,8 +878,7 @@ function addProvider() {
|
|
|
637
878
|
function addAgent(parentId) {
|
|
638
879
|
const hadSelection = Boolean(state.selectedId);
|
|
639
880
|
const index =
|
|
640
|
-
childrenOf(parentId).filter((entity) => entity.kind === "agent").length +
|
|
641
|
-
1;
|
|
881
|
+
childrenOf(parentId).filter((entity) => entity.kind === "agent").length + 1;
|
|
642
882
|
const id = `agent-${Date.now().toString(36)}`;
|
|
643
883
|
const agent = createEntity(id, `Agent ${index}`, "agent", parentId);
|
|
644
884
|
state.entities = [...state.entities, agent];
|
|
@@ -821,8 +1061,7 @@ function distanceForChildren(
|
|
|
821
1061
|
includeSubtree ? subtreeRadius(child) : collisionRadius(child),
|
|
822
1062
|
),
|
|
823
1063
|
);
|
|
824
|
-
const parentClearance =
|
|
825
|
-
parentRadius + largestChildRadius + NODE_SPACING_GAP;
|
|
1064
|
+
const parentClearance = parentRadius + largestChildRadius + NODE_SPACING_GAP;
|
|
826
1065
|
|
|
827
1066
|
if (children.length === 1) {
|
|
828
1067
|
return Math.max(minimumDistance, parentClearance);
|
|
@@ -900,6 +1139,13 @@ function collisionRadius(entity) {
|
|
|
900
1139
|
function selectEntity(id) {
|
|
901
1140
|
const hadSelection = Boolean(state.selectedId);
|
|
902
1141
|
state.selectedId = id;
|
|
1142
|
+
const entity = id ? entityById(id) : null;
|
|
1143
|
+
savedAgentLabel = entity?.kind === "agent" ? entity.label : null;
|
|
1144
|
+
savedAgentFolder =
|
|
1145
|
+
entity?.kind === "agent"
|
|
1146
|
+
? entity.config.workspaceFolder ||
|
|
1147
|
+
workspaceFolderForProvider(entity.label)
|
|
1148
|
+
: null;
|
|
903
1149
|
layoutGraph();
|
|
904
1150
|
syncCameraForPanelChange(hadSelection, Boolean(id), true);
|
|
905
1151
|
syncPanelFromSelection();
|
|
@@ -952,22 +1198,39 @@ function syncPanelFromSelection() {
|
|
|
952
1198
|
|
|
953
1199
|
if (!selected) {
|
|
954
1200
|
addButton.title = "Add LLM provider";
|
|
1201
|
+
testNodeButton.textContent = "Test";
|
|
1202
|
+
testNodeButton.hidden = true;
|
|
1203
|
+
testNodeButton.disabled = true;
|
|
955
1204
|
return;
|
|
956
1205
|
}
|
|
957
1206
|
|
|
958
1207
|
fields.name.value = selected.label;
|
|
959
1208
|
fields.kind.value = selected.kind;
|
|
960
|
-
fields.environment.value = selected.config.environment;
|
|
961
1209
|
fields.endpoint.value = selected.config.endpoint;
|
|
962
1210
|
fields.token.value = selected.config.token;
|
|
1211
|
+
restoreModelSelect(selected.config.model);
|
|
1212
|
+
fields.workspaceFolder.value = selected.config.workspaceFolder;
|
|
1213
|
+
fields.workspaceField.hidden = selected.kind !== "agent";
|
|
963
1214
|
fields.timeout.value = String(selected.config.timeout);
|
|
964
|
-
fields.retryPolicy.value = selected.config.retryPolicy;
|
|
965
1215
|
fields.description.value = selected.config.description;
|
|
966
1216
|
fields.typeBadge.textContent = labelForBadge(selected.kind);
|
|
967
|
-
fields.healthBadge.textContent = state.lastSaveAt ? "Applied" : "Ready";
|
|
968
1217
|
fields.kind.disabled = true;
|
|
1218
|
+
fields.llmFields.hidden =
|
|
1219
|
+
selected.kind === "mcp" || selected.kind === "agent";
|
|
969
1220
|
fields.mcpInventory.hidden = selected.kind !== "mcp";
|
|
970
|
-
|
|
1221
|
+
fields.agentSection.hidden = selected.kind !== "agent";
|
|
1222
|
+
fields.systemPrompt.value = selected.config.systemPrompt;
|
|
1223
|
+
fields.requiresUserInput.checked = selected.config.requiresUserInput;
|
|
1224
|
+
syncUserInputDescriptionVisibility();
|
|
1225
|
+
fields.userInputDescription.value = selected.config.userInputDescription;
|
|
1226
|
+
fields.expectedOutputMarker.value = selected.config.expectedOutputMarker;
|
|
1227
|
+
deleteNodeButton.hidden = isProtectedEntity(selected.id);
|
|
1228
|
+
testNodeButton.textContent = "Test";
|
|
1229
|
+
testNodeButton.hidden = selected.kind !== "llm" && selected.kind !== "agent";
|
|
1230
|
+
testNodeButton.disabled =
|
|
1231
|
+
selected.kind === "llm"
|
|
1232
|
+
? !selected.config.model
|
|
1233
|
+
: selected.kind !== "agent";
|
|
971
1234
|
addButton.title =
|
|
972
1235
|
selected.kind === "llm" || entityById(selected.parentId)?.kind === "llm"
|
|
973
1236
|
? "Add agent"
|
|
@@ -982,13 +1245,232 @@ function syncSelectedFromForm() {
|
|
|
982
1245
|
|
|
983
1246
|
selected.label = fields.name.value.trim() || "Untitled node";
|
|
984
1247
|
selected.kind = fields.kind.value as NodeKind;
|
|
985
|
-
selected.config.environment = fields.environment.value;
|
|
986
1248
|
selected.config.endpoint = fields.endpoint.value;
|
|
987
1249
|
selected.config.token = fields.token.value;
|
|
1250
|
+
selected.config.model = fields.model.value;
|
|
988
1251
|
selected.config.timeout = Number(fields.timeout.value || 30);
|
|
989
|
-
selected.config.retryPolicy = fields.retryPolicy.value;
|
|
990
1252
|
selected.config.description = fields.description.value;
|
|
1253
|
+
selected.config.systemPrompt = fields.systemPrompt.value;
|
|
1254
|
+
selected.config.requiresUserInput = fields.requiresUserInput.checked;
|
|
1255
|
+
selected.config.userInputDescription = fields.userInputDescription.value;
|
|
1256
|
+
selected.config.expectedOutputMarker = fields.expectedOutputMarker.value;
|
|
1257
|
+
syncUserInputDescriptionVisibility();
|
|
991
1258
|
fields.typeBadge.textContent = labelForBadge(selected.kind);
|
|
1259
|
+
|
|
1260
|
+
// Propagate model + timeout to child agents
|
|
1261
|
+
if (selected.kind === "llm") {
|
|
1262
|
+
for (const entity of state.entities) {
|
|
1263
|
+
if (entity.parentId === selected.id && entity.kind === "agent") {
|
|
1264
|
+
entity.config.model = selected.config.model;
|
|
1265
|
+
entity.config.timeout = selected.config.timeout;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function syncUserInputDescriptionVisibility() {
|
|
1272
|
+
const shouldShow =
|
|
1273
|
+
selectedEntity()?.kind === "agent" && fields.requiresUserInput.checked;
|
|
1274
|
+
fields.userInputDescriptionField.hidden = !shouldShow;
|
|
1275
|
+
fields.userInputDescription.required = shouldShow;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function restoreModelSelect(savedModel: string) {
|
|
1279
|
+
const existing = Array.from(fields.model.options).map((o) => o.value);
|
|
1280
|
+
if (savedModel && !existing.includes(savedModel)) {
|
|
1281
|
+
fields.model.innerHTML = "";
|
|
1282
|
+
const opt = document.createElement("option");
|
|
1283
|
+
opt.value = savedModel;
|
|
1284
|
+
opt.textContent = savedModel;
|
|
1285
|
+
fields.model.appendChild(opt);
|
|
1286
|
+
}
|
|
1287
|
+
fields.model.value = savedModel || (existing[0] ?? "");
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function openAgentRunDialog() {
|
|
1291
|
+
const selected = selectedEntity();
|
|
1292
|
+
if (!selected || selected.kind !== "agent") return;
|
|
1293
|
+
|
|
1294
|
+
syncSelectedFromForm();
|
|
1295
|
+
|
|
1296
|
+
const llm = entityById(selected.parentId ?? "");
|
|
1297
|
+
if (!llm || llm.kind !== "llm") {
|
|
1298
|
+
showSaveToast("No parent LLM found for this agent.", "error");
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (!llm.config.model) {
|
|
1302
|
+
showSaveToast("Parent LLM has no model selected.", "error");
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (!selected.config.systemPrompt.trim()) {
|
|
1306
|
+
showSaveToast("Write a system prompt first.", "error");
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (
|
|
1310
|
+
selected.config.requiresUserInput &&
|
|
1311
|
+
!selected.config.userInputDescription.trim()
|
|
1312
|
+
) {
|
|
1313
|
+
showSaveToast("Describe the required user input first.", "error");
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
agentRunTargetId = selected.id;
|
|
1318
|
+
agentRunTitle.textContent = `Test ${selected.label}`;
|
|
1319
|
+
agentRunHint.textContent = selected.config.requiresUserInput
|
|
1320
|
+
? selected.config.userInputDescription
|
|
1321
|
+
: "Run this agent with its configured system prompt.";
|
|
1322
|
+
agentRunInputField.hidden = !selected.config.requiresUserInput;
|
|
1323
|
+
agentRunInput.required = selected.config.requiresUserInput;
|
|
1324
|
+
agentRunInput.value = "";
|
|
1325
|
+
agentRunResult.hidden = true;
|
|
1326
|
+
agentRunResult.textContent = "";
|
|
1327
|
+
executeAgentRunButton.disabled = false;
|
|
1328
|
+
executeAgentRunButton.textContent = "Run";
|
|
1329
|
+
agentRunOverlay.hidden = false;
|
|
1330
|
+
if (selected.config.requiresUserInput) {
|
|
1331
|
+
window.setTimeout(() => agentRunInput.focus(), 0);
|
|
1332
|
+
} else {
|
|
1333
|
+
window.setTimeout(() => executeAgentRunButton.focus(), 0);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function closeAgentRunDialog() {
|
|
1338
|
+
agentRunOverlay.hidden = true;
|
|
1339
|
+
agentRunTargetId = null;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
async function executeAgentRunFromDialog() {
|
|
1343
|
+
if (agentRunInput.required && !agentRunInput.value.trim()) {
|
|
1344
|
+
agentRunInput.reportValidity();
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const selected = agentRunTargetId ? entityById(agentRunTargetId) : null;
|
|
1349
|
+
if (!selected || selected.kind !== "agent") {
|
|
1350
|
+
showAgentRunResult("error", "The selected agent is no longer available.");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const llm = entityById(selected.parentId ?? "");
|
|
1355
|
+
if (!llm || llm.kind !== "llm") {
|
|
1356
|
+
showAgentRunResult("error", "No parent LLM found for this agent.");
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
executeAgentRunButton.disabled = true;
|
|
1361
|
+
executeAgentRunButton.textContent = "Running…";
|
|
1362
|
+
showAgentRunResult("loading", "Running agent…");
|
|
1363
|
+
|
|
1364
|
+
const mcpEntity = state.entities.find((e) => e.kind === "mcp");
|
|
1365
|
+
|
|
1366
|
+
const result = await runAgentPrompt({
|
|
1367
|
+
endpoint: llm.config.endpoint,
|
|
1368
|
+
token: llm.config.token,
|
|
1369
|
+
model: llm.config.model,
|
|
1370
|
+
systemPrompt: selected.config.systemPrompt,
|
|
1371
|
+
userInput: agentRunInput.value.trim() || undefined,
|
|
1372
|
+
timeout: llm.config.timeout,
|
|
1373
|
+
mcpEndpoint: mcpEntity?.config.endpoint || undefined,
|
|
1374
|
+
expectedOutputMarker: selected.config.expectedOutputMarker || undefined,
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
executeAgentRunButton.disabled = false;
|
|
1378
|
+
executeAgentRunButton.textContent = "Run";
|
|
1379
|
+
|
|
1380
|
+
if (result.ok && result.content) {
|
|
1381
|
+
showAgentRunResult("success", result.content);
|
|
1382
|
+
showSaveToast("Agent responded.", "success");
|
|
1383
|
+
} else {
|
|
1384
|
+
showAgentRunResult("error", result.error ?? "Agent failed.");
|
|
1385
|
+
showSaveToast(result.error ?? "Agent failed.", "error");
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function showAgentRunResult(
|
|
1390
|
+
state: "loading" | "success" | "error",
|
|
1391
|
+
message: string,
|
|
1392
|
+
) {
|
|
1393
|
+
agentRunResult.hidden = false;
|
|
1394
|
+
agentRunResult.dataset.state = state;
|
|
1395
|
+
agentRunResult.textContent = message;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
async function loadModelsForSelect() {
|
|
1399
|
+
const selected = selectedEntity();
|
|
1400
|
+
if (!selected || selected.kind !== "llm") return;
|
|
1401
|
+
|
|
1402
|
+
syncSelectedFromForm();
|
|
1403
|
+
const endpoint = selected.config.endpoint;
|
|
1404
|
+
if (!endpoint) {
|
|
1405
|
+
showSaveToast("Set an endpoint first.");
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
loadModelsButton.classList.add("loading");
|
|
1410
|
+
loadModelsButton.disabled = true;
|
|
1411
|
+
fields.model.disabled = true;
|
|
1412
|
+
|
|
1413
|
+
const result = await listLlmModels({
|
|
1414
|
+
endpoint,
|
|
1415
|
+
token: selected.config.token,
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
loadModelsButton.classList.remove("loading");
|
|
1419
|
+
loadModelsButton.disabled = false;
|
|
1420
|
+
fields.model.disabled = false;
|
|
1421
|
+
|
|
1422
|
+
if (!result.ok || !result.models?.length) {
|
|
1423
|
+
showSaveToast(result.error ?? "No models found.");
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const previousValue = fields.model.value;
|
|
1428
|
+
fields.model.innerHTML = "";
|
|
1429
|
+
for (const id of result.models) {
|
|
1430
|
+
const opt = document.createElement("option");
|
|
1431
|
+
opt.value = id;
|
|
1432
|
+
opt.textContent = id;
|
|
1433
|
+
fields.model.appendChild(opt);
|
|
1434
|
+
}
|
|
1435
|
+
fields.model.value = result.models.includes(previousValue)
|
|
1436
|
+
? previousValue
|
|
1437
|
+
: result.models[0];
|
|
1438
|
+
selected.config.model = fields.model.value;
|
|
1439
|
+
testNodeButton.disabled = !fields.model.value;
|
|
1440
|
+
showSaveToast(
|
|
1441
|
+
`${result.models.length} model${result.models.length === 1 ? "" : "s"} loaded.`,
|
|
1442
|
+
);
|
|
1443
|
+
scheduleRender();
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function testSelectedLlmConnection() {
|
|
1447
|
+
const selected = selectedEntity();
|
|
1448
|
+
if (!selected || selected.kind !== "llm") {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
syncSelectedFromForm();
|
|
1453
|
+
testNodeButton.disabled = true;
|
|
1454
|
+
showLoadingToast(`Testing ${selected.config.model}…`);
|
|
1455
|
+
scheduleRender();
|
|
1456
|
+
|
|
1457
|
+
const result = await testLlmConnection({
|
|
1458
|
+
endpoint: selected.config.endpoint,
|
|
1459
|
+
token: selected.config.token,
|
|
1460
|
+
model: selected.config.model,
|
|
1461
|
+
timeout: selected.config.timeout,
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
testNodeButton.disabled = false;
|
|
1465
|
+
if (result.ok) {
|
|
1466
|
+
showSaveToast(result.detail ?? "Connection OK", "success");
|
|
1467
|
+
} else {
|
|
1468
|
+
showSaveToast(
|
|
1469
|
+
result.error || `Connection failed (${result.status || "no status"})`,
|
|
1470
|
+
"error",
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
scheduleRender();
|
|
992
1474
|
}
|
|
993
1475
|
|
|
994
1476
|
function labelForBadge(kind) {
|
|
@@ -1208,7 +1690,7 @@ function drawConfigSurface(width, height) {
|
|
|
1208
1690
|
}
|
|
1209
1691
|
|
|
1210
1692
|
const panelWidth = panelWidthForViewport(width);
|
|
1211
|
-
const panelHeight = Math.min(PANEL_HEIGHT, height -
|
|
1693
|
+
const panelHeight = Math.min(PANEL_HEIGHT, height - 118);
|
|
1212
1694
|
const panelX = width - panelWidth - 24;
|
|
1213
1695
|
const panelY = Math.max(24, (height - panelHeight) / 2);
|
|
1214
1696
|
|
|
@@ -1256,9 +1738,25 @@ function resetView(animate, resetZoom) {
|
|
|
1256
1738
|
|
|
1257
1739
|
function syncCameraForPanelChange(hadSelection, hasSelection, animate) {
|
|
1258
1740
|
if (hasSelection) {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1741
|
+
if (!hadSelection) {
|
|
1742
|
+
// Panel was closed → center selected node in the left area
|
|
1743
|
+
const selected = selectedEntity();
|
|
1744
|
+
if (selected) {
|
|
1745
|
+
const panelReserve =
|
|
1746
|
+
panelWidthForViewport(canvas.clientWidth) + PANEL_GAP;
|
|
1747
|
+
const availableWidth = canvas.clientWidth - panelReserve;
|
|
1748
|
+
const targetX = availableWidth / 2 - selected.x * state.zoom;
|
|
1749
|
+
const appliedDelta = targetX - state.cameraX;
|
|
1750
|
+
// Store negative delta so deselect reverses the shift
|
|
1751
|
+
state.panelShiftX = -appliedDelta;
|
|
1752
|
+
startViewAnimation(targetX, state.cameraY, state.zoom);
|
|
1753
|
+
}
|
|
1754
|
+
} else {
|
|
1755
|
+
// Panel already open → avoid occlusion only if needed
|
|
1756
|
+
const deltaX = selectedPanelAvoidanceDelta();
|
|
1757
|
+
state.panelShiftX += Math.max(0, -deltaX);
|
|
1758
|
+
translateCamera(deltaX, animate);
|
|
1759
|
+
}
|
|
1262
1760
|
return;
|
|
1263
1761
|
}
|
|
1264
1762
|
|
|
@@ -1277,7 +1775,8 @@ function selectedPanelAvoidanceDelta() {
|
|
|
1277
1775
|
return 0;
|
|
1278
1776
|
}
|
|
1279
1777
|
|
|
1280
|
-
const panelLeft =
|
|
1778
|
+
const panelLeft =
|
|
1779
|
+
canvas.clientWidth - panelWidthForViewport(canvas.clientWidth) - 24;
|
|
1281
1780
|
const safeRight = panelLeft - PANEL_OCCLUSION_PADDING;
|
|
1282
1781
|
const selectedBounds = screenBoundsForEntity(selected);
|
|
1283
1782
|
|
|
@@ -1488,8 +1987,7 @@ function isEntityHit(entity, point) {
|
|
|
1488
1987
|
if (entity.kind === "system" || entity.kind === "llm") {
|
|
1489
1988
|
const radius = entity.kind === "system" ? ROOT_RADIUS : PROVIDER_RADIUS;
|
|
1490
1989
|
return (
|
|
1491
|
-
Math.hypot(point.x - entity.x, point.y - entity.y) <=
|
|
1492
|
-
radius + HIT_PADDING
|
|
1990
|
+
Math.hypot(point.x - entity.x, point.y - entity.y) <= radius + HIT_PADDING
|
|
1493
1991
|
);
|
|
1494
1992
|
}
|
|
1495
1993
|
|