feat: connect alpine agents to admin api
This commit is contained in:
+323
-188
@@ -1,208 +1,343 @@
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('agents', () => ({
|
||||
agents: [],
|
||||
operations: [],
|
||||
loading: true,
|
||||
function agentUiStatus(status) {
|
||||
if (status === 'published') return 'active';
|
||||
if (status === 'archived') return 'inactive';
|
||||
return status || 'draft';
|
||||
}
|
||||
|
||||
agentSearch: '',
|
||||
openDropdown: null,
|
||||
function mapAgent(agent) {
|
||||
return {
|
||||
id: agent.id,
|
||||
slug: agent.slug,
|
||||
display_name: agent.display_name,
|
||||
description: agent.description || '',
|
||||
status: agentUiStatus(agent.status),
|
||||
raw_status: agent.status,
|
||||
operation_count: agent.operation_count || 0,
|
||||
operation_ids: agent.operation_ids || [],
|
||||
key_count: agent.key_count || 0,
|
||||
calls_today: agent.calls_today || 0,
|
||||
created_at: agent.created_at,
|
||||
current_draft_version: agent.current_draft_version || 1,
|
||||
latest_published_version: agent.latest_published_version,
|
||||
mcp_endpoint: agent.mcp_endpoint || '',
|
||||
};
|
||||
}
|
||||
|
||||
// drawer state
|
||||
drawerOpen: false,
|
||||
drawerMode: 'create', // 'create' | 'edit'
|
||||
editingId: null,
|
||||
function mapOperation(operation) {
|
||||
return {
|
||||
id: operation.id,
|
||||
name: operation.name,
|
||||
display_name: operation.display_name,
|
||||
protocol: operation.protocol,
|
||||
status: operation.status,
|
||||
current_draft_version: operation.current_draft_version || 1,
|
||||
latest_published_version: operation.latest_published_version,
|
||||
};
|
||||
}
|
||||
|
||||
// form fields
|
||||
form: {
|
||||
display_name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
status: 'active',
|
||||
selectedOps: [],
|
||||
},
|
||||
document.addEventListener('alpine:init', function() {
|
||||
Alpine.data('agents', function() {
|
||||
return {
|
||||
agents: [],
|
||||
operations: [],
|
||||
loading: true,
|
||||
loadError: '',
|
||||
workspaceId: null,
|
||||
|
||||
// operations picker
|
||||
opSearch: '',
|
||||
slugManuallyEdited: false,
|
||||
agentSearch: '',
|
||||
openDropdown: null,
|
||||
drawerOpen: false,
|
||||
drawerMode: 'create',
|
||||
editingId: null,
|
||||
saving: false,
|
||||
|
||||
async init() {
|
||||
const [agentsRes, opsRes] = await Promise.all([
|
||||
fetch((window.DATA_URL||'data/')+'agents.json'),
|
||||
fetch((window.DATA_URL||'data/')+'operations.json'),
|
||||
]);
|
||||
this.agents = await agentsRes.json();
|
||||
this.operations = await opsRes.json();
|
||||
this.loading = false;
|
||||
form: {
|
||||
display_name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
status: 'active',
|
||||
selectedOps: [],
|
||||
},
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && this.drawerOpen) this.closeDrawer();
|
||||
});
|
||||
opSearch: '',
|
||||
slugManuallyEdited: false,
|
||||
|
||||
document.addEventListener('click', () => { this.openDropdown = null; });
|
||||
},
|
||||
async init() {
|
||||
var self = this;
|
||||
|
||||
// ── computed ──
|
||||
get filteredAgents() {
|
||||
const q = this.agentSearch.toLowerCase().trim();
|
||||
if (!q) return this.agents;
|
||||
return this.agents.filter(a =>
|
||||
a.display_name.toLowerCase().includes(q) ||
|
||||
a.slug.toLowerCase().includes(q) ||
|
||||
(a.description || '').toLowerCase().includes(q)
|
||||
);
|
||||
},
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
this.workspaceId = workspace ? workspace.id : null;
|
||||
await this.reload();
|
||||
|
||||
get totalCalls() {
|
||||
return this.agents.reduce((s, a) => s + (a.calls_today || 0), 0);
|
||||
},
|
||||
get activeCount() {
|
||||
return this.agents.filter(a => a.status === 'active').length;
|
||||
},
|
||||
get totalOpsExposed() {
|
||||
return this.agents.reduce((s, a) => s + (a.operation_count || 0), 0);
|
||||
},
|
||||
|
||||
get filteredOps() {
|
||||
const q = this.opSearch.toLowerCase();
|
||||
if (!q) return this.operations;
|
||||
return this.operations.filter(op =>
|
||||
op.name.toLowerCase().includes(q) ||
|
||||
op.display_name.toLowerCase().includes(q)
|
||||
);
|
||||
},
|
||||
|
||||
// ── drawer ──
|
||||
openCreate() {
|
||||
this.drawerMode = 'create';
|
||||
this.editingId = null;
|
||||
this.form = { display_name: '', slug: '', description: '', status: 'active', selectedOps: [] };
|
||||
this.opSearch = '';
|
||||
this.slugManuallyEdited = false;
|
||||
this.drawerOpen = true;
|
||||
},
|
||||
|
||||
openEdit(agent) {
|
||||
this.drawerMode = 'edit';
|
||||
this.editingId = agent.id;
|
||||
this.form = {
|
||||
display_name: agent.display_name,
|
||||
slug: agent.slug,
|
||||
description: agent.description,
|
||||
status: agent.status,
|
||||
selectedOps: [...(agent.operation_ids || [])],
|
||||
};
|
||||
this.opSearch = '';
|
||||
this.slugManuallyEdited = true;
|
||||
this.drawerOpen = true;
|
||||
},
|
||||
|
||||
closeDrawer() {
|
||||
this.drawerOpen = false;
|
||||
},
|
||||
|
||||
// ── slug auto-derive ──
|
||||
onNameInput(val) {
|
||||
this.form.display_name = val;
|
||||
if (!this.slugManuallyEdited) {
|
||||
this.form.slug = val.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
}
|
||||
},
|
||||
|
||||
onSlugInput(val) {
|
||||
this.form.slug = val.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
||||
this.slugManuallyEdited = true;
|
||||
},
|
||||
|
||||
// ── operations picker ──
|
||||
toggleOp(opId) {
|
||||
const idx = this.form.selectedOps.indexOf(opId);
|
||||
if (idx === -1) this.form.selectedOps.push(opId);
|
||||
else this.form.selectedOps.splice(idx, 1);
|
||||
},
|
||||
isOpSelected(opId) {
|
||||
return this.form.selectedOps.includes(opId);
|
||||
},
|
||||
|
||||
// ── save ──
|
||||
saveAgent() {
|
||||
if (!this.form.display_name.trim() || !this.form.slug.trim()) return;
|
||||
|
||||
const ws = (localStorage.getItem('crank_workspace') || 'acme-workspace');
|
||||
const endpoint = '/mcp/v1/' + ws + '/' + this.form.slug;
|
||||
|
||||
if (this.drawerMode === 'create') {
|
||||
this.agents.unshift({
|
||||
id: 'ag-' + Date.now(),
|
||||
slug: this.form.slug,
|
||||
display_name: this.form.display_name,
|
||||
description: this.form.description,
|
||||
status: this.form.status,
|
||||
operation_count: this.form.selectedOps.length,
|
||||
operation_ids: [...this.form.selectedOps],
|
||||
key_count: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
created_by: 'AT',
|
||||
last_called_at: null,
|
||||
calls_today: 0,
|
||||
mcp_endpoint: endpoint,
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape' && self.drawerOpen) {
|
||||
self.closeDrawer();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const idx = this.agents.findIndex(a => a.id === this.editingId);
|
||||
if (idx !== -1) {
|
||||
this.agents[idx] = Object.assign({}, this.agents[idx], {
|
||||
display_name: this.form.display_name,
|
||||
slug: this.form.slug,
|
||||
description: this.form.description,
|
||||
status: this.form.status,
|
||||
operation_count: this.form.selectedOps.length,
|
||||
operation_ids: [...this.form.selectedOps],
|
||||
mcp_endpoint: endpoint,
|
||||
});
|
||||
|
||||
document.addEventListener('click', function() {
|
||||
self.openDropdown = null;
|
||||
});
|
||||
|
||||
window.addEventListener('crank:workspacechange', async function(event) {
|
||||
self.workspaceId = event.detail ? event.detail.id : null;
|
||||
await self.reload();
|
||||
});
|
||||
},
|
||||
|
||||
async reload() {
|
||||
this.loading = true;
|
||||
this.loadError = '';
|
||||
|
||||
if (!this.workspaceId || !window.CrankApi) {
|
||||
this.agents = [];
|
||||
this.operations = [];
|
||||
this.loading = false;
|
||||
this.loadError = 'Workspace or API is unavailable';
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.closeDrawer();
|
||||
},
|
||||
|
||||
// ── delete ──
|
||||
deleteAgent(id) {
|
||||
this.agents = this.agents.filter(a => a.id !== id);
|
||||
},
|
||||
try {
|
||||
var responses = await Promise.all([
|
||||
window.CrankApi.listAgents(this.workspaceId),
|
||||
window.CrankApi.listOperations(this.workspaceId),
|
||||
]);
|
||||
this.agents = ((responses[0] && responses[0].items) || []).map(mapAgent);
|
||||
this.operations = ((responses[1] && responses[1].items) || []).map(mapOperation);
|
||||
} catch (error) {
|
||||
this.agents = [];
|
||||
this.operations = [];
|
||||
this.loadError = error.message || 'Failed to load agents';
|
||||
}
|
||||
|
||||
// ── helpers ──
|
||||
mcpEndpoint(agent) {
|
||||
if (agent.mcp_endpoint) return agent.mcp_endpoint;
|
||||
const ws = localStorage.getItem('crank_workspace') || 'acme-workspace';
|
||||
return '/mcp/v1/' + ws + '/' + agent.slug;
|
||||
},
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
copyEndpoint(agent) {
|
||||
navigator.clipboard.writeText(this.mcpEndpoint(agent)).catch(() => {});
|
||||
},
|
||||
get filteredAgents() {
|
||||
var query = this.agentSearch.toLowerCase().trim();
|
||||
if (!query) return this.agents;
|
||||
return this.agents.filter(function(agent) {
|
||||
return agent.display_name.toLowerCase().includes(query)
|
||||
|| agent.slug.toLowerCase().includes(query)
|
||||
|| (agent.description || '').toLowerCase().includes(query);
|
||||
});
|
||||
},
|
||||
|
||||
statusClass(status) {
|
||||
return 'agent-status-badge agent-status-' + status;
|
||||
},
|
||||
get totalCalls() {
|
||||
return this.agents.reduce(function(sum, agent) {
|
||||
return sum + (agent.calls_today || 0);
|
||||
}, 0);
|
||||
},
|
||||
|
||||
protocolBadge(op) {
|
||||
if (op.protocol === 'rest') return 'badge badge-rest';
|
||||
if (op.protocol === 'graphql') return 'badge badge-graphql';
|
||||
return 'badge badge-grpc';
|
||||
},
|
||||
get activeCount() {
|
||||
return this.agents.filter(function(agent) {
|
||||
return agent.status === 'active';
|
||||
}).length;
|
||||
},
|
||||
|
||||
protocolLabel(op) {
|
||||
if (op.protocol === 'rest') return 'REST';
|
||||
if (op.protocol === 'graphql') return 'GQL';
|
||||
return 'gRPC';
|
||||
},
|
||||
get totalOpsExposed() {
|
||||
return this.agents.reduce(function(sum, agent) {
|
||||
return sum + (agent.operation_count || 0);
|
||||
}, 0);
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '—';
|
||||
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
},
|
||||
get filteredOps() {
|
||||
var query = this.opSearch.toLowerCase().trim();
|
||||
if (!query) return this.operations;
|
||||
return this.operations.filter(function(operation) {
|
||||
return operation.name.toLowerCase().includes(query)
|
||||
|| operation.display_name.toLowerCase().includes(query);
|
||||
});
|
||||
},
|
||||
|
||||
formatCalls(n) {
|
||||
if (!n) return '0';
|
||||
return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
||||
},
|
||||
}));
|
||||
openCreate() {
|
||||
this.drawerMode = 'create';
|
||||
this.editingId = null;
|
||||
this.form = {
|
||||
display_name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
status: 'active',
|
||||
selectedOps: [],
|
||||
};
|
||||
this.opSearch = '';
|
||||
this.slugManuallyEdited = false;
|
||||
this.drawerOpen = true;
|
||||
},
|
||||
|
||||
openEdit(agent) {
|
||||
this.drawerMode = 'edit';
|
||||
this.editingId = agent.id;
|
||||
this.form = {
|
||||
display_name: agent.display_name,
|
||||
slug: agent.slug,
|
||||
description: agent.description,
|
||||
status: agent.status === 'active' ? 'active' : 'draft',
|
||||
selectedOps: [].concat(agent.operation_ids || []),
|
||||
};
|
||||
this.opSearch = '';
|
||||
this.slugManuallyEdited = true;
|
||||
this.drawerOpen = true;
|
||||
},
|
||||
|
||||
closeDrawer() {
|
||||
this.drawerOpen = false;
|
||||
this.saving = false;
|
||||
},
|
||||
|
||||
onNameInput(value) {
|
||||
this.form.display_name = value;
|
||||
if (!this.slugManuallyEdited) {
|
||||
this.form.slug = value
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
}
|
||||
},
|
||||
|
||||
onSlugInput(value) {
|
||||
this.form.slug = value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
||||
this.slugManuallyEdited = true;
|
||||
},
|
||||
|
||||
toggleOp(operationId) {
|
||||
var index = this.form.selectedOps.indexOf(operationId);
|
||||
if (index === -1) {
|
||||
this.form.selectedOps.push(operationId);
|
||||
} else {
|
||||
this.form.selectedOps.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
isOpSelected(operationId) {
|
||||
return this.form.selectedOps.includes(operationId);
|
||||
},
|
||||
|
||||
async saveAgent() {
|
||||
var self = this;
|
||||
if (
|
||||
this.saving
|
||||
|| !this.workspaceId
|
||||
|| !this.form.display_name.trim()
|
||||
|| !this.form.slug.trim()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
var agentId = this.editingId;
|
||||
var currentVersion = 1;
|
||||
|
||||
if (this.drawerMode === 'create') {
|
||||
var created = await window.CrankApi.createAgent(this.workspaceId, {
|
||||
slug: this.form.slug,
|
||||
display_name: this.form.display_name,
|
||||
description: this.form.description,
|
||||
instructions: {},
|
||||
tool_selection_policy: {},
|
||||
});
|
||||
agentId = created.agent_id;
|
||||
currentVersion = created.version || 1;
|
||||
} else {
|
||||
await window.CrankApi.updateAgent(this.workspaceId, this.editingId, {
|
||||
slug: this.form.slug,
|
||||
display_name: this.form.display_name,
|
||||
description: this.form.description,
|
||||
});
|
||||
var agent = await window.CrankApi.getAgent(this.workspaceId, this.editingId);
|
||||
currentVersion = agent.current_draft_version || 1;
|
||||
}
|
||||
|
||||
await window.CrankApi.saveAgentBindings(
|
||||
this.workspaceId,
|
||||
agentId,
|
||||
this.form.selectedOps.map(function(operationId) {
|
||||
var operation = self.operations.find(function(item) {
|
||||
return item.id === operationId;
|
||||
});
|
||||
return {
|
||||
operation_id: operationId,
|
||||
operation_version: operation && operation.latest_published_version
|
||||
? operation.latest_published_version
|
||||
: operation && operation.current_draft_version
|
||||
? operation.current_draft_version
|
||||
: 1,
|
||||
tool_name: operation ? operation.name : operationId,
|
||||
tool_title: operation ? (operation.display_name || operation.name) : operationId,
|
||||
tool_description_override: null,
|
||||
enabled: true,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
if (this.form.status === 'active') {
|
||||
await window.CrankApi.publishAgent(this.workspaceId, agentId, {
|
||||
version: currentVersion,
|
||||
});
|
||||
}
|
||||
|
||||
await this.reload();
|
||||
this.closeDrawer();
|
||||
} catch (error) {
|
||||
alert(error.message || 'Failed to save agent');
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteAgent(id) {
|
||||
if (!confirm('Delete this agent? This cannot be undone.')) return;
|
||||
|
||||
try {
|
||||
await window.CrankApi.deleteAgent(this.workspaceId, id);
|
||||
await this.reload();
|
||||
} catch (error) {
|
||||
alert(error.message || 'Failed to delete agent');
|
||||
}
|
||||
},
|
||||
|
||||
mcpEndpoint(agent) {
|
||||
if (agent.mcp_endpoint) return agent.mcp_endpoint;
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
var workspaceSlug = workspace ? workspace.slug : 'default';
|
||||
return '/mcp/v1/' + workspaceSlug + '/' + agent.slug;
|
||||
},
|
||||
|
||||
copyEndpoint(agent) {
|
||||
navigator.clipboard.writeText(this.mcpEndpoint(agent)).catch(function() {});
|
||||
},
|
||||
|
||||
statusClass(status) {
|
||||
return 'agent-status-badge agent-status-' + status;
|
||||
},
|
||||
|
||||
protocolBadge(operation) {
|
||||
if (operation.protocol === 'rest') return 'badge badge-rest';
|
||||
if (operation.protocol === 'graphql') return 'badge badge-graphql';
|
||||
return 'badge badge-grpc';
|
||||
},
|
||||
|
||||
protocolLabel(operation) {
|
||||
if (operation.protocol === 'rest') return 'REST';
|
||||
if (operation.protocol === 'graphql') return 'GQL';
|
||||
return 'gRPC';
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '—';
|
||||
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
},
|
||||
|
||||
formatCalls(value) {
|
||||
if (!value) return '0';
|
||||
return value >= 1000 ? (value / 1000).toFixed(1) + 'k' : String(value);
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,5 +110,26 @@
|
||||
fileName
|
||||
);
|
||||
},
|
||||
listAgents: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents');
|
||||
},
|
||||
createAgent: function(workspaceId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents', payload);
|
||||
},
|
||||
getAgent: function(workspaceId, agentId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId));
|
||||
},
|
||||
updateAgent: function(workspaceId, agentId, payload) {
|
||||
return patch('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId), payload);
|
||||
},
|
||||
deleteAgent: function(workspaceId, agentId) {
|
||||
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId));
|
||||
},
|
||||
saveAgentBindings: function(workspaceId, agentId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/bindings', payload);
|
||||
},
|
||||
publishAgent: function(workspaceId, agentId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/publish', payload);
|
||||
},
|
||||
};
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user