392 lines
24 KiB
HTML
392 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<base href="../">
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Crank — Agents</title>
|
|
<link rel="stylesheet" href="css/fonts.css">
|
|
<link rel="stylesheet" href="css/variables.css">
|
|
<link rel="stylesheet" href="css/layout.css">
|
|
<link rel="stylesheet" href="css/pages.css">
|
|
<link rel="stylesheet" href="css/catalog.css">
|
|
<link rel="stylesheet" href="css/wizard.css">
|
|
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
|
</head>
|
|
<body x-data="agents">
|
|
|
|
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
|
|
<nav class="navbar">
|
|
<a href="/" class="nav-logo">
|
|
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
|
<span class="nav-logo-text">Crank</span>
|
|
</a>
|
|
|
|
<!-- Single workspace indicator -->
|
|
<div class="ws-switcher" id="ws-switcher">
|
|
<div class="ws-switcher-trigger">
|
|
<div class="ws-dot" id="ws-dot">A</div>
|
|
<span class="ws-current-name" id="ws-current-name">acme-workspace</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-links">
|
|
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
|
<a href="/agents" class="nav-link active" data-i18n="nav.agents">Agents</a>
|
|
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
|
|
<a href="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</a>
|
|
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
|
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
|
</div>
|
|
|
|
<div class="nav-right">
|
|
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
|
<div class="nav-divider"></div>
|
|
<div class="user-menu" @click.stop>
|
|
<div class="nav-avatar" @click="openDropdown = openDropdown === 'user' ? null : 'user'" data-i18n-title="nav.account" title="Account">AT</div>
|
|
<div class="user-dropdown" x-show="openDropdown === 'user'" x-transition style="display:none">
|
|
<div class="user-dropdown-header">
|
|
<div class="user-dropdown-name">Crank</div>
|
|
<div class="user-dropdown-role" id="user-ws-role">—</div>
|
|
</div>
|
|
<a class="user-dropdown-item" href="/settings">
|
|
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
|
|
<span data-i18n="nav.settings">Settings</span>
|
|
</a>
|
|
<div class="dropdown-divider"></div>
|
|
<button class="user-dropdown-item danger" @click="window.CrankAuth.logout()">
|
|
<svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg>
|
|
<span data-i18n="nav.logout">Log out</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="mobile-nav" hidden>
|
|
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
|
<a href="/agents" class="mobile-nav-link active" data-i18n="nav.agents">Agents</a>
|
|
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
|
|
<a href="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</a>
|
|
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
|
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
|
</div>
|
|
|
|
<!-- ═══════════════════ PAGE ═══════════════════ -->
|
|
<div class="page">
|
|
|
|
<!-- Page header -->
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-heading" data-i18n="agents.title">Agents</h1>
|
|
<p class="page-subheading" x-text="subtitleText()">Group your MCP tools by AI agent.</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>
|
|
<span data-i18n="agents.new">New agent</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-row">
|
|
<div class="stat-card">
|
|
<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" x-text="tKey('agents.stats.total_sub')">across this workspace</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<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 ? tfKey('agents.stats.published_sub', { value: Math.round(activeCount/agents.length*100) }) : ''">—</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<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="tfKey('agents.stats.tools_sub', { count: agents.length })">—</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<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" x-text="tKey('agents.stats.calls_sub')">all agents combined</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search + filter bar -->
|
|
<div class="filter-bar">
|
|
<div class="search-box">
|
|
<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"
|
|
>
|
|
</div>
|
|
<div class="filter-right">
|
|
<span class="results-count" x-show="agentSearch">
|
|
<span x-text="tfKey('agents.results', { shown: filteredAgents.length, total: agents.length })"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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 x-text="agentCalloutText()">Group MCP tools around concrete tasks for a concrete agent. Do not give one LLM too many tools at once: it can increase mistakes and hallucinations. Each agent has its own API keys.</span>
|
|
</div>
|
|
|
|
<!-- Loading -->
|
|
<div class="loading-spinner" x-show="loading">
|
|
<div class="spinner"></div>
|
|
<span data-i18n="agents.loading">Loading agents…</span>
|
|
</div>
|
|
|
|
<div class="empty-state" x-show="!loading && loadError">
|
|
<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>
|
|
|
|
<!-- Empty state: no agents at all -->
|
|
<div class="empty-state" x-show="!loading && !loadError && agents.length === 0">
|
|
<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" data-i18n="agents.empty.initial.title">No agents yet</div>
|
|
<div class="empty-state-sub" x-text="emptyStateText()">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</div>
|
|
<div class="empty-state-sub" style="margin-top:8px;" x-text="tKey('agents.empty.initial.next_step')">After creating an agent, issue an API key for it on the API Keys page.</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>
|
|
<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" 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 -->
|
|
<div class="agents-grid" x-show="!loading && !loadError && filteredAgents.length > 0">
|
|
<template x-for="agent in filteredAgents" :key="agent.id">
|
|
<div class="agent-card">
|
|
|
|
<!-- Card header -->
|
|
<div class="agent-card-header">
|
|
<div class="agent-card-icon">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><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="agent-card-title-wrap">
|
|
<div class="agent-card-name" x-text="agent.display_name"></div>
|
|
<div class="agent-card-slug" x-text="agent.slug"></div>
|
|
</div>
|
|
<span :class="statusClass(agent.status)" x-text="lifecycleLabel(agent)"></span>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="agent-card-desc" x-text="agent.description"></div>
|
|
|
|
<!-- Stats row -->
|
|
<div class="agent-card-stats">
|
|
<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 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 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 data-i18n="agents.card.calls_today">calls today</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MCP endpoint -->
|
|
<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="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>
|
|
<div class="agent-card-date" style="margin-top:8px;" x-text="endpointHelpText(agent)"></div>
|
|
|
|
<!-- Footer -->
|
|
<div class="agent-card-footer">
|
|
<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>
|
|
<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>
|
|
<span x-text="lifecycleAction(agent).label"></span>
|
|
</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>
|
|
<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>
|
|
<span data-i18n="agents.action.delete">Delete</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
</div><!-- /page -->
|
|
|
|
|
|
<!-- ═══════════════════ DRAWER ═══════════════════ -->
|
|
<!-- Overlay -->
|
|
<div class="drawer-overlay" x-show="drawerOpen" x-transition:enter="transition-opacity" @click="closeDrawer()" style="display:none"></div>
|
|
|
|
<!-- Drawer panel -->
|
|
<div class="drawer" :class="{ open: drawerOpen }" style="display:none" x-show="drawerOpen">
|
|
|
|
<!-- Drawer header -->
|
|
<div class="drawer-header">
|
|
<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="drawerSubText()"></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>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Drawer body -->
|
|
<div class="drawer-body">
|
|
|
|
<!-- Identity section -->
|
|
<div class="drawer-section">
|
|
<div class="drawer-section-title" data-i18n="agents.drawer.identity">Identity</div>
|
|
|
|
<div class="form-group" style="margin-bottom: 14px;">
|
|
<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"><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">
|
|
<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 class="form-hint" data-i18n="agents.drawer.slug_hint">Slug is used as part of the endpoint to identify the agent.</div>
|
|
</div>
|
|
|
|
<div class="form-group" style="margin-bottom: 14px;">
|
|
<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 scenario is it for?"
|
|
x-model="form.description"></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<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> <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> <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> <span data-i18n="agents.lifecycle.archived">Archived</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Operations picker section -->
|
|
<div class="drawer-section">
|
|
<div class="drawer-section-header">
|
|
<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" x-text="operationsSubText()">Select the MCP tools available to this agent.</div>
|
|
<div class="agents-rec-callout" x-show="agentToolFindings.length > 0" style="display:none; margin-bottom: 12px;">
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="8" cy="8" r="7"/><path d="M8 4.5v4M8 11.5v.2"/>
|
|
</svg>
|
|
<div>
|
|
<strong data-i18n="agents.drawer.finding.title">Recommendation</strong>
|
|
<template x-for="finding in agentToolFindings" :key="finding">
|
|
<div x-text="finding"></div>
|
|
</template>
|
|
</div>
|
|
</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" 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>
|
|
</div>
|
|
<!-- List -->
|
|
<div class="ops-picker-list">
|
|
<template x-for="op in filteredOps" :key="op.id || op.name">
|
|
<div class="ops-picker-item" :class="{ selected: isOpSelected(op.id || op.name) }" @click="toggleOp(op.id || op.name)">
|
|
<div class="ops-picker-check">
|
|
<svg x-show="isOpSelected(op.id || op.name)" width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
|
</div>
|
|
<div class="ops-picker-info">
|
|
<div class="ops-picker-name" x-text="op.display_name || op.name"></div>
|
|
<div class="ops-picker-slug" x-text="op.name"></div>
|
|
</div>
|
|
<span :class="protocolBadge(op)" x-text="protocolLabel(op)" style="font-size:10px; padding: 2px 6px;"></span>
|
|
<div class="ops-picker-status-dot" :class="'ops-status-' + op.status"></div>
|
|
</div>
|
|
</template>
|
|
<div class="ops-picker-empty" x-show="filteredOps.length === 0">
|
|
<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="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 x-text="tfKey('agents.drawer.recommendation', { count: form.selectedOps.length })">You've selected tools.</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /drawer-body -->
|
|
|
|
<!-- Drawer footer -->
|
|
<div class="drawer-footer">
|
|
<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' ? tKey('agents.drawer.create') : tKey('agents.drawer.save')">
|
|
Create agent
|
|
</button>
|
|
</div>
|
|
|
|
</div><!-- /drawer -->
|
|
|
|
|
|
<script src="%CRANK_BUNDLE_AGENTS%"></script>
|
|
</body>
|
|
</html>
|