feat: localize agents page
This commit is contained in:
+56
-55
@@ -36,12 +36,12 @@
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" style="display:none">
|
||||
<div class="ws-dropdown-label">Your workspaces</div>
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
Create workspace
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@
|
||||
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
|
||||
<span>⌘K</span>
|
||||
</div>
|
||||
<button class="nav-icon-btn" title="Notifications">
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
|
||||
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
|
||||
</button>
|
||||
<div class="nav-divider"></div>
|
||||
@@ -100,36 +100,36 @@
|
||||
<!-- Page header -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-heading">Agents</h1>
|
||||
<p class="page-subheading">Named MCP endpoints that expose a curated subset of operations to an LLM</p>
|
||||
<h1 class="page-heading" data-i18n="agents.title">Agents</h1>
|
||||
<p class="page-subheading" data-i18n="agents.subtitle">Named MCP endpoints that expose a curated subset of operations to an LLM</p>
|
||||
</div>
|
||||
<button class="btn-new" @click="openCreate()">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
New agent
|
||||
<span data-i18n="agents.new">New agent</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total agents</div>
|
||||
<div class="stat-label" data-i18n="agents.stats.total">Total agents</div>
|
||||
<div class="stat-value" x-text="agents.length">—</div>
|
||||
<div class="stat-delta">across this workspace</div>
|
||||
<div class="stat-delta" x-text="tKey('agents.stats.total_sub')">across this workspace</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Published</div>
|
||||
<div class="stat-label" data-i18n="agents.stats.published">Published</div>
|
||||
<div class="stat-value" x-text="activeCount">—</div>
|
||||
<div class="stat-delta" x-text="agents.length ? Math.round(activeCount/agents.length*100)+'% of total' : ''">—</div>
|
||||
<div class="stat-delta" x-text="agents.length ? tfKey('agents.stats.published_sub', { value: Math.round(activeCount/agents.length*100) }) : ''">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Operations exposed</div>
|
||||
<div class="stat-label" data-i18n="agents.stats.tools">Operations exposed</div>
|
||||
<div class="stat-value" x-text="totalOpsExposed">—</div>
|
||||
<div class="stat-delta" x-text="'across ' + agents.length + ' agents'">—</div>
|
||||
<div class="stat-delta" x-text="tfKey('agents.stats.tools_sub', { count: agents.length })">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Calls today</div>
|
||||
<div class="stat-label" data-i18n="agents.stats.calls">Calls today</div>
|
||||
<div class="stat-value" x-text="formatCalls(totalCalls)">—</div>
|
||||
<div class="stat-delta up">all agents combined</div>
|
||||
<div class="stat-delta up" x-text="tKey('agents.stats.calls_sub')">all agents combined</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
<svg width="14" height="14"><use href="icons/general/search.svg#icon"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
data-i18n-ph="agents.search"
|
||||
placeholder="Search agents..."
|
||||
:value="agentSearch"
|
||||
@input="agentSearch = $event.target.value"
|
||||
@@ -146,7 +147,7 @@
|
||||
</div>
|
||||
<div class="filter-right">
|
||||
<span class="results-count" x-show="agentSearch">
|
||||
<span x-text="filteredAgents.length"></span> of <span x-text="agents.length"></span> agents
|
||||
<span x-text="tfKey('agents.results', { shown: filteredAgents.length, total: agents.length })"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,17 +155,17 @@
|
||||
<!-- Info callout -->
|
||||
<div class="agents-info-callout">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="7"/><path d="M8 11V8M8 5v-.5"/></svg>
|
||||
<span>Each agent gets its own MCP endpoint. Connect your LLM client to a specific agent so it only sees the tools it needs — not all <strong x-text="operations.length">?</strong> operations in this workspace.</span>
|
||||
<span x-text="tfKey('agents.callout', { count: operations.length })">Each agent gets its own MCP endpoint.</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div class="loading-spinner" x-show="loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading agents…</span>
|
||||
<span data-i18n="agents.loading">Loading agents…</span>
|
||||
</div>
|
||||
|
||||
<div class="empty-state" x-show="!loading && loadError">
|
||||
<div class="empty-state-title">Failed to load agents</div>
|
||||
<div class="empty-state-title" data-i18n="agents.error.title">Failed to load agents</div>
|
||||
<div class="empty-state-sub" x-text="loadError"></div>
|
||||
</div>
|
||||
|
||||
@@ -173,18 +174,18 @@
|
||||
<div class="empty-state-icon">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.35"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 17.5h7M17.5 14v7"/></svg>
|
||||
</div>
|
||||
<div class="empty-state-title">No agents yet</div>
|
||||
<div class="empty-state-sub">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</div>
|
||||
<div class="empty-state-title" data-i18n="agents.empty.initial.title">No agents yet</div>
|
||||
<div class="empty-state-sub" data-i18n="agents.empty.initial.sub">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</div>
|
||||
<button class="btn-new" @click="openCreate()" style="margin-top: 16px;">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
New agent
|
||||
<span data-i18n="agents.new">New agent</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Empty state: search returned nothing -->
|
||||
<div class="empty-state" x-show="!loading && !loadError && agents.length > 0 && filteredAgents.length === 0">
|
||||
<div class="empty-state-title">No agents match "<span x-text="agentSearch"></span>"</div>
|
||||
<div class="empty-state-sub">Try a different search term.</div>
|
||||
<div class="empty-state-title" x-text="tfKey('agents.empty.search.title', { query: agentSearch })">No agents match</div>
|
||||
<div class="empty-state-sub" data-i18n="agents.empty.search.sub">Try a different search term.</div>
|
||||
</div>
|
||||
|
||||
<!-- Agents grid -->
|
||||
@@ -212,17 +213,17 @@
|
||||
<div class="agent-stat">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 5h12M2 8h8M2 11h5"/></svg>
|
||||
<span class="agent-stat-value" x-text="agent.operation_count"></span>
|
||||
<span>tools</span>
|
||||
<span data-i18n="agents.card.tools">tools</span>
|
||||
</div>
|
||||
<div class="agent-stat">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 2H4a1 1 0 00-1 1v3M7 2l7 7M7 2v5h5"/><path d="M3 9v4a1 1 0 001 1h8a1 1 0 001-1V9"/></svg>
|
||||
<span class="agent-stat-value" x-text="agent.key_count"></span>
|
||||
<span>API keys</span>
|
||||
<span data-i18n="agents.card.keys">API keys</span>
|
||||
</div>
|
||||
<div class="agent-stat" x-show="agent.calls_today > 0">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 8s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z"/><circle cx="8" cy="8" r="2"/></svg>
|
||||
<span class="agent-stat-value" x-text="formatCalls(agent.calls_today)"></span>
|
||||
<span>calls today</span>
|
||||
<span data-i18n="agents.card.calls_today">calls today</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -230,18 +231,18 @@
|
||||
<div class="agent-endpoint-row">
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8h12M10 4l4 4-4 4"/></svg>
|
||||
<code class="agent-endpoint-url" x-text="mcpEndpoint(agent)"></code>
|
||||
<button class="agent-endpoint-copy" @click.stop="copyEndpoint(agent)" title="Copy endpoint">
|
||||
<button class="agent-endpoint-copy" @click.stop="copyEndpoint(agent)" :title="tKey('agents.card.copy_endpoint')">
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="9" height="9" rx="1"/><path d="M5 5V3a1 1 0 011-1h7a1 1 0 011 1v7a1 1 0 01-1 1h-2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="agent-card-footer">
|
||||
<span class="agent-card-date" x-text="'Created ' + formatDate(agent.created_at)"></span>
|
||||
<span class="agent-card-date" x-text="tfKey('agents.card.created', { date: formatDate(agent.created_at) })"></span>
|
||||
<div class="agent-card-actions">
|
||||
<button class="agent-action-btn" @click.stop="openEdit(agent)">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 2l3 3-8 8H3v-3l8-8z"/></svg>
|
||||
Edit
|
||||
<span data-i18n="agents.action.edit">Edit</span>
|
||||
</button>
|
||||
<button class="agent-action-btn" @click.stop="applyLifecycle(agent, lifecycleAction(agent).key)">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 3.5v9l8-4.5-8-4.5z"/></svg>
|
||||
@@ -249,11 +250,11 @@
|
||||
</button>
|
||||
<button class="agent-action-btn" x-show="agent.raw_status !== 'archived'" @click.stop="applyLifecycle(agent, 'archive')">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 4.5h11v2l-1 6.5h-8l-1-6.5v-2z"/><path d="M5 4.5v-1A1.5 1.5 0 016.5 2h3A1.5 1.5 0 0111 3.5v1"/></svg>
|
||||
Archive
|
||||
<span data-i18n="agents.action.archive">Archive</span>
|
||||
</button>
|
||||
<button class="agent-action-btn danger" @click.stop="deleteAgent(agent.id)">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||
Delete
|
||||
<span data-i18n="agents.action.delete">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,8 +276,8 @@
|
||||
<!-- Drawer header -->
|
||||
<div class="drawer-header">
|
||||
<div>
|
||||
<div class="drawer-title" x-text="drawerMode === 'create' ? 'New agent' : 'Edit agent'"></div>
|
||||
<div class="drawer-subtitle" x-text="drawerMode === 'create' ? 'Configure a named MCP endpoint for your LLM' : 'Update agent settings and tool selection'"></div>
|
||||
<div class="drawer-title" x-text="drawerMode === 'create' ? tKey('agents.drawer.new_title') : tKey('agents.drawer.edit_title')"></div>
|
||||
<div class="drawer-subtitle" x-text="drawerMode === 'create' ? tKey('agents.drawer.new_sub') : tKey('agents.drawer.edit_sub')"></div>
|
||||
</div>
|
||||
<button class="drawer-close" @click="closeDrawer()">
|
||||
<svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
|
||||
@@ -288,40 +289,40 @@
|
||||
|
||||
<!-- Identity section -->
|
||||
<div class="drawer-section">
|
||||
<div class="drawer-section-title">Identity</div>
|
||||
<div class="drawer-section-title" data-i18n="agents.drawer.identity">Identity</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 14px;">
|
||||
<label class="form-label">Display name <span class="form-label-required">required</span></label>
|
||||
<input class="form-input" type="text" placeholder="Customer Support" autocomplete="off"
|
||||
<label class="form-label"><span data-i18n="agents.drawer.display_name">Display name</span> <span class="form-label-required" data-i18n="agents.drawer.required">required</span></label>
|
||||
<input class="form-input" type="text" data-i18n-ph="agents.drawer.placeholder.name" placeholder="Customer Support" autocomplete="off"
|
||||
:value="form.display_name" @input="onNameInput($event.target.value)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 14px;">
|
||||
<label class="form-label">Slug <span class="form-label-required">required</span></label>
|
||||
<input class="form-input input-mono" type="text" placeholder="customer-support" autocomplete="off"
|
||||
<label class="form-label"><span data-i18n="agents.drawer.slug">Slug</span> <span class="form-label-required" data-i18n="agents.drawer.required">required</span></label>
|
||||
<input class="form-input input-mono" type="text" data-i18n-ph="agents.drawer.placeholder.slug" placeholder="customer-support" autocomplete="off"
|
||||
:value="form.slug" @input="onSlugInput($event.target.value)">
|
||||
<div class="form-hint" x-show="form.slug">
|
||||
MCP endpoint: <code style="font-family:monospace;font-size:11px;color:var(--accent)" x-text="'/mcp/v1/' + (localStorage.getItem('crank_workspace_slug') || 'default') + '/' + form.slug"></code>
|
||||
<span data-i18n="agents.drawer.endpoint">MCP endpoint</span>: <code style="font-family:monospace;font-size:11px;color:var(--accent)" x-text="'/mcp/v1/' + (localStorage.getItem('crank_workspace_slug') || 'default') + '/' + form.slug"></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 14px;">
|
||||
<label class="form-label">Description <span class="form-label-optional">(optional)</span></label>
|
||||
<textarea class="form-textarea" rows="3" placeholder="What does this agent do? What LLM or use-case is it for?"
|
||||
<label class="form-label"><span data-i18n="agents.drawer.description">Description</span> <span class="form-label-optional" data-i18n="agents.drawer.optional">(optional)</span></label>
|
||||
<textarea class="form-textarea" rows="3" data-i18n-ph="agents.drawer.placeholder.description" placeholder="What does this agent do? What LLM or use-case is it for?"
|
||||
x-model="form.description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<label class="form-label" data-i18n="agents.drawer.status">Status</label>
|
||||
<div class="agent-status-toggle">
|
||||
<button class="agent-status-opt" :class="{ active: form.status === 'published' }" @click="form.status = 'published'">
|
||||
<span class="status-dot" style="background: var(--success, #3fb950)"></span> Published
|
||||
<span class="status-dot" style="background: var(--success, #3fb950)"></span> <span data-i18n="agents.lifecycle.published">Published</span>
|
||||
</button>
|
||||
<button class="agent-status-opt" :class="{ active: form.status === 'draft' }" @click="form.status = 'draft'">
|
||||
<span class="status-dot" style="background: var(--text-muted)"></span> Draft
|
||||
<span class="status-dot" style="background: var(--text-muted)"></span> <span data-i18n="agents.lifecycle.draft">Draft</span>
|
||||
</button>
|
||||
<button class="agent-status-opt" :class="{ active: form.status === 'archived' }" @click="form.status = 'archived'">
|
||||
<span class="status-dot" style="background: var(--orange, #d29922)"></span> Archived
|
||||
<span class="status-dot" style="background: var(--orange, #d29922)"></span> <span data-i18n="agents.lifecycle.archived">Archived</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,16 +331,16 @@
|
||||
<!-- Operations picker section -->
|
||||
<div class="drawer-section">
|
||||
<div class="drawer-section-header">
|
||||
<div class="drawer-section-title">Operations</div>
|
||||
<span class="drawer-section-count" x-text="form.selectedOps.length + ' selected'"></span>
|
||||
<div class="drawer-section-title" data-i18n="agents.drawer.operations">Operations</div>
|
||||
<span class="drawer-section-count" x-text="tfKey('agents.drawer.selected', { count: form.selectedOps.length })"></span>
|
||||
</div>
|
||||
<div class="drawer-section-sub">Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.</div>
|
||||
<div class="drawer-section-sub" data-i18n="agents.drawer.operations_sub">Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.</div>
|
||||
|
||||
<div class="ops-picker">
|
||||
<!-- Search -->
|
||||
<div class="ops-picker-search">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="1.8" stroke-linecap="round"><circle cx="7" cy="7" r="5"/><path d="M12 12l2.5 2.5"/></svg>
|
||||
<input class="ops-picker-search-input" type="text" placeholder="Filter operations…" x-model="opSearch" autocomplete="off" spellcheck="false">
|
||||
<input class="ops-picker-search-input" type="text" data-i18n-ph="agents.drawer.filter_ops" placeholder="Filter operations…" x-model="opSearch" autocomplete="off" spellcheck="false">
|
||||
<button class="ops-picker-clear" x-show="opSearch" @click="opSearch = ''" style="display:none">
|
||||
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
|
||||
</button>
|
||||
@@ -360,20 +361,20 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="ops-picker-empty" x-show="filteredOps.length === 0">
|
||||
No operations match "<span x-text="opSearch"></span>"
|
||||
<span x-text="tfKey('agents.drawer.ops_no_match', { query: opSearch })"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer count -->
|
||||
<div class="ops-picker-footer" x-show="form.selectedOps.length > 0">
|
||||
<span x-text="form.selectedOps.length"></span> operation<span x-show="form.selectedOps.length !== 1">s</span> selected
|
||||
<button @click="form.selectedOps = []" style="background:none;border:none;color:var(--text-muted);font-size:12px;cursor:pointer;margin-left:8px;">Clear all</button>
|
||||
<span x-text="tfKey('agents.drawer.ops_selected', { count: form.selectedOps.length })"></span>
|
||||
<button @click="form.selectedOps = []" style="background:none;border:none;color:var(--text-muted);font-size:12px;cursor:pointer;margin-left:8px;" data-i18n="agents.drawer.clear_all">Clear all</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recommendation callout -->
|
||||
<div class="agents-rec-callout" x-show="form.selectedOps.length > 15">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><polygon points="8,1 15,14 1,14" fill="none"/><path d="M8 6v4M8 11.5v.5"/></svg>
|
||||
<span>You've selected <strong x-text="form.selectedOps.length"></strong> tools. LLMs perform best with <15 tools per agent. Consider splitting into separate agents by use-case.</span>
|
||||
<span x-text="tfKey('agents.drawer.recommendation', { count: form.selectedOps.length })">You've selected tools.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -381,11 +382,11 @@
|
||||
|
||||
<!-- Drawer footer -->
|
||||
<div class="drawer-footer">
|
||||
<button class="btn-ghost-sm" style="padding: 8px 16px; font-size: 13px;" @click="closeDrawer()">Cancel</button>
|
||||
<button class="btn-ghost-sm" style="padding: 8px 16px; font-size: 13px;" @click="closeDrawer()" data-i18n="agents.drawer.cancel">Cancel</button>
|
||||
<button class="btn-primary-sm" style="padding: 8px 20px; font-size: 13px;"
|
||||
:disabled="!form.display_name.trim() || !form.slug.trim()"
|
||||
@click="saveAgent()"
|
||||
x-text="drawerMode === 'create' ? 'Create agent' : 'Save changes'">
|
||||
x-text="drawerMode === 'create' ? tKey('agents.drawer.create') : tKey('agents.drawer.save')">
|
||||
Create agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+50
-25
@@ -35,6 +35,10 @@ function mapOperation(operation) {
|
||||
};
|
||||
}
|
||||
|
||||
function currentLocale() {
|
||||
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', function() {
|
||||
Alpine.data('agents', function() {
|
||||
return {
|
||||
@@ -94,7 +98,7 @@ document.addEventListener('alpine:init', function() {
|
||||
this.agents = [];
|
||||
this.operations = [];
|
||||
this.loading = false;
|
||||
this.loadError = 'Workspace or API is unavailable';
|
||||
this.loadError = this.tKey('agents.error.api');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,7 +112,7 @@ document.addEventListener('alpine:init', function() {
|
||||
} catch (error) {
|
||||
this.agents = [];
|
||||
this.operations = [];
|
||||
this.loadError = error.message || 'Failed to load agents';
|
||||
this.loadError = error.message || this.tKey('agents.error.load');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
@@ -290,21 +294,29 @@ document.addEventListener('alpine:init', function() {
|
||||
await this.reload();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
this.form.display_name + ' was saved with ' + this.form.selectedOps.length + ' tool bindings.',
|
||||
this.drawerMode === 'create' ? 'Agent created' : 'Agent updated'
|
||||
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 || 'Failed to save agent', 'Agent update failed');
|
||||
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('Delete this agent? This cannot be undone.')) return;
|
||||
if (!confirm(this.tKey('agents.toast.delete_confirm'))) return;
|
||||
|
||||
try {
|
||||
var agent = this.agents.find(function(item) { return item.id === id; });
|
||||
@@ -312,13 +324,16 @@ document.addEventListener('alpine:init', function() {
|
||||
await this.reload();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
(agent ? agent.display_name : 'Agent') + ' was deleted.',
|
||||
'Agent deleted'
|
||||
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 || 'Failed to delete agent', 'Delete failed');
|
||||
window.CrankUi.error(
|
||||
error.message || this.tKey('agents.toast.delete_error_message'),
|
||||
this.tKey('agents.toast.delete_error_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -340,37 +355,39 @@ document.addEventListener('alpine:init', function() {
|
||||
}
|
||||
await this.reload();
|
||||
if (window.CrankUi) {
|
||||
var actionLabel = action === 'publish'
|
||||
? 'published'
|
||||
: action === 'unpublish'
|
||||
? 'returned to draft'
|
||||
: 'archived';
|
||||
window.CrankUi.success(
|
||||
agent.display_name + ' was ' + actionLabel + '.',
|
||||
'Agent lifecycle updated'
|
||||
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 || 'Failed to update agent lifecycle', 'Lifecycle update failed');
|
||||
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: 'Unpublish' };
|
||||
return { key: 'unpublish', label: this.tKey('agents.lifecycle.unpublish') };
|
||||
}
|
||||
if (agent.raw_status === 'archived') {
|
||||
return { key: 'unpublish', label: 'Restore draft' };
|
||||
return { key: 'unpublish', label: this.tKey('agents.lifecycle.restore_draft') };
|
||||
}
|
||||
return { key: 'publish', label: 'Publish' };
|
||||
return { key: 'publish', label: this.tKey('agents.lifecycle.publish') };
|
||||
},
|
||||
|
||||
lifecycleLabel(agent) {
|
||||
if (agent.raw_status === 'published') return 'Published';
|
||||
if (agent.raw_status === 'archived') return 'Archived';
|
||||
return 'Draft';
|
||||
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) {
|
||||
@@ -384,7 +401,7 @@ document.addEventListener('alpine:init', function() {
|
||||
var endpoint = this.mcpEndpoint(agent);
|
||||
navigator.clipboard.writeText(endpoint).catch(function() {});
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info(endpoint, 'MCP endpoint copied');
|
||||
window.CrankUi.info(endpoint, this.tKey('agents.toast.endpoint_title'));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -406,7 +423,7 @@ document.addEventListener('alpine:init', function() {
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '—';
|
||||
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||
return new Date(dateStr).toLocaleDateString(currentLocale(), {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
@@ -417,6 +434,14 @@ document.addEventListener('alpine:init', function() {
|
||||
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);
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@ var TRANSLATIONS = {
|
||||
'nav.usage': 'Usage',
|
||||
'nav.settings': 'Settings',
|
||||
'nav.logout': 'Log out',
|
||||
'nav.workspaces': 'Your workspaces',
|
||||
'nav.create_workspace': 'Create workspace',
|
||||
'nav.notifications': 'Notifications',
|
||||
|
||||
// Operations page
|
||||
'ops.title': 'Operations',
|
||||
@@ -148,6 +151,87 @@ var TRANSLATIONS = {
|
||||
'btn.revoke': 'Revoke',
|
||||
'btn.copy': 'Copy',
|
||||
|
||||
// Agents page
|
||||
'agents.title': 'Agents',
|
||||
'agents.subtitle': 'Named MCP endpoints that expose a curated subset of operations to an LLM',
|
||||
'agents.new': 'New agent',
|
||||
'agents.stats.total': 'Total agents',
|
||||
'agents.stats.total_sub': 'across this workspace',
|
||||
'agents.stats.published': 'Published',
|
||||
'agents.stats.published_sub': '{value}% of total',
|
||||
'agents.stats.tools': 'Operations exposed',
|
||||
'agents.stats.tools_sub': 'across {count} agents',
|
||||
'agents.stats.calls': 'Calls today',
|
||||
'agents.stats.calls_sub': 'all agents combined',
|
||||
'agents.search': 'Search agents...',
|
||||
'agents.results': '{shown} of {total} agents',
|
||||
'agents.callout': 'Each agent gets its own MCP endpoint. Connect your LLM client to a specific agent so it only sees the tools it needs — not all {count} operations in this workspace.',
|
||||
'agents.loading': 'Loading agents…',
|
||||
'agents.error.title': 'Failed to load agents',
|
||||
'agents.error.api': 'Workspace or API is unavailable',
|
||||
'agents.error.load': 'Failed to load agents',
|
||||
'agents.empty.initial.title': 'No agents yet',
|
||||
'agents.empty.initial.sub': 'Create your first agent to get a dedicated MCP endpoint with a curated set of tools.',
|
||||
'agents.empty.search.title': 'No agents match "{query}"',
|
||||
'agents.empty.search.sub': 'Try a different search term.',
|
||||
'agents.card.tools': 'tools',
|
||||
'agents.card.keys': 'API keys',
|
||||
'agents.card.calls_today': 'calls today',
|
||||
'agents.card.created': 'Created {date}',
|
||||
'agents.card.copy_endpoint': 'Copy endpoint',
|
||||
'agents.action.edit': 'Edit',
|
||||
'agents.action.archive': 'Archive',
|
||||
'agents.action.delete': 'Delete',
|
||||
'agents.lifecycle.publish': 'Publish',
|
||||
'agents.lifecycle.unpublish': 'Unpublish',
|
||||
'agents.lifecycle.restore_draft': 'Restore draft',
|
||||
'agents.lifecycle.published': 'Published',
|
||||
'agents.lifecycle.archived': 'Archived',
|
||||
'agents.lifecycle.draft': 'Draft',
|
||||
'agents.drawer.new_title': 'New agent',
|
||||
'agents.drawer.edit_title': 'Edit agent',
|
||||
'agents.drawer.new_sub': 'Configure a named MCP endpoint for your LLM',
|
||||
'agents.drawer.edit_sub': 'Update agent settings and tool selection',
|
||||
'agents.drawer.identity': 'Identity',
|
||||
'agents.drawer.display_name': 'Display name',
|
||||
'agents.drawer.slug': 'Slug',
|
||||
'agents.drawer.description': 'Description',
|
||||
'agents.drawer.optional': '(optional)',
|
||||
'agents.drawer.required': 'required',
|
||||
'agents.drawer.status': 'Status',
|
||||
'agents.drawer.endpoint': 'MCP endpoint',
|
||||
'agents.drawer.placeholder.name': 'Customer Support',
|
||||
'agents.drawer.placeholder.slug': 'customer-support',
|
||||
'agents.drawer.placeholder.description': 'What does this agent do? What LLM or use-case is it for?',
|
||||
'agents.drawer.operations': 'Operations',
|
||||
'agents.drawer.selected': '{count} selected',
|
||||
'agents.drawer.operations_sub': 'Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.',
|
||||
'agents.drawer.filter_ops': 'Filter operations…',
|
||||
'agents.drawer.ops_no_match': 'No operations match "{query}"',
|
||||
'agents.drawer.ops_selected': '{count} operations selected',
|
||||
'agents.drawer.clear_all': 'Clear all',
|
||||
'agents.drawer.recommendation': "You've selected {count} tools. LLMs perform best with <15 tools per agent. Consider splitting into separate agents by use-case.",
|
||||
'agents.drawer.cancel': 'Cancel',
|
||||
'agents.drawer.create': 'Create agent',
|
||||
'agents.drawer.save': 'Save changes',
|
||||
'agents.toast.saved_title_create': 'Agent created',
|
||||
'agents.toast.saved_title_update': 'Agent updated',
|
||||
'agents.toast.saved_message': '{name} was saved with {count} tool bindings.',
|
||||
'agents.toast.save_error_title': 'Agent update failed',
|
||||
'agents.toast.save_error_message': 'Failed to save agent',
|
||||
'agents.toast.delete_confirm': 'Delete this agent? This cannot be undone.',
|
||||
'agents.toast.delete_title': 'Agent deleted',
|
||||
'agents.toast.delete_message': '{name} was deleted.',
|
||||
'agents.toast.delete_error_title': 'Delete failed',
|
||||
'agents.toast.delete_error_message': 'Failed to delete agent',
|
||||
'agents.toast.lifecycle_title': 'Agent lifecycle updated',
|
||||
'agents.toast.lifecycle_publish': '{name} was published.',
|
||||
'agents.toast.lifecycle_unpublish': '{name} was returned to draft.',
|
||||
'agents.toast.lifecycle_archive': '{name} was archived.',
|
||||
'agents.toast.lifecycle_error_title': 'Lifecycle update failed',
|
||||
'agents.toast.lifecycle_error_message': 'Failed to update agent lifecycle',
|
||||
'agents.toast.endpoint_title': 'MCP endpoint copied',
|
||||
|
||||
// Login
|
||||
'login.title': 'Sign in',
|
||||
'login.subtitle': 'Welcome back to your workspace',
|
||||
@@ -166,6 +250,9 @@ var TRANSLATIONS = {
|
||||
'nav.usage': 'Использование',
|
||||
'nav.settings': 'Настройки',
|
||||
'nav.logout': 'Выйти',
|
||||
'nav.workspaces': 'Ваши воркспейсы',
|
||||
'nav.create_workspace': 'Создать воркспейс',
|
||||
'nav.notifications': 'Уведомления',
|
||||
|
||||
// Operations page
|
||||
'ops.title': 'Операции',
|
||||
@@ -301,6 +388,87 @@ var TRANSLATIONS = {
|
||||
'btn.revoke': 'Отозвать',
|
||||
'btn.copy': 'Копировать',
|
||||
|
||||
// Agents page
|
||||
'agents.title': 'Агенты',
|
||||
'agents.subtitle': 'Именованные MCP endpoint-ы, открывающие LLM ограниченный набор операций',
|
||||
'agents.new': 'Новый агент',
|
||||
'agents.stats.total': 'Всего агентов',
|
||||
'agents.stats.total_sub': 'в этом воркспейсе',
|
||||
'agents.stats.published': 'Опубликованы',
|
||||
'agents.stats.published_sub': '{value}% от общего числа',
|
||||
'agents.stats.tools': 'Открытых операций',
|
||||
'agents.stats.tools_sub': 'в {count} агентах',
|
||||
'agents.stats.calls': 'Вызовов сегодня',
|
||||
'agents.stats.calls_sub': 'по всем агентам',
|
||||
'agents.search': 'Поиск агентов...',
|
||||
'agents.results': '{shown} из {total} агентов',
|
||||
'agents.callout': 'У каждого агента свой MCP endpoint. Подключайте LLM-клиент к конкретному агенту, чтобы он видел только нужные инструменты, а не все {count} операций этого воркспейса.',
|
||||
'agents.loading': 'Загрузка агентов…',
|
||||
'agents.error.title': 'Не удалось загрузить агентов',
|
||||
'agents.error.api': 'Воркспейс или API недоступен',
|
||||
'agents.error.load': 'Не удалось загрузить агентов',
|
||||
'agents.empty.initial.title': 'Агентов пока нет',
|
||||
'agents.empty.initial.sub': 'Создайте первого агента, чтобы получить отдельный MCP endpoint с нужным набором инструментов.',
|
||||
'agents.empty.search.title': 'Нет агентов по запросу "{query}"',
|
||||
'agents.empty.search.sub': 'Попробуйте другой поисковый запрос.',
|
||||
'agents.card.tools': 'инструментов',
|
||||
'agents.card.keys': 'API-ключей',
|
||||
'agents.card.calls_today': 'вызовов сегодня',
|
||||
'agents.card.created': 'Создан {date}',
|
||||
'agents.card.copy_endpoint': 'Скопировать endpoint',
|
||||
'agents.action.edit': 'Редактировать',
|
||||
'agents.action.archive': 'Архивировать',
|
||||
'agents.action.delete': 'Удалить',
|
||||
'agents.lifecycle.publish': 'Опубликовать',
|
||||
'agents.lifecycle.unpublish': 'Снять с публикации',
|
||||
'agents.lifecycle.restore_draft': 'Вернуть в черновик',
|
||||
'agents.lifecycle.published': 'Опубликован',
|
||||
'agents.lifecycle.archived': 'Архивирован',
|
||||
'agents.lifecycle.draft': 'Черновик',
|
||||
'agents.drawer.new_title': 'Новый агент',
|
||||
'agents.drawer.edit_title': 'Редактировать агента',
|
||||
'agents.drawer.new_sub': 'Настройте именованный MCP endpoint для вашей LLM',
|
||||
'agents.drawer.edit_sub': 'Обновите настройки агента и выбор инструментов',
|
||||
'agents.drawer.identity': 'Идентичность',
|
||||
'agents.drawer.display_name': 'Отображаемое имя',
|
||||
'agents.drawer.slug': 'Slug',
|
||||
'agents.drawer.description': 'Описание',
|
||||
'agents.drawer.optional': '(необязательно)',
|
||||
'agents.drawer.required': 'обязательно',
|
||||
'agents.drawer.status': 'Статус',
|
||||
'agents.drawer.endpoint': 'MCP endpoint',
|
||||
'agents.drawer.placeholder.name': 'Customer Support',
|
||||
'agents.drawer.placeholder.slug': 'customer-support',
|
||||
'agents.drawer.placeholder.description': 'Что делает этот агент? Для какой LLM или сценария он нужен?',
|
||||
'agents.drawer.operations': 'Операции',
|
||||
'agents.drawer.selected': 'Выбрано: {count}',
|
||||
'agents.drawer.operations_sub': 'Выберите операции, которые открывает этот агент. LLM, подключенная к агенту, увидит только эти инструменты.',
|
||||
'agents.drawer.filter_ops': 'Фильтр операций…',
|
||||
'agents.drawer.ops_no_match': 'Нет операций по запросу "{query}"',
|
||||
'agents.drawer.ops_selected': 'Выбрано операций: {count}',
|
||||
'agents.drawer.clear_all': 'Очистить все',
|
||||
'agents.drawer.recommendation': 'Сейчас выбрано {count} инструментов. LLM лучше работает, когда у агента меньше 15 инструментов. Подумайте о разбиении по сценариям.',
|
||||
'agents.drawer.cancel': 'Отмена',
|
||||
'agents.drawer.create': 'Создать агента',
|
||||
'agents.drawer.save': 'Сохранить изменения',
|
||||
'agents.toast.saved_title_create': 'Агент создан',
|
||||
'agents.toast.saved_title_update': 'Агент обновлен',
|
||||
'agents.toast.saved_message': '{name} сохранен, привязано инструментов: {count}.',
|
||||
'agents.toast.save_error_title': 'Не удалось обновить агента',
|
||||
'agents.toast.save_error_message': 'Не удалось сохранить агента',
|
||||
'agents.toast.delete_confirm': 'Удалить этого агента? Действие нельзя отменить.',
|
||||
'agents.toast.delete_title': 'Агент удален',
|
||||
'agents.toast.delete_message': 'Агент {name} удален.',
|
||||
'agents.toast.delete_error_title': 'Не удалось удалить',
|
||||
'agents.toast.delete_error_message': 'Не удалось удалить агента',
|
||||
'agents.toast.lifecycle_title': 'Жизненный цикл агента обновлен',
|
||||
'agents.toast.lifecycle_publish': '{name} опубликован.',
|
||||
'agents.toast.lifecycle_unpublish': '{name} возвращен в черновик.',
|
||||
'agents.toast.lifecycle_archive': '{name} архивирован.',
|
||||
'agents.toast.lifecycle_error_title': 'Не удалось обновить жизненный цикл',
|
||||
'agents.toast.lifecycle_error_message': 'Не удалось обновить состояние агента',
|
||||
'agents.toast.endpoint_title': 'MCP endpoint скопирован',
|
||||
|
||||
// Login
|
||||
'login.title': 'Войти',
|
||||
'login.subtitle': 'Добро пожаловать обратно',
|
||||
|
||||
Reference in New Issue
Block a user