Files
crank/apps/ui/js/agents.js
T

723 lines
24 KiB
JavaScript
Raw 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 || [],
tool_selection_policy: agent.tool_selection_policy || { mode: 'direct', groups: [], search: { max_results: 8 } },
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: [],
accessMode: 'direct',
groups: [],
searchMaxResults: 8,
},
opSearch: '',
slugManuallyEdited: false,
searchPreviewQuery: '',
searchPreviewGroup: '',
searchPreviewItems: [],
searchPreviewLoading: false,
searchPreviewRan: 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 && this.form.accessMode === 'direct') {
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: [],
accessMode: 'direct',
groups: [],
searchMaxResults: 8,
};
this.opSearch = '';
this.slugManuallyEdited = false;
this.resetSearchPreview();
this.drawerOpen = true;
},
openEdit(agent) {
var policy = agent.tool_selection_policy || {};
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 || []),
accessMode: policy.mode === 'search' ? 'search' : 'direct',
groups: (policy.groups || []).map(function(group) {
return {
id: group.id || '',
name: group.name || '',
description: group.description || '',
tool_names: [].concat(group.tool_names || []),
};
}),
searchMaxResults: policy.search && policy.search.max_results
? policy.search.max_results
: 8,
};
this.opSearch = '';
this.slugManuallyEdited = true;
this.resetSearchPreview();
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);
var operation = this.operations.find(function(item) { return item.id === operationId; });
if (operation) {
this.form.groups.forEach(function(group) {
group.tool_names = group.tool_names.filter(function(name) {
return name !== operation.name;
});
});
}
}
this.resetSearchPreview();
},
isOpSelected(operationId) {
return this.form.selectedOps.includes(operationId);
},
clearSelectedOperations() {
this.form.selectedOps = [];
this.form.groups.forEach(function(group) { group.tool_names = []; });
this.resetSearchPreview();
},
setAccessMode(mode) {
this.form.accessMode = mode === 'search' ? 'search' : 'direct';
this.resetSearchPreview();
},
addToolGroup() {
this.form.groups.push({ id: '', name: '', description: '', tool_names: [] });
this.resetSearchPreview();
},
removeToolGroup(index) {
this.form.groups.splice(index, 1);
this.resetSearchPreview();
},
onToolGroupName(index, value) {
var group = this.form.groups[index];
if (!group) return;
var previousSlug = this.slugifyGroupName(group.name);
group.name = value;
if (!group.id || group.id === previousSlug) {
group.id = this.slugifyGroupName(value);
}
this.resetSearchPreview();
},
onToolGroupId(index, value) {
var group = this.form.groups[index];
if (!group) return;
group.id = this.slugifyGroupName(value);
this.resetSearchPreview();
},
slugifyGroupName(value) {
return String(value || '')
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
},
toggleToolGroup(groupIndex, toolName) {
var group = this.form.groups[groupIndex];
if (!group) return;
var index = group.tool_names.indexOf(toolName);
if (index === -1) group.tool_names.push(toolName);
else group.tool_names.splice(index, 1);
this.resetSearchPreview();
},
toolInGroup(groupIndex, toolName) {
var group = this.form.groups[groupIndex];
return Boolean(group && group.tool_names.includes(toolName));
},
toolSelectionPolicy() {
return {
mode: this.form.accessMode,
groups: this.form.accessMode === 'search'
? this.form.groups.map(function(group) {
return {
id: group.id.trim(),
name: group.name.trim(),
description: group.description.trim(),
tool_names: [].concat(group.tool_names || []),
};
})
: [],
search: {
max_results: Math.max(1, Math.min(20, Number(this.form.searchMaxResults) || 8)),
},
};
},
get catalogConfigValid() {
if (this.form.accessMode !== 'search') return true;
var ids = [];
for (var index = 0; index < this.form.groups.length; index += 1) {
var group = this.form.groups[index];
if (!group.id.trim() || !group.name.trim() || !group.description.trim()) return false;
if (ids.includes(group.id.trim())) return false;
ids.push(group.id.trim());
}
return true;
},
agentBindings() {
var self = this;
return 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,
};
});
},
resetSearchPreview() {
this.searchPreviewItems = [];
this.searchPreviewRan = false;
},
async previewToolSearch() {
if (
this.searchPreviewLoading
|| !this.workspaceId
|| !this.searchPreviewQuery.trim()
|| this.form.accessMode !== 'search'
) return;
this.searchPreviewLoading = true;
this.searchPreviewRan = false;
try {
var response = await window.CrankApi.previewAgentToolSearch(this.workspaceId, {
query: this.searchPreviewQuery.trim(),
group_ids: this.searchPreviewGroup ? [this.searchPreviewGroup] : [],
bindings: this.agentBindings(),
tool_selection_policy: this.toolSelectionPolicy(),
});
this.searchPreviewItems = response.items || [];
this.searchPreviewRan = true;
} catch (error) {
this.searchPreviewItems = [];
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.drawer.search.preview_error'),
this.tKey('agents.drawer.search.preview_error_title')
);
}
} finally {
this.searchPreviewLoading = false;
}
},
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: this.toolSelectionPolicy(),
});
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;
}
var savedVersion = await window.CrankApi.saveAgentBindings(
this.workspaceId,
agentId,
{
bindings: this.agentBindings(),
tool_selection_policy: this.toolSelectionPolicy(),
},
);
currentVersion = savedVersion.version || currentVersion;
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);
},
};
});
});