Files
crank/apps/ui/js/agents.js
github-ops 7099faad2b
Deploy / deploy (push) Successful in 1m34s
CI / Rust Checks (push) Successful in 5m3s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 4m44s
Warn about confusing agent tool catalogs
2026-06-20 21:09:45 +00:00

551 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function agentUiStatus(status) {
if (status === 'published') return 'published';
if (status === 'archived') return 'archived';
return status || 'draft';
}
function mapAgent(agent) {
var mapped = {
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 || '',
};
return window.localizeDemoAgent ? window.localizeDemoAgent(mapped) : mapped;
}
function mapOperation(operation) {
var mapped = {
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,
};
return window.localizeDemoOperation ? window.localizeDemoOperation(mapped) : mapped;
}
function currentLocale() {
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
}
document.addEventListener('alpine:init', function() {
Alpine.data('agents', function() {
return {
agents: [],
operations: [],
capabilities: null,
loading: true,
loadError: '',
workspaceId: null,
agentSearch: '',
openDropdown: null,
drawerOpen: false,
drawerMode: 'create',
editingId: null,
saving: false,
form: {
display_name: '',
slug: '',
description: '',
status: 'published',
selectedOps: [],
},
opSearch: '',
slugManuallyEdited: false,
async init() {
var self = this;
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
this.workspaceId = workspace ? workspace.id : null;
await this.loadCapabilities();
await this.reload();
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && self.drawerOpen) {
self.closeDrawer();
}
});
document.addEventListener('click', function() {
self.openDropdown = null;
});
window.addEventListener('crank:workspacechange', async function(event) {
self.workspaceId = event.detail ? event.detail.id : null;
await self.loadCapabilities();
await self.reload();
});
},
async loadCapabilities() {
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
this.capabilities = null;
return;
}
try {
this.capabilities = await window.CrankApi.getCapabilities();
} catch (_error) {
this.capabilities = null;
}
},
async reload() {
this.loading = true;
this.loadError = '';
if (!this.workspaceId || !window.CrankApi) {
this.agents = [];
this.operations = [];
this.loading = false;
this.loadError = this.tKey('agents.error.api');
return;
}
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 || this.tKey('agents.error.load');
}
this.loading = false;
},
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);
});
},
get totalCalls() {
return this.agents.reduce(function(sum, agent) {
return sum + (agent.calls_today || 0);
}, 0);
},
get activeCount() {
return this.agents.filter(function(agent) {
return agent.status === 'published';
}).length;
},
get totalOpsExposed() {
return this.agents.reduce(function(sum, agent) {
return sum + (agent.operation_count || 0);
}, 0);
},
get isCommunityBuild() {
return !!(this.capabilities && this.capabilities.edition === 'community');
},
agentCalloutText() {
if (this.isCommunityBuild) {
return this.tfKey('agents.callout.community', { count: this.operations.length });
}
return this.tfKey('agents.callout.default', { count: this.operations.length });
},
subtitleText() {
return this.tKey(this.isCommunityBuild ? 'agents.subtitle.community' : 'agents.subtitle');
},
emptyStateText() {
return this.tKey(this.isCommunityBuild ? 'agents.empty.initial.sub_community' : 'agents.empty.initial.sub');
},
drawerSubText() {
if (this.drawerMode === 'edit') {
return this.tKey('agents.drawer.edit_sub');
}
return this.tKey(this.isCommunityBuild ? 'agents.drawer.new_sub_community' : 'agents.drawer.new_sub');
},
operationsSubText() {
return this.tKey(this.isCommunityBuild ? 'agents.drawer.operations_sub_community' : 'agents.drawer.operations_sub');
},
get selectedOperations() {
var selected = this.form.selectedOps;
return this.operations.filter(function(operation) {
return selected.includes(operation.id);
});
},
get agentToolFindings() {
var findings = [];
var selected = this.selectedOperations;
if (selected.length > 8) {
findings.push(this.tKey('agents.drawer.finding.too_many_tools'));
}
for (var leftIndex = 0; leftIndex < selected.length; leftIndex += 1) {
for (var rightIndex = leftIndex + 1; rightIndex < selected.length; rightIndex += 1) {
if (this.operationsLookSimilar(selected[leftIndex], selected[rightIndex])) {
findings.push(this.tfKey('agents.drawer.finding.similar_tools', {
left: selected[leftIndex].display_name || selected[leftIndex].name,
right: selected[rightIndex].display_name || selected[rightIndex].name,
}));
return findings;
}
}
}
return findings;
},
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);
});
},
openCreate() {
this.drawerMode = 'create';
this.editingId = null;
this.form = {
display_name: '',
slug: '',
description: '',
status: 'published',
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.raw_status || agent.status || '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);
},
operationsLookSimilar(left, right) {
var leftTokens = this.operationTokens(left);
var rightTokens = this.operationTokens(right);
if (!leftTokens.length || !rightTokens.length) return false;
var shared = leftTokens.filter(function(token) { return rightTokens.includes(token); }).length;
var smaller = Math.min(leftTokens.length, rightTokens.length);
return shared >= 2 && (shared / smaller) >= 0.6;
},
operationTokens(operation) {
var text = [
operation.name || '',
operation.display_name || '',
].join(' ').toLowerCase();
var tokens = [];
text.split(/[^a-z0-9а-яё]+/i).forEach(function(token) {
if (token.length < 3 || tokens.includes(token)) return;
tokens.push(token);
});
return tokens;
},
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;
var existingAgent = this.drawerMode === 'edit'
? this.agents.find(function(item) { return item.id === agentId; })
: null;
var previousStatus = existingAgent ? (existingAgent.raw_status || 'draft') : 'draft';
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 === 'published') {
await window.CrankApi.publishAgent(this.workspaceId, agentId, {
version: currentVersion,
});
} else if (this.form.status === 'archived') {
await window.CrankApi.archiveAgent(this.workspaceId, agentId);
} else if (this.drawerMode === 'edit' && previousStatus !== 'draft') {
await window.CrankApi.unpublishAgent(this.workspaceId, agentId);
}
await this.reload();
if (window.CrankUi) {
window.CrankUi.success(
this.tfKey('agents.toast.saved_message', {
name: this.form.display_name,
count: this.form.selectedOps.length
}),
this.drawerMode === 'create'
? this.tKey('agents.toast.saved_title_create')
: this.tKey('agents.toast.saved_title_update')
);
}
this.closeDrawer();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.toast.save_error_message'),
this.tKey('agents.toast.save_error_title')
);
}
this.saving = false;
}
},
async deleteAgent(id) {
if (!confirm(this.tKey('agents.toast.delete_confirm'))) return;
try {
var agent = this.agents.find(function(item) { return item.id === id; });
await window.CrankApi.deleteAgent(this.workspaceId, id);
await this.reload();
if (window.CrankUi) {
window.CrankUi.success(
this.tfKey('agents.toast.delete_message', { name: agent ? agent.display_name : '' }),
this.tKey('agents.toast.delete_title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.toast.delete_error_message'),
this.tKey('agents.toast.delete_error_title')
);
}
}
},
async applyLifecycle(agent, action) {
if (!this.workspaceId || !window.CrankApi) {
return;
}
try {
if (action === 'publish') {
await window.CrankApi.publishAgent(this.workspaceId, agent.id, {
version: agent.current_draft_version || 1,
});
} else if (action === 'unpublish') {
await window.CrankApi.unpublishAgent(this.workspaceId, agent.id);
} else if (action === 'archive') {
await window.CrankApi.archiveAgent(this.workspaceId, agent.id);
}
await this.reload();
if (window.CrankUi) {
window.CrankUi.success(
action === 'publish'
? this.tfKey('agents.toast.lifecycle_publish', { name: agent.display_name })
: action === 'unpublish'
? this.tfKey('agents.toast.lifecycle_unpublish', { name: agent.display_name })
: this.tfKey('agents.toast.lifecycle_archive', { name: agent.display_name }),
this.tKey('agents.toast.lifecycle_title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.toast.lifecycle_error_message'),
this.tKey('agents.toast.lifecycle_error_title')
);
}
}
},
lifecycleAction(agent) {
if (agent.raw_status === 'published') {
return { key: 'unpublish', label: this.tKey('agents.lifecycle.unpublish') };
}
if (agent.raw_status === 'archived') {
return { key: 'unpublish', label: this.tKey('agents.lifecycle.restore_draft') };
}
return { key: 'publish', label: this.tKey('agents.lifecycle.publish') };
},
lifecycleLabel(agent) {
if (agent.raw_status === 'published') return this.tKey('agents.lifecycle.published');
if (agent.raw_status === 'archived') return this.tKey('agents.lifecycle.archived');
return this.tKey('agents.lifecycle.draft');
},
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;
},
endpointHelpText(agent) {
return this.tfKey(
this.isCommunityBuild ? 'agents.card.endpoint_help_community' : 'agents.card.endpoint_help',
{ count: agent.key_count || 0 }
);
},
copyEndpoint(agent) {
var endpoint = this.mcpEndpoint(agent);
navigator.clipboard.writeText(endpoint).catch(function() {});
if (window.CrankUi) {
window.CrankUi.info(endpoint, this.tKey('agents.toast.endpoint_title'));
}
},
statusClass(status) {
return 'agent-status-badge agent-status-' + status;
},
protocolBadge(operation) {
return 'badge badge-rest';
},
protocolLabel(operation) {
return 'REST';
},
formatDate(dateStr) {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleDateString(currentLocale(), {
month: 'short',
day: 'numeric',
year: 'numeric',
});
},
formatCalls(value) {
if (!value) return '0';
return value >= 1000 ? (value / 1000).toFixed(1) + 'k' : String(value);
},
tKey(key) {
return window.t ? t(key) : key;
},
tfKey(key, vars) {
return window.tf ? tf(key, vars) : this.tKey(key);
},
};
});
});