@intranefr/superbackend 1.4.4 → 1.5.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/index.js +16 -1
- package/package.json +5 -2
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/controllers/admin.controller.js +89 -0
- package/src/controllers/adminHeadless.controller.js +82 -0
- package/src/controllers/adminScripts.controller.js +229 -0
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/orgAdmin.controller.js +286 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware.js +115 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminHeadless.routes.js +6 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +29 -0
- package/src/routes/llmUi.routes.js +26 -0
- package/src/routes/orgAdmin.routes.js +5 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +312 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-organizations.ejs +365 -9
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +709 -0
- package/views/admin-users.ejs +261 -4
- package/views/partials/dashboard/nav-items.ejs +3 -0
package/views/admin-headless.ejs
CHANGED
|
@@ -44,7 +44,10 @@
|
|
|
44
44
|
<div class="lg:col-span-1">
|
|
45
45
|
<div class="flex items-center justify-between mb-3">
|
|
46
46
|
<h2 class="text-lg font-semibold">Models</h2>
|
|
47
|
-
<
|
|
47
|
+
<div class="flex gap-2">
|
|
48
|
+
<button id="btn-import-external" class="bg-white border px-3 py-2 rounded hover:bg-gray-100 text-sm">Import from Mongo</button>
|
|
49
|
+
<button id="btn-new-model" class="bg-green-500 text-white px-3 py-2 rounded hover:bg-green-600 text-sm">New</button>
|
|
50
|
+
</div>
|
|
48
51
|
</div>
|
|
49
52
|
<div id="models-list" class="space-y-2">
|
|
50
53
|
<div class="text-gray-500 text-sm">Loading…</div>
|
|
@@ -58,6 +61,7 @@
|
|
|
58
61
|
<div id="selected-model-title" class="text-xl font-semibold">None</div>
|
|
59
62
|
</div>
|
|
60
63
|
<div class="flex gap-2">
|
|
64
|
+
<button id="btn-sync-external" class="bg-white border px-3 py-2 rounded hover:bg-gray-100 text-sm" disabled>Sync/Infer</button>
|
|
61
65
|
<button id="btn-save-model" class="bg-blue-600 text-white px-3 py-2 rounded hover:bg-blue-700 text-sm" disabled>Save schema</button>
|
|
62
66
|
<button id="btn-delete-model" class="bg-red-500 text-white px-3 py-2 rounded hover:bg-red-600 text-sm" disabled>Disable</button>
|
|
63
67
|
</div>
|
|
@@ -89,7 +93,7 @@
|
|
|
89
93
|
<div>
|
|
90
94
|
<label class="block text-sm font-medium mb-1">Code identifier</label>
|
|
91
95
|
<input id="model-code" class="border rounded px-3 py-2 w-full mono" placeholder="e.g. products" disabled />
|
|
92
|
-
<div class="text-xs text-gray-500 mt-1">Stored as Mongo collection <span class="mono">headless_<code></span></div>
|
|
96
|
+
<div class="text-xs text-gray-500 mt-1" id="model-collection-hint">Stored as Mongo collection <span class="mono">headless_<code></span></div>
|
|
93
97
|
</div>
|
|
94
98
|
</div>
|
|
95
99
|
|
|
@@ -435,6 +439,57 @@
|
|
|
435
439
|
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
436
440
|
</div>
|
|
437
441
|
|
|
442
|
+
<div id="modal-import-external" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
|
443
|
+
<div class="bg-white rounded-lg shadow-lg w-full max-w-2xl mx-4">
|
|
444
|
+
<div class="px-6 py-4 border-b flex items-center justify-between">
|
|
445
|
+
<div>
|
|
446
|
+
<div class="text-lg font-semibold">Import from Mongo</div>
|
|
447
|
+
<div class="text-xs text-gray-500">Creates an external model with code identifier prefix <span class="mono">ext_</span>. Schema is inferred and read-only.</div>
|
|
448
|
+
</div>
|
|
449
|
+
<button id="btn-import-close" class="text-gray-500 hover:text-gray-800" type="button">Close</button>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div class="px-6 py-4 space-y-4">
|
|
453
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
454
|
+
<div>
|
|
455
|
+
<label class="block text-sm font-medium mb-1">Mongo collection</label>
|
|
456
|
+
<select id="import-collection" class="border rounded px-3 py-2 w-full"></select>
|
|
457
|
+
<div class="text-xs text-gray-500 mt-1">Lists collections from the connected MongoDB.</div>
|
|
458
|
+
</div>
|
|
459
|
+
<div>
|
|
460
|
+
<label class="block text-sm font-medium mb-1">Sample size</label>
|
|
461
|
+
<input id="import-sampleSize" type="number" class="border rounded px-3 py-2 w-full" value="200" />
|
|
462
|
+
<div class="text-xs text-gray-500 mt-1">Max 1000.</div>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
467
|
+
<div>
|
|
468
|
+
<label class="block text-sm font-medium mb-1">Model code identifier</label>
|
|
469
|
+
<input id="import-code" class="border rounded px-3 py-2 w-full mono" placeholder="ext_users" />
|
|
470
|
+
<div class="text-xs text-gray-500 mt-1">Must start with <span class="mono">ext_</span>.</div>
|
|
471
|
+
</div>
|
|
472
|
+
<div>
|
|
473
|
+
<label class="block text-sm font-medium mb-1">Display name</label>
|
|
474
|
+
<input id="import-displayName" class="border rounded px-3 py-2 w-full" placeholder="Users" />
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<div class="flex items-center justify-between">
|
|
479
|
+
<div class="text-sm text-gray-600">Inference preview</div>
|
|
480
|
+
<div class="flex gap-2">
|
|
481
|
+
<button id="btn-import-infer" class="bg-white border px-3 py-2 rounded hover:bg-gray-100 text-sm" type="button">Infer preview</button>
|
|
482
|
+
<button id="btn-import-run" class="bg-blue-600 text-white px-3 py-2 rounded hover:bg-blue-700 text-sm" type="button">Import</button>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<div id="import-preview" class="hidden">
|
|
487
|
+
<pre id="import-preview-pre" class="mono text-xs bg-gray-50 border rounded p-3 overflow-x-auto overflow-y-auto max-h-64"></pre>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
438
493
|
<script>
|
|
439
494
|
const baseUrl = '<%= baseUrl %>';
|
|
440
495
|
const adminPath = '<%= adminPath %>';
|
|
@@ -520,6 +575,115 @@
|
|
|
520
575
|
}
|
|
521
576
|
}
|
|
522
577
|
|
|
578
|
+
function setImportModalOpen(open) {
|
|
579
|
+
const modal = document.getElementById('modal-import-external');
|
|
580
|
+
if (!modal) return;
|
|
581
|
+
modal.classList.toggle('hidden', !open);
|
|
582
|
+
modal.classList.toggle('flex', open);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function setImportPreview(value) {
|
|
586
|
+
const container = document.getElementById('import-preview');
|
|
587
|
+
const pre = document.getElementById('import-preview-pre');
|
|
588
|
+
if (!container || !pre) return;
|
|
589
|
+
if (!value) {
|
|
590
|
+
container.classList.add('hidden');
|
|
591
|
+
pre.textContent = '';
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
container.classList.remove('hidden');
|
|
595
|
+
pre.textContent = JSON.stringify(value, null, 2);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function normalizeExtCodeFromCollectionName(collectionName) {
|
|
599
|
+
const base = String(collectionName || '')
|
|
600
|
+
.trim()
|
|
601
|
+
.toLowerCase()
|
|
602
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
603
|
+
.replace(/^_+|_+$/g, '');
|
|
604
|
+
const safe = base && /^[a-z][a-z0-9_]*$/.test(base) ? base : 'model';
|
|
605
|
+
return `ext_${safe}`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function loadExternalCollectionsIntoModal() {
|
|
609
|
+
const select = document.getElementById('import-collection');
|
|
610
|
+
if (!select) return;
|
|
611
|
+
select.innerHTML = '';
|
|
612
|
+
const { items } = await api('/api/admin/headless/external/collections');
|
|
613
|
+
const cols = Array.isArray(items) ? items : [];
|
|
614
|
+
cols.forEach((c) => {
|
|
615
|
+
const name = String(c && c.name ? c.name : '').trim();
|
|
616
|
+
if (!name) return;
|
|
617
|
+
const opt = document.createElement('option');
|
|
618
|
+
opt.value = name;
|
|
619
|
+
opt.textContent = name;
|
|
620
|
+
select.appendChild(opt);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const selected = select.value;
|
|
624
|
+
const codeInput = document.getElementById('import-code');
|
|
625
|
+
const dnInput = document.getElementById('import-displayName');
|
|
626
|
+
if (codeInput && selected) codeInput.value = normalizeExtCodeFromCollectionName(selected);
|
|
627
|
+
if (dnInput && selected) dnInput.value = String(selected).replace(/_/g, ' ');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function importInferPreview() {
|
|
631
|
+
const collectionName = document.getElementById('import-collection')?.value;
|
|
632
|
+
const sampleSize = Number(document.getElementById('import-sampleSize')?.value || 200) || 200;
|
|
633
|
+
if (!collectionName) {
|
|
634
|
+
toast('Select a collection', 'error');
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const res = await api('/api/admin/headless/external/infer', {
|
|
638
|
+
method: 'POST',
|
|
639
|
+
body: JSON.stringify({ collectionName, sampleSize }),
|
|
640
|
+
});
|
|
641
|
+
setImportPreview(res);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function importRun() {
|
|
645
|
+
const collectionName = document.getElementById('import-collection')?.value;
|
|
646
|
+
const sampleSize = Number(document.getElementById('import-sampleSize')?.value || 200) || 200;
|
|
647
|
+
const codeIdentifier = String(document.getElementById('import-code')?.value || '').trim();
|
|
648
|
+
const displayName = String(document.getElementById('import-displayName')?.value || '').trim();
|
|
649
|
+
if (!collectionName) {
|
|
650
|
+
toast('Select a collection', 'error');
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (!codeIdentifier || !codeIdentifier.startsWith('ext_')) {
|
|
654
|
+
toast('codeIdentifier must start with ext_', 'error');
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const res = await api('/api/admin/headless/external/import', {
|
|
658
|
+
method: 'POST',
|
|
659
|
+
body: JSON.stringify({ collectionName, codeIdentifier, displayName, sampleSize }),
|
|
660
|
+
});
|
|
661
|
+
toast('Imported', 'success');
|
|
662
|
+
setImportModalOpen(false);
|
|
663
|
+
setImportPreview(null);
|
|
664
|
+
await loadModels();
|
|
665
|
+
if (res && res.item && res.item.codeIdentifier) {
|
|
666
|
+
await selectModel(res.item.codeIdentifier);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function syncExternalSelectedModel() {
|
|
671
|
+
const m = state.selectedModel;
|
|
672
|
+
if (!m) return;
|
|
673
|
+
const isExternal = m && (m.sourceType === 'external' || m.isExternal === true);
|
|
674
|
+
if (!isExternal) return;
|
|
675
|
+
const sampleSize = Number(prompt('Sample size for inference (max 1000):', '200') || '200');
|
|
676
|
+
const res = await api(`/api/admin/headless/models/${encodeURIComponent(m.codeIdentifier)}/sync`, {
|
|
677
|
+
method: 'POST',
|
|
678
|
+
body: JSON.stringify({ sampleSize }),
|
|
679
|
+
});
|
|
680
|
+
toast('Synced', 'success');
|
|
681
|
+
await loadModels();
|
|
682
|
+
if (res && res.item && res.item.codeIdentifier) {
|
|
683
|
+
await selectModel(res.item.codeIdentifier);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
523
687
|
async function advancedSave() {
|
|
524
688
|
try {
|
|
525
689
|
const v = await advancedValidate();
|
|
@@ -758,8 +922,15 @@
|
|
|
758
922
|
state.models.forEach((m) => {
|
|
759
923
|
const btn = document.createElement('button');
|
|
760
924
|
const active = state.selectedModel && state.selectedModel.codeIdentifier === m.codeIdentifier;
|
|
925
|
+
const isExternal = m && (m.sourceType === 'external' || m.isExternal === true);
|
|
761
926
|
btn.className = `w-full text-left px-3 py-2 rounded border ${active ? 'bg-blue-50 border-blue-200' : 'bg-white border-gray-200 hover:bg-gray-50'}`;
|
|
762
|
-
|
|
927
|
+
const label = isExternal
|
|
928
|
+
? `<div class="flex items-center justify-between gap-2"><div class="font-medium">${m.displayName}</div><span class="text-[10px] uppercase bg-gray-200 text-gray-800 px-2 py-0.5 rounded">External</span></div>`
|
|
929
|
+
: `<div class="font-medium">${m.displayName}</div>`;
|
|
930
|
+
const sub = isExternal && m.sourceCollectionName
|
|
931
|
+
? `<div class="text-xs text-gray-500 mono">${m.codeIdentifier} · ${String(m.sourceCollectionName).replace(/</g,'<')}</div>`
|
|
932
|
+
: `<div class="text-xs text-gray-500 mono">${m.codeIdentifier}</div>`;
|
|
933
|
+
btn.innerHTML = `${label}${sub}`;
|
|
763
934
|
btn.addEventListener('click', () => selectModel(m.codeIdentifier));
|
|
764
935
|
list.appendChild(btn);
|
|
765
936
|
});
|
|
@@ -771,6 +942,7 @@
|
|
|
771
942
|
document.getElementById('btn-add-field').disabled = !enabled;
|
|
772
943
|
document.getElementById('btn-save-model').disabled = !enabled;
|
|
773
944
|
document.getElementById('btn-delete-model').disabled = !enabled || isNew;
|
|
945
|
+
document.getElementById('btn-sync-external').disabled = true;
|
|
774
946
|
document.getElementById('btn-mode-simple').disabled = !enabled;
|
|
775
947
|
document.getElementById('btn-mode-advanced').disabled = !enabled;
|
|
776
948
|
document.getElementById('btn-mode-ai').disabled = !enabled;
|
|
@@ -786,6 +958,34 @@
|
|
|
786
958
|
// Data tab controls are managed separately
|
|
787
959
|
}
|
|
788
960
|
|
|
961
|
+
function setModelFormEnabledForModel(model) {
|
|
962
|
+
if (!model) {
|
|
963
|
+
setModelFormEnabled(false);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const isNew = Boolean(model.__isNew);
|
|
968
|
+
const isExternal = model && (model.sourceType === 'external' || model.isExternal === true);
|
|
969
|
+
|
|
970
|
+
setModelFormEnabled(true, isNew);
|
|
971
|
+
|
|
972
|
+
if (isExternal) {
|
|
973
|
+
document.getElementById('btn-save-model').disabled = true;
|
|
974
|
+
document.getElementById('btn-add-field').disabled = true;
|
|
975
|
+
document.getElementById('btn-mode-advanced').disabled = true;
|
|
976
|
+
document.getElementById('btn-mode-ai').disabled = true;
|
|
977
|
+
document.getElementById('btn-advanced-load').disabled = true;
|
|
978
|
+
document.getElementById('btn-advanced-validate').disabled = true;
|
|
979
|
+
document.getElementById('btn-advanced-save').disabled = true;
|
|
980
|
+
document.getElementById('ai-input').disabled = true;
|
|
981
|
+
document.getElementById('btn-ai-send').disabled = true;
|
|
982
|
+
document.getElementById('btn-ai-clear').disabled = true;
|
|
983
|
+
document.getElementById('btn-ai-apply').disabled = true;
|
|
984
|
+
document.getElementById('btn-sync-external').disabled = false;
|
|
985
|
+
if (state.modelUi.mode !== 'simple') setModelMode('simple');
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
789
989
|
function setModelMode(mode) {
|
|
790
990
|
state.modelUi.mode = mode;
|
|
791
991
|
document.getElementById('panel-simple').classList.toggle('hidden', mode !== 'simple');
|
|
@@ -856,21 +1056,32 @@
|
|
|
856
1056
|
const title = document.getElementById('selected-model-title');
|
|
857
1057
|
const codeInput = document.getElementById('model-code');
|
|
858
1058
|
const nameInput = document.getElementById('model-displayName');
|
|
1059
|
+
const hint = document.getElementById('model-collection-hint');
|
|
859
1060
|
|
|
860
1061
|
const m = state.selectedModel;
|
|
861
1062
|
if (!m) {
|
|
862
1063
|
title.textContent = 'None';
|
|
863
1064
|
codeInput.value = '';
|
|
864
1065
|
nameInput.value = '';
|
|
865
|
-
|
|
1066
|
+
if (hint) hint.innerHTML = 'Stored as Mongo collection <span class="mono">headless_<code></span>';
|
|
1067
|
+
setModelFormEnabledForModel(null);
|
|
866
1068
|
renderFieldsTable();
|
|
867
1069
|
return;
|
|
868
1070
|
}
|
|
869
1071
|
|
|
1072
|
+
const isExternal = m && (m.sourceType === 'external' || m.isExternal === true);
|
|
870
1073
|
title.textContent = m.displayName;
|
|
871
1074
|
codeInput.value = m.codeIdentifier;
|
|
872
1075
|
nameInput.value = m.displayName;
|
|
873
|
-
|
|
1076
|
+
if (hint) {
|
|
1077
|
+
if (isExternal && m.sourceCollectionName) {
|
|
1078
|
+
hint.innerHTML = `External Mongo collection <span class="mono">${String(m.sourceCollectionName).replace(/</g,'<')}</span>`;
|
|
1079
|
+
} else {
|
|
1080
|
+
hint.innerHTML = 'Stored as Mongo collection <span class="mono">headless_<code></span>';
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
setModelFormEnabledForModel(m);
|
|
874
1085
|
renderFieldsTable();
|
|
875
1086
|
}
|
|
876
1087
|
|
|
@@ -891,6 +1102,8 @@
|
|
|
891
1102
|
|
|
892
1103
|
tbody.innerHTML = '';
|
|
893
1104
|
|
|
1105
|
+
const isExternal = m && (m.sourceType === 'external' || m.isExternal === true);
|
|
1106
|
+
|
|
894
1107
|
fields.forEach((f, idx) => {
|
|
895
1108
|
const tr = document.createElement('tr');
|
|
896
1109
|
tr.className = 'border-t';
|
|
@@ -899,32 +1112,36 @@
|
|
|
899
1112
|
const opts = typeOptions.map((t) => `<option value="${t}" ${String(f.type).toLowerCase()===t?'selected':''}>${t}</option>`).join('');
|
|
900
1113
|
|
|
901
1114
|
tr.innerHTML = `
|
|
902
|
-
<td class="py-2 pr-3"><input class="field-name border rounded px-2 py-1 w-40 mono" value="${f.name || ''}" data-idx="${idx}" /></td>
|
|
903
|
-
<td class="py-2 pr-3"><select class="field-type border rounded px-2 py-1" data-idx="${idx}">${opts}</select></td>
|
|
904
|
-
<td class="py-2 pr-3"><input type="checkbox" class="field-required" data-idx="${idx}" ${f.required?'checked':''} /></td>
|
|
905
|
-
<td class="py-2 pr-3"><input type="checkbox" class="field-unique" data-idx="${idx}" ${f.unique?'checked':''} /></td>
|
|
906
|
-
<td class="py-2 pr-3"><input class="field-default border rounded px-2 py-1 w-40" value="${f.default===undefined?'':String(f.default)}" data-idx="${idx}" placeholder="(optional)" /></td>
|
|
907
|
-
<td class="py-2 pr-3"><input class="field-ref border rounded px-2 py-1 w-40 mono" value="${f.refModelCode||''}" data-idx="${idx}" placeholder="ref model code" /></td>
|
|
908
|
-
<td class="py-2 pr-3 text-right"
|
|
1115
|
+
<td class="py-2 pr-3"><input class="field-name border rounded px-2 py-1 w-40 mono" value="${f.name || ''}" data-idx="${idx}" ${isExternal ? 'disabled' : ''} /></td>
|
|
1116
|
+
<td class="py-2 pr-3"><select class="field-type border rounded px-2 py-1" data-idx="${idx}" ${isExternal ? 'disabled' : ''}>${opts}</select></td>
|
|
1117
|
+
<td class="py-2 pr-3"><input type="checkbox" class="field-required" data-idx="${idx}" ${f.required?'checked':''} ${isExternal ? 'disabled' : ''} /></td>
|
|
1118
|
+
<td class="py-2 pr-3"><input type="checkbox" class="field-unique" data-idx="${idx}" ${f.unique?'checked':''} ${isExternal ? 'disabled' : ''} /></td>
|
|
1119
|
+
<td class="py-2 pr-3"><input class="field-default border rounded px-2 py-1 w-40" value="${f.default===undefined?'':String(f.default)}" data-idx="${idx}" placeholder="(optional)" ${isExternal ? 'disabled' : ''} /></td>
|
|
1120
|
+
<td class="py-2 pr-3"><input class="field-ref border rounded px-2 py-1 w-40 mono" value="${f.refModelCode||''}" data-idx="${idx}" placeholder="ref model code" ${isExternal ? 'disabled' : ''} /></td>
|
|
1121
|
+
<td class="py-2 pr-3 text-right">${isExternal ? '' : `<button class="btn-remove-field text-red-600 hover:underline" data-idx="${idx}">Remove</button>`}</td>
|
|
909
1122
|
`;
|
|
910
1123
|
|
|
911
1124
|
tbody.appendChild(tr);
|
|
912
1125
|
});
|
|
913
1126
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1127
|
+
if (!isExternal) {
|
|
1128
|
+
tbody.querySelectorAll('input,select').forEach((el) => {
|
|
1129
|
+
el.addEventListener('change', () => syncFieldsFromUI());
|
|
1130
|
+
el.addEventListener('input', () => syncFieldsFromUI());
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
918
1133
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1134
|
+
if (!isExternal) {
|
|
1135
|
+
tbody.querySelectorAll('.btn-remove-field').forEach((btn) => {
|
|
1136
|
+
btn.addEventListener('click', () => {
|
|
1137
|
+
const idx = Number(btn.dataset.idx);
|
|
1138
|
+
const fields = Array.isArray(state.selectedModel.fields) ? state.selectedModel.fields : [];
|
|
1139
|
+
fields.splice(idx, 1);
|
|
1140
|
+
state.selectedModel.fields = fields;
|
|
1141
|
+
renderFieldsTable();
|
|
1142
|
+
});
|
|
926
1143
|
});
|
|
927
|
-
}
|
|
1144
|
+
}
|
|
928
1145
|
|
|
929
1146
|
updateRefVisibility();
|
|
930
1147
|
}
|
|
@@ -1045,6 +1262,12 @@
|
|
|
1045
1262
|
try {
|
|
1046
1263
|
const m = state.selectedModel;
|
|
1047
1264
|
if (!m) return;
|
|
1265
|
+
|
|
1266
|
+
if (m && (m.sourceType === 'external' || m.isExternal === true)) {
|
|
1267
|
+
toast('External models are read-only. Use Sync/Infer to refresh.', 'error');
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1048
1271
|
syncFieldsFromUI();
|
|
1049
1272
|
|
|
1050
1273
|
const payload = {
|
|
@@ -1580,6 +1803,53 @@
|
|
|
1580
1803
|
await loadTokens();
|
|
1581
1804
|
});
|
|
1582
1805
|
|
|
1806
|
+
document.getElementById('btn-import-external').addEventListener('click', async () => {
|
|
1807
|
+
try {
|
|
1808
|
+
setImportPreview(null);
|
|
1809
|
+
setImportModalOpen(true);
|
|
1810
|
+
await loadExternalCollectionsIntoModal();
|
|
1811
|
+
} catch (e) {
|
|
1812
|
+
toast(e.message, 'error');
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
document.getElementById('btn-import-close').addEventListener('click', () => {
|
|
1817
|
+
setImportModalOpen(false);
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
document.getElementById('import-collection').addEventListener('change', (e) => {
|
|
1821
|
+
const name = String(e.target.value || '').trim();
|
|
1822
|
+
const codeInput = document.getElementById('import-code');
|
|
1823
|
+
const dnInput = document.getElementById('import-displayName');
|
|
1824
|
+
if (codeInput && name) codeInput.value = normalizeExtCodeFromCollectionName(name);
|
|
1825
|
+
if (dnInput && name) dnInput.value = String(name).replace(/_/g, ' ');
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
document.getElementById('btn-import-infer').addEventListener('click', async () => {
|
|
1829
|
+
try {
|
|
1830
|
+
await importInferPreview();
|
|
1831
|
+
toast('Inferred', 'success');
|
|
1832
|
+
} catch (e) {
|
|
1833
|
+
toast(e.message, 'error');
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
document.getElementById('btn-import-run').addEventListener('click', async () => {
|
|
1838
|
+
try {
|
|
1839
|
+
await importRun();
|
|
1840
|
+
} catch (e) {
|
|
1841
|
+
toast(e.message, 'error');
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
document.getElementById('btn-sync-external').addEventListener('click', async () => {
|
|
1846
|
+
try {
|
|
1847
|
+
await syncExternalSelectedModel();
|
|
1848
|
+
} catch (e) {
|
|
1849
|
+
toast(e.message, 'error');
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1583
1853
|
// Copy cURL buttons
|
|
1584
1854
|
function setupCurlCopyButtons() {
|
|
1585
1855
|
document.querySelectorAll('.btn-copy-curl').forEach(btn => {
|