feat: localize remaining alpine ui views

This commit is contained in:
a.tolmachev
2026-04-03 00:24:46 +03:00
parent 140548bb20
commit 2b488749c1
21 changed files with 594 additions and 254 deletions
+4 -4
View File
@@ -56,7 +56,7 @@
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<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-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
@@ -66,11 +66,11 @@
</button>
<div class="nav-divider"></div>
<div class="user-menu" @click.stop>
<div class="nav-avatar" @click="openDropdown = openDropdown === 'user' ? null : 'user'" title="Account">AT</div>
<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">Operator</div>
<div class="user-dropdown-role" id="user-ws-role">Admin · acme-workspace</div>
<div class="user-dropdown-name">Crank</div>
<div class="user-dropdown-role" id="user-ws-role"></div>
</div>
<button class="user-dropdown-item" onclick="window.location.href='/settings'">
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
+4 -4
View File
@@ -68,7 +68,7 @@
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<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-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
@@ -82,11 +82,11 @@
<div class="nav-divider"></div>
<div class="user-menu">
<div class="nav-avatar" title="Account">AT</div>
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</div>
<div class="user-dropdown-name">Crank</div>
<div class="user-dropdown-role"></div>
</div>
<button class="user-dropdown-item" data-action="settings-link">
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
+4 -4
View File
@@ -53,7 +53,7 @@
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<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-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
@@ -63,11 +63,11 @@
</button>
<div class="nav-divider"></div>
<div class="user-menu">
<div class="nav-avatar" title="Account">AT</div>
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</div>
<div class="user-dropdown-name">Crank</div>
<div class="user-dropdown-role"></div>
</div>
<button class="user-dropdown-item" data-action="settings-link">
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
+7 -7
View File
@@ -53,7 +53,7 @@
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<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-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
@@ -63,11 +63,11 @@
</button>
<div class="nav-divider"></div>
<div class="user-menu">
<div class="nav-avatar" title="Account">AT</div>
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</div>
<div class="user-dropdown-name">Crank</div>
<div class="user-dropdown-role"></div>
</div>
<button class="user-dropdown-item" data-action="settings-link">
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
@@ -213,7 +213,7 @@
<div class="avatar-upload">
<div class="avatar-large" id="profile-avatar">AT</div>
<div class="avatar-upload-actions">
<div class="avatar-name" id="profile-display-name">Operator</div>
<div class="avatar-name" id="profile-display-name">Crank</div>
<div class="avatar-sub" id="profile-email">operator@acme-workspace</div>
<div class="field-hint" data-i18n="settings.profile.avatar_hint">Your avatar is generated from your display name and email.</div>
</div>
@@ -223,7 +223,7 @@
<div class="field-row">
<div class="field-group">
<label class="field-label" data-i18n="settings.profile.first_name">First name</label>
<input class="field-input" id="field-firstname" type="text" value="Operator" autocomplete="given-name">
<input class="field-input" id="field-firstname" type="text" value="Crank" autocomplete="given-name">
</div>
<div class="field-group">
<label class="field-label" data-i18n="settings.profile.last_name">Last name</label>
@@ -293,7 +293,7 @@
<div style="padding: 8px 20px 16px;">
<div class="member-row">
<div class="member-info">
<div class="member-name">Browser session — <span style="color:var(--green);font-size:12px;" data-i18n="settings.session.current">current</span></div>
<div class="member-name"><span data-i18n="settings.session.browser">Browser session</span><span style="color:var(--green);font-size:12px;" data-i18n="settings.session.current">current</span></div>
<div class="member-email" id="settings-session-summary" data-i18n="settings.session.loading">Loading current session details…</div>
</div>
</div>
+4 -4
View File
@@ -53,7 +53,7 @@
<a href="/usage" class="nav-link active" data-i18n="nav.usage">Usage</a>
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<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-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
@@ -63,11 +63,11 @@
</button>
<div class="nav-divider"></div>
<div class="user-menu">
<div class="nav-avatar" title="Account">AT</div>
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</div>
<div class="user-dropdown-name">Crank</div>
<div class="user-dropdown-role"></div>
</div>
<button class="user-dropdown-item" data-action="settings-link">
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
+10 -10
View File
@@ -55,7 +55,7 @@
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<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-kbd">
<svg width="12" height="12"><use href="../../icons/general/grid.svg#icon"/></svg>
@@ -69,11 +69,11 @@
<div class="nav-divider"></div>
<div class="user-menu">
<div class="nav-avatar" title="Account">AT</div>
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</div>
<div class="user-dropdown-name">Crank</div>
<div class="user-dropdown-role"></div>
</div>
<button class="user-dropdown-item" data-action="settings-link">
<svg width="13" height="13"><use href="../../icons/general/settings.svg#icon"/></svg>
@@ -141,7 +141,7 @@
<div class="step-item active">
<div class="step-indicator">1</div>
<div class="step-content">
<div class="step-number">Step 1</div>
<div class="step-number" data-step="1">Step 1</div>
<div class="step-name" data-i18n="wizard.step_name.protocol">Protocol</div>
<div class="step-status-text" data-i18n="wizard.status.in_progress">In progress</div>
</div>
@@ -150,7 +150,7 @@
<div class="step-item pending">
<div class="step-indicator">2</div>
<div class="step-content">
<div class="step-number">Step 2</div>
<div class="step-number" data-step="2">Step 2</div>
<div class="step-name" data-i18n="wizard.step_name.upstream">Upstream target</div>
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
</div>
@@ -159,7 +159,7 @@
<div class="step-item pending" id="sidebar-step-3">
<div class="step-indicator">3</div>
<div class="step-content">
<div class="step-number">Step 3</div>
<div class="step-number" data-step="3">Step 3</div>
<div class="step-name" id="sidebar-step-3-name">Request config</div>
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
</div>
@@ -168,7 +168,7 @@
<div class="step-item pending">
<div class="step-indicator">4</div>
<div class="step-content">
<div class="step-number">Step 4</div>
<div class="step-number" data-step="4">Step 4</div>
<div class="step-name" data-i18n="wizard.step_name.tool">Tool config</div>
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
</div>
@@ -177,7 +177,7 @@
<div class="step-item pending">
<div class="step-indicator">5</div>
<div class="step-content">
<div class="step-number">Step 5</div>
<div class="step-number" data-step="5">Step 5</div>
<div class="step-name" data-i18n="wizard.step_name.mapping">Mapping</div>
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
</div>
@@ -221,7 +221,7 @@
<div class="action-bar-spacer"></div>
<span class="step-counter">Step <strong>1</strong> of 5</span>
<span class="step-counter" data-step-counter>Step <strong>1</strong> of 5</span>
<button id="btn-continue" class="btn btn-continue" type="button">
<span data-i18n="wizard.button.continue">Continue</span>
+9 -9
View File
@@ -23,8 +23,8 @@
<path d="M12 3a15.3 15.3 0 010 18M12 3a15.3 15.3 0 000 18"/>
</svg>
</div>
<div class="protocol-card-name">REST / HTTP</div>
<div class="protocol-card-tagline">Standard HTTP verbs over a RESTful endpoint. The most broadly supported option.</div>
<div class="protocol-card-name" data-i18n="wizard.step1.rest_name">REST / HTTP</div>
<div class="protocol-card-tagline" data-i18n="wizard.step1.rest_tagline">Standard HTTP verbs over a RESTful endpoint. The most broadly supported option.</div>
<div class="protocol-card-tags">
<span class="protocol-tag">GET</span>
<span class="protocol-tag">POST</span>
@@ -49,11 +49,11 @@
<circle cx="2.5" cy="7.5" r="1.5" fill="#bc8cff"/>
</svg>
</div>
<div class="protocol-card-name">GraphQL</div>
<div class="protocol-card-tagline">Query and mutate a GraphQL schema. Supports queries and mutations over one fixed operation.</div>
<div class="protocol-card-name" data-i18n="wizard.step1.graphql_name">GraphQL</div>
<div class="protocol-card-tagline" data-i18n="wizard.step1.graphql_tagline">Query and mutate a GraphQL schema. Supports queries and mutations over one fixed operation.</div>
<div class="protocol-card-tags">
<span class="protocol-tag">Query</span>
<span class="protocol-tag">Mutation</span>
<span class="protocol-tag" data-i18n="wizard.step1.query">Query</span>
<span class="protocol-tag" data-i18n="wizard.step1.mutation">Mutation</span>
</div>
</div>
@@ -68,10 +68,10 @@
<path d="M10 8h4M10 12h4M10 16h4"/>
</svg>
</div>
<div class="protocol-card-name">gRPC</div>
<div class="protocol-card-tagline">High-performance binary RPC over HTTP/2. Ideal for low-latency internal services.</div>
<div class="protocol-card-name" data-i18n="wizard.step1.grpc_name">gRPC</div>
<div class="protocol-card-tagline" data-i18n="wizard.step1.grpc_tagline">High-performance binary RPC over HTTP/2. Ideal for low-latency internal services.</div>
<div class="protocol-card-tags">
<span class="protocol-tag">Unary</span>
<span class="protocol-tag" data-i18n="wizard.step1.unary">Unary</span>
<span class="protocol-tag">Protobuf</span>
</div>
</div>
+21 -21
View File
@@ -21,10 +21,10 @@
</svg>
</div>
<div>
<div class="config-card-title">Upstream</div>
<div class="config-card-subtitle">Shared host, base URL, and authentication</div>
<div class="config-card-title" data-i18n="wizard.step2.upstream_title">Upstream</div>
<div class="config-card-subtitle" data-i18n="wizard.step2.upstream_subtitle">Shared host, base URL, and authentication</div>
</div>
<span class="config-card-optional" style="color: var(--error, #f87171);">Required</span>
<span class="config-card-optional" style="color: var(--error, #f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body" style="gap: 12px; padding: 16px;">
@@ -32,7 +32,7 @@
<div class="upstream-combobox" id="upstream-combobox">
<div class="upstream-combobox-trigger" id="upstream-combobox-trigger" onclick="toggleUpstreamDropdown(event)">
<div class="upstream-combobox-value" id="upstream-combobox-value">
<span class="upstream-combobox-placeholder">Select an upstream…</span>
<span class="upstream-combobox-placeholder" data-i18n="wizard.step2.upstream_placeholder">Select an upstream…</span>
</div>
<svg class="upstream-combobox-chevron" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 6l4 4 4-4"/>
@@ -45,7 +45,7 @@
<svg width="13" height="13" 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="upstream-search-input" id="upstream-search" type="text" placeholder="Search by name or URL…" oninput="filterUpstreams(this.value)" autocomplete="off" spellcheck="false">
<input class="upstream-search-input" id="upstream-search" type="text" data-i18n-ph="wizard.step2.search_placeholder" placeholder="Search by name or URL…" oninput="filterUpstreams(this.value)" autocomplete="off" spellcheck="false">
</div>
<div class="upstream-dropdown-list" id="upstream-dropdown-list">
<!-- populated by JS -->
@@ -58,7 +58,7 @@
<div class="upstream-preview-name" id="upstream-preview-name"></div>
<div class="upstream-preview-url" id="upstream-preview-url"></div>
<span class="upstream-auth-badge" id="upstream-preview-badge"></span>
<button class="upstream-preview-change" onclick="openUpstreamDropdown()">Change</button>
<button class="upstream-preview-change" onclick="openUpstreamDropdown()" data-i18n="wizard.step2.change">Change</button>
</div>
<!-- Register new upstream trigger row -->
@@ -69,7 +69,7 @@
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="2.2" stroke-linecap="round">
<path d="M8 3v10M3 8h10"/>
</svg>
<span>Register new upstream</span>
<span data-i18n="wizard.step2.register_new">Register new upstream</span>
</div>
<!-- Register new upstream form (kept as-is per design) -->
@@ -77,18 +77,18 @@
<div class="upstream-new-form-inner">
<div class="form-row" style="grid-template-columns: 1fr 2fr;">
<div class="form-group">
<label class="form-label">Name <span class="form-label-required">required</span></label>
<label class="form-label"><span data-i18n="wizard.step2.name">Name</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
<input class="form-input" id="new-upstream-name" type="text" placeholder="e.g. acme-api" autocomplete="off" spellcheck="false">
<div class="form-hint">Unique identifier for this upstream.</div>
<div class="form-hint" data-i18n="wizard.step2.unique_hint">Unique identifier for this upstream.</div>
</div>
<div class="form-group">
<label class="form-label">Base URL <span class="form-label-required">required</span></label>
<label class="form-label"><span data-i18n="wizard.step2.base_url">Base URL</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
<input class="form-input input-mono" id="new-upstream-url" type="text" placeholder="https://api.example.com" autocomplete="off" spellcheck="false">
<div class="form-hint">Root URL — no trailing slash. Supports <code style="font-family:monospace;font-size:11px;color:var(--accent);">${secrets.*}</code>.</div>
<div class="form-hint" data-i18n="wizard.step2.base_url_hint">Root URL — no trailing slash. Supports ${secrets.*}.</div>
</div>
</div>
<div class="form-group">
<label class="form-label">Auth headers</label>
<label class="form-label" data-i18n="wizard.step2.auth_headers">Auth headers</label>
<div class="code-block" style="border-radius: 6px;">
<div class="code-toolbar">
<div class="code-dots"><span></span><span></span><span></span></div>
@@ -98,11 +98,11 @@
"Authorization": "${secrets.API_KEY}"
}</textarea>
</div>
<div class="form-hint">These headers are sent on every request to this upstream. Use secrets for credentials.</div>
<div class="form-hint" data-i18n="wizard.step2.auth_headers_hint">These headers are sent on every request to this upstream. Use secrets for credentials.</div>
</div>
<div style="display:flex; gap:8px; margin-top:4px;">
<button class="btn-primary-sm" onclick="saveNewUpstream(event)">Save upstream</button>
<button class="btn-ghost-sm" onclick="cancelNewUpstream(event)">Cancel</button>
<button class="btn-primary-sm" onclick="saveNewUpstream(event)" data-i18n="wizard.step2.save_upstream">Save upstream</button>
<button class="btn-ghost-sm" onclick="cancelNewUpstream(event)" data-i18n="btn.cancel">Cancel</button>
</div>
</div>
</div>
@@ -119,19 +119,19 @@
</svg>
</div>
<div>
<div class="config-card-title">Endpoint</div>
<div class="config-card-subtitle">Path and HTTP method for this specific operation</div>
<div class="config-card-title" data-i18n="wizard.step2.endpoint_title">Endpoint</div>
<div class="config-card-subtitle" data-i18n="wizard.step2.endpoint_subtitle">Path and HTTP method for this specific operation</div>
</div>
<span class="config-card-optional" style="color: var(--error, #f87171);">Required</span>
<span class="config-card-optional" style="color: var(--error, #f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-group">
<label class="form-label">
Path template
<span class="form-label-required">required</span>
<span data-i18n="wizard.step2.path_template">Path template</span>
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
</label>
<input class="form-input input-mono" id="endpoint-path" type="text" value="/v1/leads" autocomplete="off" spellcheck="false">
<div class="form-hint">Appended to the upstream base URL. Use <code style="font-family:monospace;font-size:11px;color:var(--accent);">{param}</code> for path variables.</div>
<div class="form-hint" data-i18n="wizard.step2.path_hint">Appended to the upstream base URL. Use {param} for path variables.</div>
</div>
</div>
</div>
+10 -12
View File
@@ -18,20 +18,20 @@
</svg>
</div>
<div>
<div class="config-card-title">Operation type</div>
<div class="config-card-subtitle">GraphQL operation kind — subscriptions are not supported</div>
<div class="config-card-title" data-i18n="wizard.step3.graphql.type_title">Operation type</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.graphql.type_subtitle">GraphQL operation kind — subscriptions are not supported</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body">
<div class="gql-type-grid">
<button class="gql-type-card active" data-gql-type="query" onclick="selectGqlType(this)">
<div class="gql-type-keyword">query</div>
<div class="gql-type-desc">Fetch data without side-effects</div>
<div class="gql-type-desc" data-i18n="wizard.step3.graphql.query_desc">Fetch data without side-effects</div>
</button>
<button class="gql-type-card" data-gql-type="mutation" onclick="selectGqlType(this)">
<div class="gql-type-keyword">mutation</div>
<div class="gql-type-desc">Create, update or delete data</div>
<div class="gql-type-desc" data-i18n="wizard.step3.graphql.mutation_desc">Create, update or delete data</div>
</button>
</div>
</div>
@@ -47,10 +47,10 @@
</svg>
</div>
<div>
<div class="config-card-title">Query document</div>
<div class="config-card-subtitle">GraphQL document with typed variable declarations</div>
<div class="config-card-title" data-i18n="wizard.step3.graphql.doc_title">Query document</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.graphql.doc_subtitle">GraphQL document with typed variable declarations</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body" style="gap: 0; padding: 0;">
<div class="code-block" style="border-radius: 0; border: none;">
@@ -82,10 +82,8 @@
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title">Variables are mapped in step 5</div>
<div class="info-callout-text">
Declare all variables in the query document using the <code style="font-family:monospace;font-size:11px;color:var(--accent);">$variable: Type</code> syntax. In step 5 you will map MCP input fields to these variables. Crank serialises the final <code style="font-family:monospace;font-size:11px;color:var(--accent);">{"query","variables"}</code> payload automatically.
</div>
<div class="info-callout-title" data-i18n="wizard.step3.graphql.variables_title">Variables are mapped in step 5</div>
<div class="info-callout-text" data-i18n="wizard.step3.graphql.variables_body">Declare all variables in the query document using the $variable: Type syntax. In step 5 you will map MCP input fields to these variables. Crank serializes the final {"query","variables"} payload automatically.</div>
</div>
</div>
+42 -42
View File
@@ -12,15 +12,15 @@
<div class="grpc-source-tabs">
<button class="grpc-source-btn active" data-source="proto" onclick="selectGrpcSource('proto')">
<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="M9 2H4a1 1 0 00-1 1v10a1 1 0 001 1h8a1 1 0 001-1V6z"/><path d="M9 2v4h4"/></svg>
Proto file
<span data-i18n="wizard.step3.grpc.source_proto">Proto file</span>
</button>
<button class="grpc-source-btn" data-source="reflection" onclick="selectGrpcSource('reflection')">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.1 3.1l1.4 1.4M11.5 11.5l1.4 1.4M3.1 12.9l1.4-1.4M11.5 4.5l1.4-1.4"/></svg>
Descriptor set
<span data-i18n="wizard.step3.grpc.source_reflection">Descriptor set</span>
</button>
<button class="grpc-source-btn" data-source="manual" onclick="selectGrpcSource('manual')">
<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>
Manual
<span data-i18n="wizard.step3.grpc.source_manual">Manual</span>
</button>
</div>
@@ -32,10 +32,10 @@
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 2H4a1 1 0 00-1 1v10a1 1 0 001 1h8a1 1 0 001-1V6z"/><path d="M9 2v4h4"/></svg>
</div>
<div>
<div class="config-card-title">Proto definition</div>
<div class="config-card-subtitle">Upload or paste your .proto file</div>
<div class="config-card-title" data-i18n="wizard.step3.grpc.proto_title">Proto definition</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.grpc.proto_subtitle">Upload or paste your .proto file</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body" style="gap: 12px; padding: 16px;">
<div class="proto-dropzone" id="proto-dropzone"
@@ -47,19 +47,19 @@
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<div class="proto-dropzone-title">Drop <code style="font-family:monospace;font-size:12px;">.proto</code> file here</div>
<div class="proto-dropzone-sub">or click to browse</div>
<div class="proto-dropzone-title" data-i18n="wizard.step3.grpc.drop_title">Drop .proto file here</div>
<div class="proto-dropzone-sub" data-i18n="wizard.step3.grpc.drop_subtitle">or click to browse</div>
<input type="file" id="proto-file-input" accept=".proto" style="display:none" onchange="handleProtoFile(event)">
</div>
<div class="proto-file-info" id="proto-file-info" style="display:none">
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 2H4a1 1 0 00-1 1v10a1 1 0 001 1h8a1 1 0 001-1V6z"/><path d="M9 2v4h4"/></svg>
<span class="proto-file-name" id="proto-file-name"></span>
<span class="proto-file-size" id="proto-file-size"></span>
<button class="proto-file-change" onclick="resetProto()">Change file</button>
<button class="proto-file-change" onclick="resetProto()" data-i18n="wizard.step3.grpc.change_file">Change file</button>
</div>
<div style="display:flex; align-items:center; gap:10px;">
<div style="flex:1; height:1px; background:var(--border-subtle);"></div>
<button class="btn-ghost-sm" onclick="toggleProtoPaste()" id="proto-paste-btn">Paste content</button>
<button class="btn-ghost-sm" onclick="toggleProtoPaste()" id="proto-paste-btn" data-i18n="wizard.step3.grpc.paste_content">Paste content</button>
<div style="flex:1; height:1px; background:var(--border-subtle);"></div>
</div>
<div id="proto-paste-area" style="display:none">
@@ -72,8 +72,8 @@
placeholder='syntax = "proto3";&#10;&#10;package acme;&#10;&#10;service ContactService {&#10; rpc GetContact (GetContactRequest) returns (Contact) {}&#10;}' spellcheck="false"></textarea>
</div>
<div style="display:flex; gap: 8px; margin-top: 8px;">
<button class="btn-primary-sm" onclick="parseProtoPasted()">Parse proto</button>
<button class="btn-ghost-sm" onclick="cancelProtoPaste()">Cancel</button>
<button class="btn-primary-sm" onclick="parseProtoPasted()" data-i18n="wizard.step3.grpc.parse_proto">Parse proto</button>
<button class="btn-ghost-sm" onclick="cancelProtoPaste()" data-i18n="btn.cancel">Cancel</button>
</div>
</div>
</div>
@@ -82,14 +82,14 @@
<div class="info-callout" id="proto-streaming-notice" style="margin-bottom:20px;display:none">
<svg class="info-callout-icon" 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>
<div class="info-callout-body">
<div class="info-callout-title">Streaming methods excluded</div>
<div class="info-callout-text"><span id="proto-streaming-count">0</span> streaming method(s) found and hidden — not compatible with MCP tool calls.</div>
<div class="info-callout-title" data-i18n="wizard.step3.grpc.streaming_excluded">Streaming methods excluded</div>
<div class="info-callout-text" id="proto-streaming-text"><span id="proto-streaming-count">0</span> streaming method(s) found and hidden — not compatible with MCP tool calls.</div>
</div>
</div>
<div class="config-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div><div class="config-card-title">Select method</div><div class="config-card-subtitle" id="proto-services-summary"></div></div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
<div><div class="config-card-title" data-i18n="wizard.step3.grpc.select_method">Select method</div><div class="config-card-subtitle" id="proto-services-summary"></div></div>
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body" style="padding: 0 16px 16px; gap: 0;">
<div id="proto-services-list"></div>
@@ -106,8 +106,8 @@
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.1 3.1l1.4 1.4M11.5 11.5l1.4 1.4M3.1 12.9l1.4-1.4M11.5 4.5l1.4-1.4"/></svg>
</div>
<div>
<div class="config-card-title">Descriptor-driven discovery</div>
<div class="config-card-subtitle">Use the saved draft and a descriptor set to discover services through the backend</div>
<div class="config-card-title" data-i18n="wizard.step3.grpc.discovery_live_title">Descriptor-driven discovery</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.grpc.discovery_live_subtitle">Use the saved draft and a descriptor set to discover services through the backend</div>
</div>
</div>
<div class="config-card-body" style="gap: 14px; padding: 16px;">
@@ -115,30 +115,30 @@
<div class="grpc-reflect-upstream">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="6" height="10" rx="1"/><rect x="9" y="3" width="6" height="10" rx="1"/></svg>
<div class="grpc-reflect-upstream-info">
<span class="grpc-reflect-upstream-name" id="grpc-reflect-upstream-name">No upstream selected</span>
<span class="grpc-reflect-upstream-url" id="grpc-reflect-upstream-url">Select an upstream in step 2 first</span>
<span class="grpc-reflect-upstream-name" id="grpc-reflect-upstream-name" data-i18n="wizard.step3.grpc.no_upstream">No upstream selected</span>
<span class="grpc-reflect-upstream-url" id="grpc-reflect-upstream-url" data-i18n="wizard.step3.grpc.select_upstream_first">Select an upstream in step 2 first</span>
</div>
</div>
<!-- Idle -->
<div id="grpc-reflect-idle">
<button class="btn-primary-sm" onclick="startReflection()">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px"><circle cx="8" cy="8" r="2.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2"/></svg>
Open live descriptor tools
<span data-i18n="wizard.step3.grpc.open_tools">Open live descriptor tools</span>
</button>
</div>
<!-- Loading -->
<div id="grpc-reflect-loading" style="display:none">
<div class="grpc-reflect-status">
<div class="grpc-reflect-spinner"></div>
<span id="grpc-reflect-status-text">Connecting…</span>
<span id="grpc-reflect-status-text" data-i18n="wizard.step3.grpc.connecting">Connecting…</span>
</div>
</div>
<!-- Error -->
<div id="grpc-reflect-error" style="display:none">
<div class="grpc-reflect-error-row">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="var(--error,#f87171)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="7"/><path d="M8 5v3M8 10.5v.5"/></svg>
<span id="grpc-reflect-error-text">Live descriptor discovery is handled in step 5.</span>
<button class="proto-file-change" onclick="startReflection()">Retry</button>
<span id="grpc-reflect-error-text" data-i18n="wizard.step3.grpc.live_step5">Live descriptor discovery is handled in step 5.</span>
<button class="proto-file-change" onclick="startReflection()" data-i18n="wizard.step3.grpc.retry">Retry</button>
</div>
</div>
</div>
@@ -147,14 +147,14 @@
<div class="info-callout" id="reflect-streaming-notice" style="margin-bottom:20px;display:none">
<svg class="info-callout-icon" 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>
<div class="info-callout-body">
<div class="info-callout-title">Streaming methods excluded</div>
<div class="info-callout-title" data-i18n="wizard.step3.grpc.streaming_excluded">Streaming methods excluded</div>
<div class="info-callout-text"><span id="reflect-streaming-count">0</span> streaming method(s) discovered and hidden.</div>
</div>
</div>
<div class="config-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div><div class="config-card-title">Select method</div><div class="config-card-subtitle" id="reflect-services-summary"></div></div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
<div><div class="config-card-title" data-i18n="wizard.step3.grpc.select_method">Select method</div><div class="config-card-subtitle" id="reflect-services-summary"></div></div>
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body" style="padding: 0 16px 16px; gap: 0;">
<div id="reflect-services-list"></div>
@@ -171,27 +171,27 @@
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8h12M10 4l4 4-4 4"/></svg>
</div>
<div>
<div class="config-card-title">Method route</div>
<div class="config-card-subtitle">Full gRPC path to the unary method</div>
<div class="config-card-title" data-i18n="wizard.step3.grpc.manual_title">Method route</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.grpc.manual_subtitle">Full gRPC path to the unary method</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-group">
<label class="form-label">gRPC route <span class="form-label-required">required</span></label>
<label class="form-label"><span data-i18n="wizard.step3.grpc.route">gRPC route</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
<input class="form-input input-mono" id="grpc-manual-route" type="text" placeholder="/package.ServiceName/MethodName" autocomplete="off" spellcheck="false" oninput="grpcManualRouteInput(this.value)">
<div class="form-hint">Format: <code style="font-family:monospace;font-size:11px;color:var(--accent);">/[package.]ServiceName/MethodName</code></div>
<div class="form-hint" data-i18n="wizard.step3.grpc.route_hint">Format: /[package.]ServiceName/MethodName</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Request type <span class="form-label-optional">(optional)</span></label>
<label class="form-label"><span data-i18n="wizard.step3.grpc.request_type">Request type</span> <span class="form-label-optional" data-i18n="workspace_setup.optional">(optional)</span></label>
<input class="form-input input-mono" id="grpc-manual-req-type" type="text" placeholder="GetContactRequest" autocomplete="off" spellcheck="false" oninput="grpcManualTypeInput()">
<div class="form-hint">Protobuf message name, for documentation.</div>
<div class="form-hint" data-i18n="wizard.step3.grpc.proto_doc_hint">Protobuf message name, for documentation.</div>
</div>
<div class="form-group">
<label class="form-label">Response type <span class="form-label-optional">(optional)</span></label>
<label class="form-label"><span data-i18n="wizard.step3.grpc.response_type">Response type</span> <span class="form-label-optional" data-i18n="workspace_setup.optional">(optional)</span></label>
<input class="form-input input-mono" id="grpc-manual-res-type" type="text" placeholder="Contact" autocomplete="off" spellcheck="false" oninput="grpcManualTypeInput()">
<div class="form-hint">Protobuf message name, for documentation.</div>
<div class="form-hint" data-i18n="wizard.step3.grpc.proto_doc_hint">Protobuf message name, for documentation.</div>
</div>
</div>
</div>
@@ -199,8 +199,8 @@
<div class="info-callout" style="margin-bottom: 20px;">
<svg class="info-callout-icon" 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 11V8M8 5v-.5"/></svg>
<div class="info-callout-body">
<div class="info-callout-title">Input and output schemas are defined in step 4</div>
<div class="info-callout-text">In manual mode you only need the route. The MCP tool input/output schemas — which the LLM uses to call this tool — are configured in the next step regardless of Protobuf definitions.</div>
<div class="info-callout-title" data-i18n="wizard.step3.grpc.schema_title">Input and output schemas are defined in step 4</div>
<div class="info-callout-text" data-i18n="wizard.step3.grpc.schema_body">In manual mode you only need the route. The MCP tool input/output schemas — which the LLM uses to call this tool — are configured in the next step regardless of Protobuf definitions.</div>
</div>
</div>
</div><!-- /grpc-src-manual -->
@@ -212,27 +212,27 @@
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8h12M10 4l4 4-4 4"/></svg>
</div>
<div>
<div class="config-card-title">Method signature</div>
<div class="config-card-title" data-i18n="wizard.step3.grpc.signature_title">Method signature</div>
<div class="config-card-subtitle" id="grpc-method-subtitle"></div>
</div>
</div>
<div class="config-card-body" style="gap: 16px; padding: 16px;">
<div class="proto-path-row">
<span class="proto-path-label">gRPC route</span>
<span class="proto-path-label" data-i18n="wizard.step3.grpc.route_label">gRPC route</span>
<code class="proto-path-value" id="grpc-path-value"></code>
</div>
<div class="proto-sig-grid" id="grpc-detail-sig">
<div class="proto-sig-col">
<div class="proto-sig-col-header">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 8h12M8 3l5 5-5 5"/></svg>
Request <code class="proto-type-badge" id="grpc-req-type"></code>
<span data-i18n="wizard.step3.grpc.request_label">Request</span> <code class="proto-type-badge" id="grpc-req-type"></code>
</div>
<div class="proto-field-list" id="grpc-req-fields"></div>
</div>
<div class="proto-sig-col">
<div class="proto-sig-col-header">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14 8H2M8 3l-5 5 5 5"/></svg>
Response <code class="proto-type-badge" id="grpc-res-type"></code>
<span data-i18n="wizard.step3.grpc.response_label">Response</span> <code class="proto-type-badge" id="grpc-res-type"></code>
</div>
<div class="proto-field-list" id="grpc-res-fields"></div>
</div>
+15 -15
View File
@@ -18,32 +18,32 @@
</svg>
</div>
<div>
<div class="config-card-title">HTTP method</div>
<div class="config-card-subtitle">Verb sent to the upstream on every tool invocation</div>
<div class="config-card-title" data-i18n="wizard.step3.rest.method_title">HTTP method</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.rest.method_subtitle">Verb sent to the upstream on every tool invocation</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body">
<div class="method-grid">
<button class="method-card" data-method="GET" onclick="selectMethod(this)">
<span class="method-name">GET</span>
<span class="method-desc">Read</span>
<span class="method-desc" data-i18n="wizard.step3.rest.read">Read</span>
</button>
<button class="method-card active" data-method="POST" onclick="selectMethod(this)">
<span class="method-name">POST</span>
<span class="method-desc">Create</span>
<span class="method-desc" data-i18n="wizard.step3.rest.create">Create</span>
</button>
<button class="method-card" data-method="PUT" onclick="selectMethod(this)">
<span class="method-name">PUT</span>
<span class="method-desc">Replace</span>
<span class="method-desc" data-i18n="wizard.step3.rest.replace">Replace</span>
</button>
<button class="method-card" data-method="PATCH" onclick="selectMethod(this)">
<span class="method-name">PATCH</span>
<span class="method-desc">Update</span>
<span class="method-desc" data-i18n="wizard.step3.rest.update">Update</span>
</button>
<button class="method-card" data-method="DELETE" onclick="selectMethod(this)">
<span class="method-name">DELETE</span>
<span class="method-desc">Remove</span>
<span class="method-desc" data-i18n="wizard.step3.rest.remove">Remove</span>
</button>
</div>
<div class="method-callout" id="method-callout-rest" style="display:none;"></div>
@@ -60,31 +60,31 @@
</svg>
</div>
<div>
<div class="config-card-title">Request format</div>
<div class="config-card-subtitle">Content negotiation and serialisation</div>
<div class="config-card-title" data-i18n="wizard.step3.rest.format_title">Request format</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.rest.format_subtitle">Content negotiation and serialisation</div>
</div>
<span class="config-card-optional">Optional</span>
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-row">
<div class="form-group">
<label class="form-label">Content-Type</label>
<label class="form-label" data-i18n="wizard.step3.rest.content_type">Content-Type</label>
<select class="form-select">
<option selected>application/json</option>
<option>application/x-www-form-urlencoded</option>
<option>multipart/form-data</option>
<option>text/plain</option>
</select>
<div class="form-hint">How the request body is encoded when sent to the upstream.</div>
<div class="form-hint" data-i18n="wizard.step3.rest.content_type_hint">How the request body is encoded when sent to the upstream.</div>
</div>
<div class="form-group">
<label class="form-label">Accept</label>
<label class="form-label" data-i18n="wizard.step3.rest.accept">Accept</label>
<select class="form-select">
<option selected>application/json</option>
<option>text/plain</option>
<option>*/*</option>
</select>
<div class="form-hint">Accepted response content types from the upstream server.</div>
<div class="form-hint" data-i18n="wizard.step3.rest.accept_hint">Accepted response content types from the upstream server.</div>
</div>
</div>
</div>
+24 -28
View File
@@ -16,47 +16,47 @@
</svg>
</div>
<div>
<div class="config-card-title">Tool identity</div>
<div class="config-card-subtitle">Machine-readable name and LLM-facing description</div>
<div class="config-card-title" data-i18n="wizard.step4.identity_title">Tool identity</div>
<div class="config-card-subtitle" data-i18n="wizard.step4.identity_subtitle">Machine-readable name and LLM-facing description</div>
</div>
</div>
<div class="config-card-body" style="gap: 20px;">
<div class="form-group">
<label class="form-label">
Tool name
<span class="form-label-required">required</span>
<span data-i18n="wizard.step4.tool_name">Tool name</span>
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
</label>
<input id="tool-name" class="form-input input-mono" type="text" value="create_crm_lead" autocomplete="off" spellcheck="false">
<div class="form-hint">Machine-readable identifier used in MCP tool calls. Only lowercase letters, numbers and underscores. Cannot be changed after publishing.</div>
<div class="form-hint" data-i18n="wizard.step4.tool_name_hint">Machine-readable identifier used in MCP tool calls. Only lowercase letters, numbers and underscores. Cannot be changed after publishing.</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">
Display name
<span class="form-label-required">required</span>
<span data-i18n="wizard.step4.display_name">Display name</span>
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
</label>
<input id="tool-display-name" class="form-input" type="text" value="Create CRM Lead" autocomplete="off">
<div class="form-hint">Human-readable name shown in the console and audit log.</div>
<div class="form-hint" data-i18n="wizard.step4.display_name_hint">Human-readable name shown in the console and audit log.</div>
</div>
<div class="form-group">
<label class="form-label">
Tool title
<span class="form-label-optional">(optional)</span>
<span data-i18n="wizard.step4.tool_title">Tool title</span>
<span class="form-label-optional" data-i18n="workspace_setup.optional">(optional)</span>
</label>
<input id="tool-title" class="form-input" type="text" value="Create a new CRM lead record" autocomplete="off">
<div class="form-hint">Short imperative sentence shown in the MCP tool manifest.</div>
<div class="form-hint" data-i18n="wizard.step4.tool_title_hint">Short imperative sentence shown in the MCP tool manifest.</div>
</div>
</div>
<div class="form-group">
<label class="form-label">
Description
<span class="form-label-required">required</span>
<span data-i18n="wizard.step4.description">Description</span>
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
</label>
<textarea id="tool-description" class="form-textarea" rows="5">Creates a new lead record in the CRM from the provided contact details. Use this tool when the user wants to add a new prospect or contact. Returns the new lead ID and creation timestamp on success.</textarea>
<div class="form-hint">LLM-facing description. Be precise — this is the primary signal the model uses to decide whether to invoke this tool.</div>
<div class="form-hint" data-i18n="wizard.step4.description_hint">LLM-facing description. Be precise — this is the primary signal the model uses to decide whether to invoke this tool.</div>
</div>
</div>
@@ -68,10 +68,8 @@
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title">Writing effective descriptions</div>
<div class="info-callout-text">
Start with a verb ("Creates", "Fetches", "Updates"). Mention key input requirements. Describe what a successful response looks like. Avoid vague phrasing like "handles" or "manages". See <a href="#">description best practices →</a>
</div>
<div class="info-callout-title" data-i18n="wizard.step4.description_title">Writing effective descriptions</div>
<div class="info-callout-text" data-i18n="wizard.step4.description_body">Start with a verb ("Creates", "Fetches", "Updates"). Mention key input requirements. Describe what a successful response looks like. Avoid vague phrasing like "handles" or "manages". See description best practices →</div>
</div>
</div>
@@ -84,10 +82,10 @@
</svg>
</div>
<div>
<div class="config-card-title">Input schema</div>
<div class="config-card-subtitle">Parameters the LLM passes when calling this tool</div>
<div class="config-card-title" data-i18n="wizard.step4.input_schema_title">Input schema</div>
<div class="config-card-subtitle" data-i18n="wizard.step4.input_schema_subtitle">Parameters the LLM passes when calling this tool</div>
</div>
<span class="config-card-optional">JSON Schema draft-07</span>
<span class="config-card-optional" data-i18n="wizard.step4.schema_draft">JSON Schema draft-07</span>
</div>
<div class="config-card-body" style="gap: 0; padding: 0;">
<div class="code-block" style="border-radius: 0; border: none;">
@@ -122,10 +120,10 @@
</svg>
</div>
<div>
<div class="config-card-title">Output schema</div>
<div class="config-card-subtitle">Shape of the data returned to the LLM after a successful call</div>
<div class="config-card-title" data-i18n="wizard.step4.output_schema_title">Output schema</div>
<div class="config-card-subtitle" data-i18n="wizard.step4.output_schema_subtitle">Shape of the data returned to the LLM after a successful call</div>
</div>
<span class="config-card-optional">JSON Schema draft-07</span>
<span class="config-card-optional" data-i18n="wizard.step4.schema_draft">JSON Schema draft-07</span>
</div>
<div class="config-card-body" style="gap: 0; padding: 0;">
<div class="code-block" style="border-radius: 0; border: none;">
@@ -153,10 +151,8 @@
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title">Schemas are protocol-agnostic</div>
<div class="info-callout-text">
These schemas describe the MCP contract, not the upstream wire format. Field mapping in Step 5 translates between the two. You can upload a sample JSON response to auto-generate the output schema.
</div>
<div class="info-callout-title" data-i18n="wizard.step4.protocol_agnostic_title">Schemas are protocol-agnostic</div>
<div class="info-callout-text" data-i18n="wizard.step4.protocol_agnostic_body">These schemas describe the MCP contract, not the upstream wire format. Field mapping in Step 5 translates between the two. You can upload a sample JSON response to auto-generate the output schema.</div>
</div>
</div>
</div><!-- /step-pane-4 -->
+43 -45
View File
@@ -9,7 +9,7 @@
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label">Input → Request</span>
<span class="section-divider-label" data-i18n="wizard.step5.input_request">Input → Request</span>
<div class="section-divider-line"></div>
</div>
@@ -31,7 +31,7 @@ source: "$.input.source"</textarea>
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label">Response → Output</span>
<span class="section-divider-label" data-i18n="wizard.step5.output_response">Response → Output</span>
<div class="section-divider-line"></div>
</div>
@@ -51,7 +51,7 @@ owner_id: "$.response.data.owner.id"</textarea>
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label">Execution</span>
<span class="section-divider-label" data-i18n="wizard.step5.execution">Execution</span>
<div class="section-divider-line"></div>
</div>
@@ -64,10 +64,10 @@ owner_id: "$.response.data.owner.id"</textarea>
</svg>
</div>
<div>
<div class="config-card-title">Execution config</div>
<div class="config-card-subtitle">Timeouts, retry policy and auth profile reference</div>
<div class="config-card-title" data-i18n="wizard.step5.exec_title">Execution config</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.exec_subtitle">Timeouts, retry policy and auth profile reference</div>
</div>
<span class="config-card-optional">Optional</span>
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
</div>
<div class="config-card-body" style="gap: 0; padding: 0;">
<div class="code-block" style="border-radius: 0; border: none;">
@@ -88,7 +88,7 @@ tls:
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label">Live validation and publishing</span>
<span class="section-divider-label" data-i18n="wizard.step5.live_title">Live validation and publishing</span>
<div class="section-divider-line"></div>
</div>
@@ -98,7 +98,7 @@ tls:
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" id="wizard-live-status-title">Draft actions</div>
<div class="info-callout-title" id="wizard-live-status-title" data-i18n="wizard.step5.draft_actions">Draft actions</div>
<div class="info-callout-text" id="wizard-live-status-text"></div>
</div>
</div>
@@ -109,8 +109,8 @@ tls:
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title">Live actions save the draft first</div>
<div class="info-callout-text">Sample uploads, test runs, YAML actions, publish and gRPC descriptor discovery all persist the current draft before calling the backend.</div>
<div class="info-callout-title" data-i18n="wizard.step5.live_save_title">Live actions save the draft first</div>
<div class="info-callout-text" data-i18n="wizard.step5.live_save_body">Sample uploads, test runs, YAML actions, publish and gRPC descriptor discovery all persist the current draft before calling the backend.</div>
</div>
</div>
@@ -123,14 +123,14 @@ tls:
</svg>
</div>
<div>
<div class="config-card-title">Samples and draft generation</div>
<div class="config-card-subtitle">Persist input/output samples, then generate schemas and mappings from real payloads.</div>
<div class="config-card-title" data-i18n="wizard.step5.samples_title">Samples and draft generation</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.samples_subtitle">Persist input/output samples, then generate schemas and mappings from real payloads.</div>
</div>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-row">
<div class="form-group">
<label class="form-label">Input sample</label>
<label class="form-label" data-i18n="wizard.step5.input_sample">Input sample</label>
<textarea id="wizard-input-sample" class="form-textarea code-textarea" rows="9" spellcheck="false">{
"first_name": "Ada",
"last_name": "Lovelace",
@@ -138,7 +138,7 @@ tls:
}</textarea>
</div>
<div class="form-group">
<label class="form-label">Output sample</label>
<label class="form-label" data-i18n="wizard.step5.output_sample">Output sample</label>
<textarea id="wizard-output-sample" class="form-textarea code-textarea" rows="9" spellcheck="false">{
"id": "lead_123",
"status": "created"
@@ -146,9 +146,9 @@ tls:
</div>
</div>
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
<button id="wizard-upload-input-sample" class="btn-primary-sm" type="button">Save input sample</button>
<button id="wizard-upload-output-sample" class="btn-ghost-sm" type="button">Save output sample</button>
<button id="wizard-generate-draft" class="btn-ghost-sm" type="button">Generate draft from samples</button>
<button id="wizard-upload-input-sample" class="btn-primary-sm" type="button" data-i18n="wizard.step5.save_input_sample">Save input sample</button>
<button id="wizard-upload-output-sample" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.save_output_sample">Save output sample</button>
<button id="wizard-generate-draft" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.generate_draft">Generate draft from samples</button>
</div>
</div>
</div>
@@ -161,13 +161,13 @@ tls:
</svg>
</div>
<div>
<div class="config-card-title">Test run</div>
<div class="config-card-subtitle">Run the current draft against the upstream before publishing it.</div>
<div class="config-card-title" data-i18n="wizard.step5.test_title">Test run</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.test_subtitle">Run the current draft against the upstream before publishing it.</div>
</div>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-group">
<label class="form-label">Test input payload</label>
<label class="form-label" data-i18n="wizard.step5.test_input_payload">Test input payload</label>
<textarea id="wizard-test-input" class="form-textarea code-textarea" rows="8" spellcheck="false">{
"first_name": "Ada",
"last_name": "Lovelace",
@@ -175,21 +175,21 @@ tls:
}</textarea>
</div>
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
<button id="wizard-run-test" class="btn-primary-sm" type="button">Run test</button>
<button id="wizard-copy-test-response" class="btn-ghost-sm" type="button">Use response as output sample</button>
<button id="wizard-run-test" class="btn-primary-sm" type="button" data-i18n="wizard.step5.run_test">Run test</button>
<button id="wizard-copy-test-response" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.use_response_output">Use response as output sample</button>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Request preview</label>
<label class="form-label" data-i18n="wizard.step5.request_preview">Request preview</label>
<textarea id="wizard-test-request-preview" class="form-textarea code-textarea" rows="8" spellcheck="false" readonly></textarea>
</div>
<div class="form-group">
<label class="form-label">Response preview</label>
<label class="form-label" data-i18n="wizard.step5.response_preview">Response preview</label>
<textarea id="wizard-test-response-preview" class="form-textarea code-textarea" rows="8" spellcheck="false" readonly></textarea>
</div>
</div>
<div class="form-group">
<label class="form-label">Errors</label>
<label class="form-label" data-i18n="wizard.step5.errors">Errors</label>
<textarea id="wizard-test-errors" class="form-textarea code-textarea" rows="4" spellcheck="false" readonly></textarea>
</div>
</div>
@@ -204,16 +204,16 @@ tls:
</svg>
</div>
<div>
<div class="config-card-title">gRPC descriptor set</div>
<div class="config-card-subtitle">Upload a compiled descriptor set and discover unary methods from the saved draft.</div>
<div class="config-card-title" data-i18n="wizard.step5.grpc_descriptor_title">gRPC descriptor set</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.grpc_descriptor_subtitle">Upload a compiled descriptor set and discover unary methods from the saved draft.</div>
</div>
</div>
<div class="config-card-body" style="gap: 16px;">
<div style="display:flex; gap: 8px; align-items:center; flex-wrap: wrap;">
<input id="wizard-descriptor-set-input" type="file" accept=".bin,.pb,.desc,.protoset,application/octet-stream" style="display:none">
<button id="wizard-select-descriptor-set" class="btn-ghost-sm" type="button">Choose descriptor set</button>
<button id="wizard-upload-descriptor-set" class="btn-primary-sm" type="button">Upload and discover services</button>
<span id="wizard-descriptor-set-name" style="color: var(--text-muted); font-size: 12px;">No descriptor set selected</span>
<button id="wizard-select-descriptor-set" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.choose_descriptor">Choose descriptor set</button>
<button id="wizard-upload-descriptor-set" class="btn-primary-sm" type="button" data-i18n="wizard.step5.upload_discover">Upload and discover services</button>
<span id="wizard-descriptor-set-name" style="color: var(--text-muted); font-size: 12px;" data-i18n="wizard.step5.no_descriptor">No descriptor set selected</span>
</div>
<div id="wizard-descriptor-services-summary" class="form-hint"></div>
<div id="wizard-descriptor-services-list" style="display:grid; gap: 10px;"></div>
@@ -228,20 +228,20 @@ tls:
</svg>
</div>
<div>
<div class="config-card-title">YAML import and export</div>
<div class="config-card-subtitle">Move operation drafts between environments without leaving the wizard.</div>
<div class="config-card-title" data-i18n="wizard.step5.yaml_title">YAML import and export</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.yaml_subtitle">Move operation drafts between environments without leaving the wizard.</div>
</div>
</div>
<div class="config-card-body" style="gap: 16px;">
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
<button id="wizard-export-yaml" class="btn-ghost-sm" type="button">Export YAML</button>
<button id="wizard-import-yaml-file-trigger" class="btn-ghost-sm" type="button">Load YAML file</button>
<button id="wizard-import-yaml" class="btn-primary-sm" type="button">Import YAML</button>
<button id="wizard-export-yaml" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.export_yaml">Export YAML</button>
<button id="wizard-import-yaml-file-trigger" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.load_yaml_file">Load YAML file</button>
<button id="wizard-import-yaml" class="btn-primary-sm" type="button" data-i18n="wizard.step5.import_yaml">Import YAML</button>
<input id="wizard-import-yaml-file" type="file" accept=".yaml,.yml,text/yaml" style="display:none">
</div>
<div class="form-group">
<label class="form-label">YAML document</label>
<textarea id="wizard-import-yaml-text" class="form-textarea code-textarea" rows="10" spellcheck="false" placeholder="Paste exported YAML here to create or upsert an operation."></textarea>
<label class="form-label" data-i18n="wizard.step5.yaml_document">YAML document</label>
<textarea id="wizard-import-yaml-text" class="form-textarea code-textarea" rows="10" spellcheck="false" data-i18n-ph="wizard.step5.yaml_placeholder" placeholder="Paste exported YAML here to create or upsert an operation."></textarea>
</div>
</div>
</div>
@@ -254,14 +254,14 @@ tls:
</svg>
</div>
<div>
<div class="config-card-title">Publish</div>
<div class="config-card-subtitle">Save the current draft and expose it to published agents.</div>
<div class="config-card-title" data-i18n="wizard.step5.publish_title">Publish</div>
<div class="config-card-subtitle" data-i18n="wizard.step5.publish_subtitle">Save the current draft and expose it to published agents.</div>
</div>
</div>
<div class="config-card-body" style="gap: 12px;">
<div class="form-hint">Publishing uses the current draft version from this wizard session.</div>
<div class="form-hint" data-i18n="wizard.step5.publish_hint">Publishing uses the current draft version from this wizard session.</div>
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
<button id="wizard-publish-operation" class="btn-primary-sm" type="button">Publish operation</button>
<button id="wizard-publish-operation" class="btn-primary-sm" type="button" data-i18n="wizard.step5.publish_action">Publish operation</button>
</div>
</div>
</div>
@@ -272,10 +272,8 @@ tls:
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title">Operation stays editable in this wizard</div>
<div class="info-callout-text">
Saving creates or updates the current draft in place. Use the live actions above to upload samples, test the draft, export or import YAML, and publish when the upstream contract is ready.
</div>
<div class="info-callout-title" data-i18n="wizard.step5.editable_title">Operation stays editable in this wizard</div>
<div class="info-callout-text" data-i18n="wizard.step5.editable_body">Saving creates or updates the current draft in place. Use the live actions above to upload samples, test the draft, export or import YAML, and publish when the upstream contract is ready.</div>
</div>
</div>
</div><!-- /step-pane-5 -->
+20 -20
View File
@@ -167,11 +167,11 @@
<div class="member-row">
<div class="member-avatar" style="background:linear-gradient(135deg,#0d9488,#6366f1);">AT</div>
<div class="member-info">
<div class="member-name">Operator <span style="font-size:11px;font-weight:400;color:var(--text-muted);">(you)</span></div>
<div class="member-name">Crank <span style="font-size:11px;font-weight:400;color:var(--text-muted);" data-i18n="workspace_setup.members.you">(you)</span></div>
<div class="member-email">operator@acme.com</div>
</div>
<div class="member-last-active">Today</div>
<span class="member-role-badge role-owner">Owner</span>
<span class="member-role-badge role-owner" data-i18n="workspace_setup.role.owner">Owner</span>
</div>
<div class="member-row">
<div class="member-avatar" style="background:linear-gradient(135deg,#7c3aed,#4f46e5);">BS</div>
@@ -181,11 +181,11 @@
</div>
<div class="member-last-active">2 days ago</div>
<select class="member-role-select" onchange="updateRole(this, 'blake@acme.com')">
<option value="admin" selected>Admin</option>
<option value="developer">Developer</option>
<option value="viewer">Viewer</option>
<option value="admin" selected data-i18n="workspace_setup.role.admin">Admin</option>
<option value="developer" data-i18n="workspace_setup.role.operator">Developer</option>
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Blake Smith')" title="Remove member">
<button class="member-remove-btn" onclick="removeMember(this, 'Blake Smith')" data-i18n-title="workspace_setup.members.remove" title="Remove member">
<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>
</button>
</div>
@@ -197,11 +197,11 @@
</div>
<div class="member-last-active">1 week ago</div>
<select class="member-role-select" onchange="updateRole(this, 'carol@acme.com')">
<option value="admin">Admin</option>
<option value="developer" selected>Developer</option>
<option value="viewer">Viewer</option>
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
<option value="developer" selected data-i18n="workspace_setup.role.operator">Developer</option>
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Carol Lane')" title="Remove member">
<button class="member-remove-btn" onclick="removeMember(this, 'Carol Lane')" data-i18n-title="workspace_setup.members.remove" title="Remove member">
<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>
</button>
</div>
@@ -213,11 +213,11 @@
</div>
<div class="member-last-active">Never</div>
<select class="member-role-select" onchange="updateRole(this, 'dan@acme.com')">
<option value="admin">Admin</option>
<option value="developer">Developer</option>
<option value="viewer" selected>Viewer</option>
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
<option value="developer" data-i18n="workspace_setup.role.operator">Developer</option>
<option value="viewer" selected data-i18n="workspace_setup.role.viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Dan Morgan')" title="Remove member">
<button class="member-remove-btn" onclick="removeMember(this, 'Dan Morgan')" data-i18n-title="workspace_setup.members.remove" title="Remove member">
<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>
</button>
</div>
@@ -235,8 +235,8 @@
<div class="member-email">Invited 3 days ago · not yet accepted</div>
</div>
<div class="member-last-active"></div>
<span class="member-role-badge" style="background:rgba(139,148,158,0.12);color:var(--text-muted);border:1px solid var(--border);">Developer</span>
<button class="member-remove-btn" onclick="revokeInvite(this)" title="Revoke invite">
<span class="member-role-badge" style="background:rgba(139,148,158,0.12);color:var(--text-muted);border:1px solid var(--border);" data-i18n="workspace_setup.role.operator">Developer</span>
<button class="member-remove-btn" onclick="revokeInvite(this)" data-i18n-title="workspace_setup.members.revoke" title="Revoke invite">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="2" y1="2" x2="14" y2="14"/><line x1="14" y1="2" x2="2" y2="14"/></svg>
</button>
</div>
@@ -246,19 +246,19 @@
<div style="border-top:1px solid var(--border-subtle);padding:16px 24px 20px;">
<div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:14px;" data-i18n="workspace_setup.roles.title">Roles &amp; permissions</div>
<div class="role-def-row">
<span class="member-role-badge role-owner" style="min-width:80px;text-align:center;">Owner</span>
<span class="member-role-badge role-owner" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.owner">Owner</span>
<div class="role-def-text" data-i18n="workspace_setup.roles.owner_body">Full workspace control, billing access, can transfer or delete the workspace.</div>
</div>
<div class="role-def-row">
<span class="member-role-badge role-admin" style="min-width:80px;text-align:center;">Admin</span>
<span class="member-role-badge role-admin" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.admin">Admin</span>
<div class="role-def-text" data-i18n="workspace_setup.roles.admin_body">Manage members and roles, create and edit all operations and agents, manage API keys.</div>
</div>
<div class="role-def-row">
<span class="member-role-badge role-developer" style="min-width:80px;text-align:center;">Developer</span>
<span class="member-role-badge role-developer" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.operator">Developer</span>
<div class="role-def-text" data-i18n="workspace_setup.roles.developer_body">Create and edit operations and agents. Cannot manage members or workspace settings.</div>
</div>
<div class="role-def-row">
<span class="member-role-badge role-viewer" style="min-width:80px;text-align:center;">Viewer</span>
<span class="member-role-badge role-viewer" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.viewer">Viewer</span>
<div class="role-def-text" data-i18n="workspace_setup.roles.viewer_body">Read-only access to operations, agents, and logs.</div>
</div>
</div>
+4 -4
View File
@@ -55,7 +55,7 @@
<div class="nav-right">
<!-- hamburger (mobile only) -->
<button class="nav-hamburger" @click.stop="navOpen = !navOpen" :class="{ open: navOpen }" aria-label="Menu">
<button class="nav-hamburger" @click.stop="navOpen = !navOpen" :class="{ open: navOpen }" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu">
<span></span><span></span><span></span>
</button>
@@ -72,12 +72,12 @@
<!-- User menu -->
<div class="user-menu" @click.stop>
<div class="nav-avatar" @click="toggleDropdown('user')" title="Account">AT</div>
<div class="nav-avatar" @click="toggleDropdown('user')" data-i18n-title="nav.account" title="Account">AT</div>
<div class="user-dropdown" x-show="openDropdown === 'user'" x-transition>
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</div>
<div class="user-dropdown-name">Crank</div>
<div class="user-dropdown-role"></div>
</div>
<button class="user-dropdown-item" @click="openDropdown = null; window.location.href='/settings'">
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
+10 -2
View File
@@ -8,6 +8,14 @@
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
function roleLabel(role) {
var normalized = String(role || 'viewer').toLowerCase();
if (normalized === 'operator') return tKey('workspace_setup.role.operator');
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
return normalized.replace(/^\w/, function(char) { return char.toUpperCase(); });
}
function currentWorkspaceLabel() {
if (window.getCurrentWorkspace) {
@@ -86,7 +94,7 @@
id: session.user.id,
name: session.user.display_name,
email: session.user.email,
role: membership ? String(membership.role || '').replace(/^\w/, function(char) { return char.toUpperCase(); }) : tKey('workspace_setup.role.viewer'),
role: membership ? roleLabel(membership.role) : tKey('workspace_setup.role.viewer'),
workspace: membership ? membership.workspace.slug : '',
workspaceId: membership ? membership.workspace.id : '',
initials: initials(session.user.display_name, session.user.email),
@@ -115,7 +123,7 @@
? session.user.email
: fallbackUser && fallbackUser.email;
var role = membership
? String(membership.role || '').replace(/^\w/, function(char) { return char.toUpperCase(); })
? roleLabel(membership.role)
: (fallbackUser && fallbackUser.role) || tKey('workspace_setup.role.viewer');
var workspace = membership && membership.workspace
? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id)
+312 -6
View File
@@ -16,6 +16,10 @@ var TRANSLATIONS = {
'nav.workspaces': 'Your workspaces',
'nav.create_workspace': 'Create workspace',
'nav.notifications': 'Notifications',
'nav.menu': 'Menu',
'nav.account': 'Account',
'nav.user_fallback': 'Crank',
'nav.workspace_fallback': 'workspace',
// Operations page
'ops.title': 'Operations',
@@ -302,9 +306,11 @@ var TRANSLATIONS = {
'settings.security.capabilities_body': 'Two-factor authentication, passkeys and multi-session inventory are planned, but the current live flow is password-based login with one HttpOnly browser session.',
'settings.security.not_available': 'Not available in this build.',
'settings.session.title': 'Current session',
'settings.session.browser': 'Browser session',
'settings.session.current': 'current',
'settings.session.loading': 'Loading current session details…',
'settings.session.unavailable': 'Session details are unavailable. Use Log out to revoke the current browser session.',
'settings.session.current_workspace': 'current workspace',
'settings.session.summary': '{email} · {role} in {workspace}. Use Log out to revoke the current browser session.',
'settings.notifications.title': 'Notification preferences',
'settings.notifications.not_ready_title': 'Notifications are not wired yet.',
@@ -411,6 +417,7 @@ var TRANSLATIONS = {
'wizard.progress.edit': 'Edit operation',
'wizard.exit': 'Exit wizard',
'wizard.step_of': 'Step <strong>{step}</strong> of {total}',
'wizard.step_short': 'Step {step}',
'wizard.status.completed': 'Completed',
'wizard.status.in_progress': 'In progress',
'wizard.status.not_started': 'Not started',
@@ -431,23 +438,167 @@ var TRANSLATIONS = {
'wizard.sidebar.brand.edit': 'Edit <span>operation</span>',
'wizard.step1.title': 'Choose a protocol',
'wizard.step1.subtitle': 'Select the transport protocol your upstream service uses. This determines how Crank will communicate with your API and which configuration options are available in subsequent steps.',
'wizard.step1.rest_name': 'REST / HTTP',
'wizard.step1.rest_tagline': 'Standard HTTP verbs over a RESTful endpoint. The most broadly supported option.',
'wizard.step1.graphql_name': 'GraphQL',
'wizard.step1.graphql_tagline': 'Query and mutate a GraphQL schema. Supports queries and mutations over one fixed operation.',
'wizard.step1.grpc_name': 'gRPC',
'wizard.step1.grpc_tagline': 'High-performance binary RPC over HTTP/2. Ideal for low-latency internal services.',
'wizard.step1.query': 'Query',
'wizard.step1.mutation': 'Mutation',
'wizard.step1.unary': 'Unary',
'wizard.step1.tip_title': 'You can change this later',
'wizard.step1.tip_body': 'Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See protocol migration guide for details.',
'wizard.step2.title': 'Select upstream & endpoint',
'wizard.step2.subtitle': 'Choose an existing upstream or register a new one. Then define the specific endpoint path this operation will call.',
'wizard.required': 'Required',
'wizard.optional': 'Optional',
'wizard.step2.upstream_title': 'Upstream',
'wizard.step2.upstream_subtitle': 'Shared host, base URL, and authentication',
'wizard.step2.upstream_placeholder': 'Select an upstream…',
'wizard.step2.search_placeholder': 'Search by name or URL…',
'wizard.step2.change': 'Change',
'wizard.step2.register_new': 'Register new upstream',
'wizard.step2.name': 'Name',
'wizard.step2.base_url': 'Base URL',
'wizard.step2.unique_hint': 'Unique identifier for this upstream.',
'wizard.step2.base_url_hint': 'Root URL — no trailing slash. Supports ${secrets.*}.',
'wizard.step2.auth_headers': 'Auth headers',
'wizard.step2.auth_headers_hint': 'These headers are sent on every request to this upstream. Use secrets for credentials.',
'wizard.step2.save_upstream': 'Save upstream',
'wizard.step2.endpoint_title': 'Endpoint',
'wizard.step2.endpoint_subtitle': 'Path and HTTP method for this specific operation',
'wizard.step2.path_template': 'Path template',
'wizard.step2.path_hint': 'Appended to the upstream base URL. Use {param} for path variables.',
'wizard.step3.rest.label': 'Request config',
'wizard.step3.graphql.label': 'GQL operation',
'wizard.step3.grpc.label': 'RPC method',
'wizard.step3.rest.title': 'HTTP method & format',
'wizard.step3.rest.subtitle': 'Choose the HTTP verb this operation sends and configure content negotiation headers. Crank will serialize MCP tool arguments into the appropriate request body format.',
'wizard.step3.rest.method_title': 'HTTP method',
'wizard.step3.rest.method_subtitle': 'Verb sent to the upstream on every tool invocation',
'wizard.step3.rest.read': 'Read',
'wizard.step3.rest.create': 'Create',
'wizard.step3.rest.replace': 'Replace',
'wizard.step3.rest.update': 'Update',
'wizard.step3.rest.remove': 'Remove',
'wizard.step3.rest.format_title': 'Request format',
'wizard.step3.rest.format_subtitle': 'Content negotiation and serialisation',
'wizard.step3.rest.content_type': 'Content-Type',
'wizard.step3.rest.content_type_hint': 'How the request body is encoded when sent to the upstream.',
'wizard.step3.rest.accept': 'Accept',
'wizard.step3.rest.accept_hint': 'Accepted response content types from the upstream server.',
'wizard.step3.graphql.title': 'GraphQL operation',
'wizard.step3.graphql.subtitle': 'Define the GraphQL operation this tool will execute. All requests are sent as HTTP POST to the endpoint configured in the previous step. Variables are populated from MCP tool arguments at runtime.',
'wizard.step3.graphql.type_title': 'Operation type',
'wizard.step3.graphql.type_subtitle': 'GraphQL operation kind — subscriptions are not supported',
'wizard.step3.graphql.query_desc': 'Fetch data without side-effects',
'wizard.step3.graphql.mutation_desc': 'Create, update or delete data',
'wizard.step3.graphql.doc_title': 'Query document',
'wizard.step3.graphql.doc_subtitle': 'GraphQL document with typed variable declarations',
'wizard.step3.graphql.variables_title': 'Variables are mapped in step 5',
'wizard.step3.graphql.variables_body': 'Declare all variables in the query document using the $variable: Type syntax. In step 5 you will map MCP input fields to these variables. Crank serializes the final {"query","variables"} payload automatically.',
'wizard.step3.grpc.title': 'RPC method',
'wizard.step3.grpc.subtitle': 'Choose how to describe the gRPC service contract. Upload a .proto file, discover methods from a live server via reflection, or enter the route manually. Only unary RPCs are supported.',
'wizard.step3.grpc.source_proto': 'Proto file',
'wizard.step3.grpc.source_reflection': 'Descriptor set',
'wizard.step3.grpc.source_manual': 'Manual',
'wizard.step3.grpc.proto_title': 'Proto definition',
'wizard.step3.grpc.proto_subtitle': 'Upload or paste your .proto file',
'wizard.step3.grpc.drop_title': 'Drop .proto file here',
'wizard.step3.grpc.drop_subtitle': 'or click to browse',
'wizard.step3.grpc.change_file': 'Change file',
'wizard.step3.grpc.paste_content': 'Paste content',
'wizard.step3.grpc.parse_proto': 'Parse proto',
'wizard.step3.grpc.streaming_excluded': 'Streaming methods excluded',
'wizard.step3.grpc.streaming_hidden': '{count} streaming method(s) found and hidden — not compatible with MCP tool calls.',
'wizard.step3.grpc.select_method': 'Select method',
'wizard.step3.grpc.discovery_live_title': 'Descriptor-driven discovery',
'wizard.step3.grpc.discovery_live_subtitle': 'Use the saved draft and a descriptor set to discover services through the backend',
'wizard.step3.grpc.no_upstream': 'No upstream selected',
'wizard.step3.grpc.select_upstream_first': 'Select an upstream in step 2 first',
'wizard.step3.grpc.open_tools': 'Open live descriptor tools',
'wizard.step3.grpc.connecting': 'Connecting…',
'wizard.step3.grpc.live_step5': 'Live descriptor discovery is handled in step 5.',
'wizard.step3.grpc.retry': 'Retry',
'wizard.step3.grpc.manual_title': 'Method route',
'wizard.step3.grpc.manual_subtitle': 'Full gRPC path to the unary method',
'wizard.step3.grpc.route': 'gRPC route',
'wizard.step3.grpc.route_hint': 'Format: /[package.]ServiceName/MethodName',
'wizard.step3.grpc.request_type': 'Request type',
'wizard.step3.grpc.response_type': 'Response type',
'wizard.step3.grpc.proto_doc_hint': 'Protobuf message name, for documentation.',
'wizard.step3.grpc.schema_title': 'Input and output schemas are defined in step 4',
'wizard.step3.grpc.schema_body': 'In manual mode you only need the route. The MCP tool input/output schemas — which the LLM uses to call this tool — are configured in the next step regardless of Protobuf definitions.',
'wizard.step3.grpc.signature_title': 'Method signature',
'wizard.step3.grpc.route_label': 'gRPC route',
'wizard.step3.grpc.request_label': 'Request',
'wizard.step3.grpc.response_label': 'Response',
'wizard.step4.title': 'Tool config',
'wizard.step4.subtitle': 'Name your tool, write the LLM-facing description, and define the input/output schemas. The description is the primary signal the model uses when deciding whether to call this tool.',
'wizard.step4.identity_title': 'Tool identity',
'wizard.step4.identity_subtitle': 'Machine-readable name and LLM-facing description',
'wizard.step4.tool_name': 'Tool name',
'wizard.step4.tool_name_hint': 'Machine-readable identifier used in MCP tool calls. Only lowercase letters, numbers and underscores. Cannot be changed after publishing.',
'wizard.step4.display_name': 'Display name',
'wizard.step4.display_name_hint': 'Human-readable name shown in the console and audit log.',
'wizard.step4.tool_title': 'Tool title',
'wizard.step4.tool_title_hint': 'Short imperative sentence shown in the MCP tool manifest.',
'wizard.step4.description': 'Description',
'wizard.step4.description_hint': 'LLM-facing description. Be precise — this is the primary signal the model uses to decide whether to invoke this tool.',
'wizard.step4.description_title': 'Writing effective descriptions',
'wizard.step4.description_body': 'Start with a verb ("Creates", "Fetches", "Updates"). Mention key input requirements. Describe what a successful response looks like. Avoid vague phrasing like "handles" or "manages". See description best practices →',
'wizard.step4.input_schema_title': 'Input schema',
'wizard.step4.input_schema_subtitle': 'Parameters the LLM passes when calling this tool',
'wizard.step4.output_schema_title': 'Output schema',
'wizard.step4.output_schema_subtitle': 'Shape of the data returned to the LLM after a successful call',
'wizard.step4.schema_draft': 'JSON Schema draft-07',
'wizard.step4.protocol_agnostic_title': 'Schemas are protocol-agnostic',
'wizard.step4.protocol_agnostic_body': 'These schemas describe the MCP contract, not the upstream wire format. Field mapping in Step 5 translates between the two. You can upload a sample JSON response to auto-generate the output schema.',
'wizard.step5.title': 'Mapping and execution',
'wizard.step5.subtitle': 'Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 3.',
'wizard.step5.input_request': 'Input → Request',
'wizard.step5.output_response': 'Response → Output',
'wizard.step5.execution': 'Execution',
'wizard.step5.exec_title': 'Execution config',
'wizard.step5.exec_subtitle': 'Timeouts, retry policy and auth profile reference',
'wizard.step5.live_title': 'Live validation and publishing',
'wizard.step5.draft_actions': 'Draft actions',
'wizard.step5.live_save_title': 'Live actions save the draft first',
'wizard.step5.live_save_body': 'Sample uploads, test runs, YAML actions, publish and gRPC descriptor discovery all persist the current draft before calling the backend.',
'wizard.step5.samples_title': 'Samples and draft generation',
'wizard.step5.samples_subtitle': 'Persist input/output samples, then generate schemas and mappings from real payloads.',
'wizard.step5.input_sample': 'Input sample',
'wizard.step5.output_sample': 'Output sample',
'wizard.step5.save_input_sample': 'Save input sample',
'wizard.step5.save_output_sample': 'Save output sample',
'wizard.step5.generate_draft': 'Generate draft from samples',
'wizard.step5.test_title': 'Test run',
'wizard.step5.test_subtitle': 'Run the current draft against the upstream before publishing it.',
'wizard.step5.test_input_payload': 'Test input payload',
'wizard.step5.run_test': 'Run test',
'wizard.step5.use_response_output': 'Use response as output sample',
'wizard.step5.request_preview': 'Request preview',
'wizard.step5.response_preview': 'Response preview',
'wizard.step5.errors': 'Errors',
'wizard.step5.grpc_descriptor_title': 'gRPC descriptor set',
'wizard.step5.grpc_descriptor_subtitle': 'Upload a compiled descriptor set and discover unary methods from the saved draft.',
'wizard.step5.choose_descriptor': 'Choose descriptor set',
'wizard.step5.upload_discover': 'Upload and discover services',
'wizard.step5.no_descriptor': 'No descriptor set selected',
'wizard.step5.yaml_title': 'YAML import and export',
'wizard.step5.yaml_subtitle': 'Move operation drafts between environments without leaving the wizard.',
'wizard.step5.export_yaml': 'Export YAML',
'wizard.step5.load_yaml_file': 'Load YAML file',
'wizard.step5.import_yaml': 'Import YAML',
'wizard.step5.yaml_document': 'YAML document',
'wizard.step5.yaml_placeholder': 'Paste exported YAML here to create or upsert an operation.',
'wizard.step5.publish_title': 'Publish',
'wizard.step5.publish_subtitle': 'Save the current draft and expose it to published agents.',
'wizard.step5.publish_hint': 'Publishing uses the current draft version from this wizard session.',
'wizard.step5.publish_action': 'Publish operation',
'wizard.step5.editable_title': 'Operation stays editable in this wizard',
'wizard.step5.editable_body': 'Saving creates or updates the current draft in place. Use the live actions above to upload samples, test the draft, export or import YAML, and publish when the upstream contract is ready.',
'wizard.live.failed_title': 'Live action failed',
'wizard.live.failed_body': 'The backend request failed. Review the draft and try again.',
'wizard.error.no_workspace': 'No workspace selected',
@@ -630,6 +781,10 @@ var TRANSLATIONS = {
'nav.workspaces': 'Ваши воркспейсы',
'nav.create_workspace': 'Создать воркспейс',
'nav.notifications': 'Уведомления',
'nav.menu': 'Меню',
'nav.account': 'Аккаунт',
'nav.user_fallback': 'Crank',
'nav.workspace_fallback': 'workspace',
// Operations page
'ops.title': 'Операции',
@@ -916,9 +1071,11 @@ var TRANSLATIONS = {
'settings.security.capabilities_body': 'Двухфакторная аутентификация, passkeys и инвентарь нескольких сессий запланированы, но в текущем live-flow используется логин по паролю и одна HttpOnly browser session.',
'settings.security.not_available': 'Недоступно в этой сборке.',
'settings.session.title': 'Текущая сессия',
'settings.session.browser': 'Сессия браузера',
'settings.session.current': 'текущая',
'settings.session.loading': 'Загрузка деталей текущей сессии…',
'settings.session.unavailable': 'Детали сессии недоступны. Используйте «Выйти», чтобы отозвать текущую browser session.',
'settings.session.current_workspace': 'текущий воркспейс',
'settings.session.summary': '{email} · {role} в {workspace}. Используйте «Выйти», чтобы отозвать текущую browser session.',
'settings.notifications.title': 'Настройки уведомлений',
'settings.notifications.not_ready_title': 'Уведомления пока не подключены.',
@@ -988,10 +1145,10 @@ var TRANSLATIONS = {
'workspace_setup.danger.delete_body': 'Безвозвратно удаляет все операции, ключи, логи и настройки. Действие нельзя отменить. Вы будете немедленно разлогинены.',
'workspace_setup.export': 'Экспорт',
'workspace_setup.delete': 'Удалить воркспейс',
'workspace_setup.role.owner': 'Owner',
'workspace_setup.role.admin': 'Admin',
'workspace_setup.role.operator': 'Developer',
'workspace_setup.role.viewer': 'Viewer',
'workspace_setup.role.owner': 'Владелец',
'workspace_setup.role.admin': 'Администратор',
'workspace_setup.role.operator': 'Разработчик',
'workspace_setup.role.viewer': 'Наблюдатель',
'workspace_setup.member_count': '{count} участников',
'workspace_setup.member_count_one': '1 участник',
'workspace_setup.saving_create': 'Создание…',
@@ -1025,6 +1182,7 @@ var TRANSLATIONS = {
'wizard.progress.edit': 'Редактировать операцию',
'wizard.exit': 'Выйти из мастера',
'wizard.step_of': 'Шаг <strong>{step}</strong> из {total}',
'wizard.step_short': 'Шаг {step}',
'wizard.status.completed': 'Завершено',
'wizard.status.in_progress': 'В процессе',
'wizard.status.not_started': 'Не начато',
@@ -1033,9 +1191,9 @@ var TRANSLATIONS = {
'wizard.help.link': 'гайд по сравнению протоколов →',
'wizard.progress.section': 'Прогресс',
'wizard.step_name.protocol': 'Протокол',
'wizard.step_name.upstream': 'Upstream target',
'wizard.step_name.upstream': 'Upstream',
'wizard.step_name.tool': 'Конфиг инструмента',
'wizard.step_name.mapping': 'Mapping',
'wizard.step_name.mapping': 'Маппинг',
'wizard.button.back': 'Назад',
'wizard.button.continue': 'Продолжить',
'wizard.button.save_draft': 'Сохранить черновик',
@@ -1045,23 +1203,167 @@ var TRANSLATIONS = {
'wizard.sidebar.brand.edit': 'Редактировать <span>операцию</span>',
'wizard.step1.title': 'Выберите протокол',
'wizard.step1.subtitle': 'Выберите транспортный протокол upstream-сервиса. От этого зависит, как Crank будет общаться с вашим API и какие настройки будут доступны на следующих шагах.',
'wizard.step1.rest_name': 'REST / HTTP',
'wizard.step1.rest_tagline': 'Стандартные HTTP-методы поверх REST endpoint-а. Самый универсальный вариант.',
'wizard.step1.graphql_name': 'GraphQL',
'wizard.step1.graphql_tagline': 'Запросы и мутации к GraphQL-схеме. Поддерживаются query и mutation для одной фиксированной операции.',
'wizard.step1.grpc_name': 'gRPC',
'wizard.step1.grpc_tagline': 'Высокопроизводительный бинарный RPC поверх HTTP/2. Подходит для низколатентных внутренних сервисов.',
'wizard.step1.query': 'Запрос',
'wizard.step1.mutation': 'Мутация',
'wizard.step1.unary': 'Unary',
'wizard.step1.tip_title': 'Это можно изменить позже',
'wizard.step1.tip_body': 'Смена протокола после настройки следующих шагов очистит схемы и mapping. Сохраните операцию как черновик перед экспериментами. См. гайд по миграции протоколов.',
'wizard.step2.title': 'Выберите upstream и endpoint',
'wizard.step2.subtitle': 'Выберите существующий upstream или зарегистрируйте новый. Затем задайте конкретный путь endpoint-а для этой операции.',
'wizard.required': 'Обязательно',
'wizard.optional': 'Необязательно',
'wizard.step2.upstream_title': 'Upstream',
'wizard.step2.upstream_subtitle': 'Общий хост, базовый URL и аутентификация',
'wizard.step2.upstream_placeholder': 'Выберите upstream…',
'wizard.step2.search_placeholder': 'Поиск по имени или URL…',
'wizard.step2.change': 'Изменить',
'wizard.step2.register_new': 'Зарегистрировать новый upstream',
'wizard.step2.name': 'Имя',
'wizard.step2.base_url': 'Base URL',
'wizard.step2.unique_hint': 'Уникальный идентификатор этого upstream-а.',
'wizard.step2.base_url_hint': 'Корневой URL без завершающего слеша. Поддерживает ${secrets.*}.',
'wizard.step2.auth_headers': 'Auth headers',
'wizard.step2.auth_headers_hint': 'Эти заголовки отправляются с каждым запросом в этот upstream. Используйте secrets для учетных данных.',
'wizard.step2.save_upstream': 'Сохранить upstream',
'wizard.step2.endpoint_title': 'Endpoint',
'wizard.step2.endpoint_subtitle': 'Путь и HTTP-метод для конкретной операции',
'wizard.step2.path_template': 'Шаблон пути',
'wizard.step2.path_hint': 'Добавляется к base URL upstream-а. Используйте {param} для path-параметров.',
'wizard.step3.rest.label': 'Конфиг запроса',
'wizard.step3.graphql.label': 'GQL операция',
'wizard.step3.grpc.label': 'RPC метод',
'wizard.step3.rest.title': 'HTTP метод и формат',
'wizard.step3.rest.subtitle': 'Выберите HTTP-метод, который отправляет эта операция, и настройте content negotiation headers. Crank сериализует аргументы MCP-инструмента в нужный формат request body.',
'wizard.step3.rest.method_title': 'HTTP метод',
'wizard.step3.rest.method_subtitle': 'Метод, который отправляется в upstream при каждом вызове инструмента',
'wizard.step3.rest.read': 'Чтение',
'wizard.step3.rest.create': 'Создание',
'wizard.step3.rest.replace': 'Замена',
'wizard.step3.rest.update': 'Обновление',
'wizard.step3.rest.remove': 'Удаление',
'wizard.step3.rest.format_title': 'Формат запроса',
'wizard.step3.rest.format_subtitle': 'Content negotiation и сериализация',
'wizard.step3.rest.content_type': 'Content-Type',
'wizard.step3.rest.content_type_hint': 'Как кодируется request body перед отправкой в upstream.',
'wizard.step3.rest.accept': 'Accept',
'wizard.step3.rest.accept_hint': 'Какие типы ответа принимаются от upstream-сервера.',
'wizard.step3.graphql.title': 'GraphQL операция',
'wizard.step3.graphql.subtitle': 'Определите GraphQL-операцию, которую будет выполнять этот инструмент. Все запросы отправляются как HTTP POST на endpoint, настроенный на предыдущем шаге. Variables заполняются из аргументов MCP-инструмента во время выполнения.',
'wizard.step3.graphql.type_title': 'Тип операции',
'wizard.step3.graphql.type_subtitle': 'Вид GraphQL-операции — subscriptions не поддерживаются',
'wizard.step3.graphql.query_desc': 'Получение данных без побочных эффектов',
'wizard.step3.graphql.mutation_desc': 'Создание, изменение или удаление данных',
'wizard.step3.graphql.doc_title': 'Документ запроса',
'wizard.step3.graphql.doc_subtitle': 'GraphQL-документ с типизированными объявлениями переменных',
'wizard.step3.graphql.variables_title': 'Переменные мапятся на шаге 5',
'wizard.step3.graphql.variables_body': 'Объявите все переменные в документе запроса в формате $variable: Type. На шаге 5 вы свяжете поля MCP input-а с этими переменными. Crank автоматически сериализует итоговый payload {"query","variables"}.',
'wizard.step3.grpc.title': 'RPC метод',
'wizard.step3.grpc.subtitle': 'Выберите, как описывать контракт gRPC-сервиса. Загрузите .proto файл, откройте методы через reflection или введите route вручную. Поддерживаются только unary RPC.',
'wizard.step3.grpc.source_proto': 'Proto файл',
'wizard.step3.grpc.source_reflection': 'Descriptor set',
'wizard.step3.grpc.source_manual': 'Вручную',
'wizard.step3.grpc.proto_title': 'Proto definition',
'wizard.step3.grpc.proto_subtitle': 'Загрузите или вставьте содержимое .proto файла',
'wizard.step3.grpc.drop_title': 'Перетащите сюда .proto файл',
'wizard.step3.grpc.drop_subtitle': 'или нажмите для выбора',
'wizard.step3.grpc.change_file': 'Сменить файл',
'wizard.step3.grpc.paste_content': 'Вставить содержимое',
'wizard.step3.grpc.parse_proto': 'Разобрать proto',
'wizard.step3.grpc.streaming_excluded': 'Стриминговые методы исключены',
'wizard.step3.grpc.streaming_hidden': 'Найдено и скрыто streaming method(s): {count} — они несовместимы с MCP tool calls.',
'wizard.step3.grpc.select_method': 'Выберите метод',
'wizard.step3.grpc.discovery_live_title': 'Поиск через descriptor set',
'wizard.step3.grpc.discovery_live_subtitle': 'Используйте сохраненный черновик и descriptor set, чтобы открыть сервисы через backend',
'wizard.step3.grpc.no_upstream': 'Upstream не выбран',
'wizard.step3.grpc.select_upstream_first': 'Сначала выберите upstream на шаге 2',
'wizard.step3.grpc.open_tools': 'Открыть live-инструменты descriptor-а',
'wizard.step3.grpc.connecting': 'Подключение…',
'wizard.step3.grpc.live_step5': 'Live-поиск descriptor-а выполняется на шаге 5.',
'wizard.step3.grpc.retry': 'Повторить',
'wizard.step3.grpc.manual_title': 'Маршрут метода',
'wizard.step3.grpc.manual_subtitle': 'Полный gRPC path до unary-метода',
'wizard.step3.grpc.route': 'gRPC route',
'wizard.step3.grpc.route_hint': 'Формат: /[package.]ServiceName/MethodName',
'wizard.step3.grpc.request_type': 'Тип запроса',
'wizard.step3.grpc.response_type': 'Тип ответа',
'wizard.step3.grpc.proto_doc_hint': 'Имя Protobuf-сообщения, только для документации.',
'wizard.step3.grpc.schema_title': 'Входная и выходная схемы задаются на шаге 4',
'wizard.step3.grpc.schema_body': 'В ручном режиме нужен только route. Входная и выходная схемы MCP-инструмента — которые LLM использует при вызове — настраиваются на следующем шаге независимо от Protobuf-определений.',
'wizard.step3.grpc.signature_title': 'Сигнатура метода',
'wizard.step3.grpc.route_label': 'gRPC route',
'wizard.step3.grpc.request_label': 'Запрос',
'wizard.step3.grpc.response_label': 'Ответ',
'wizard.step4.title': 'Конфиг инструмента',
'wizard.step4.subtitle': 'Задайте имя инструмента, описание для LLM и определите входную/выходную схему. Описание — главный сигнал, по которому модель решает, вызывать ли этот инструмент.',
'wizard.step4.identity_title': 'Идентичность инструмента',
'wizard.step4.identity_subtitle': 'Машиночитаемое имя и описание для LLM',
'wizard.step4.tool_name': 'Имя инструмента',
'wizard.step4.tool_name_hint': 'Машиночитаемый идентификатор для MCP tool calls. Только строчные буквы, цифры и подчеркивания. После публикации изменить нельзя.',
'wizard.step4.display_name': 'Отображаемое имя',
'wizard.step4.display_name_hint': 'Человеко-читаемое имя для консоли и журнала аудита.',
'wizard.step4.tool_title': 'Tool title',
'wizard.step4.tool_title_hint': 'Короткая императивная фраза, которая попадет в MCP tool manifest.',
'wizard.step4.description': 'Описание',
'wizard.step4.description_hint': 'Описание для LLM. Пишите точно — это главный сигнал, по которому модель решает, нужно ли вызывать инструмент.',
'wizard.step4.description_title': 'Как писать хорошие описания',
'wizard.step4.description_body': 'Начинайте с глагола ("Создает", "Получает", "Обновляет"). Укажите ключевые требования ко входу. Опишите, как выглядит успешный ответ. Избегайте расплывчатых формулировок вроде "обрабатывает" или "управляет". См. рекомендации по описаниям →',
'wizard.step4.input_schema_title': 'Входная схема',
'wizard.step4.input_schema_subtitle': 'Параметры, которые LLM передает при вызове инструмента',
'wizard.step4.output_schema_title': 'Выходная схема',
'wizard.step4.output_schema_subtitle': 'Форма данных, которые LLM получает после успешного вызова',
'wizard.step4.schema_draft': 'JSON Schema draft-07',
'wizard.step4.protocol_agnostic_title': 'Схемы не зависят от протокола',
'wizard.step4.protocol_agnostic_body': 'Эти схемы описывают MCP-контракт, а не wire-format upstream-а. Маппинг на шаге 5 переводит одно в другое. Можно загрузить sample JSON-ответа, чтобы автоматически сгенерировать output schema.',
'wizard.step5.title': 'Mapping и исполнение',
'wizard.step5.subtitle': 'Определите, как аргументы MCP-инструмента мапятся в поля upstream-запроса и как ответ upstream-а мапится обратно в MCP output. Затем задайте execution-параметры — auth настраивается на уровне upstream на шаге 3.',
'wizard.step5.input_request': 'Input → Request',
'wizard.step5.output_response': 'Response → Output',
'wizard.step5.execution': 'Исполнение',
'wizard.step5.exec_title': 'Execution config',
'wizard.step5.exec_subtitle': 'Таймауты, retry policy и ссылка на auth profile',
'wizard.step5.live_title': 'Live-проверка и публикация',
'wizard.step5.draft_actions': 'Действия черновика',
'wizard.step5.live_save_title': 'Live-действия сначала сохраняют черновик',
'wizard.step5.live_save_body': 'Загрузка sample-ов, тестовые прогоны, YAML-действия, публикация и gRPC descriptor discovery сначала сохраняют текущий черновик перед вызовом backend-а.',
'wizard.step5.samples_title': 'Samples и генерация черновика',
'wizard.step5.samples_subtitle': 'Сохраните входные и выходные sample-ы, затем сгенерируйте схемы и маппинг из реальных payload-ов.',
'wizard.step5.input_sample': 'Входной sample',
'wizard.step5.output_sample': 'Выходной sample',
'wizard.step5.save_input_sample': 'Сохранить входной sample',
'wizard.step5.save_output_sample': 'Сохранить выходной sample',
'wizard.step5.generate_draft': 'Сгенерировать черновик из sample-ов',
'wizard.step5.test_title': 'Тестовый прогон',
'wizard.step5.test_subtitle': 'Проверьте текущий черновик на upstream-е перед публикацией.',
'wizard.step5.test_input_payload': 'Входной payload для теста',
'wizard.step5.run_test': 'Запустить тест',
'wizard.step5.use_response_output': 'Использовать ответ как выходной sample',
'wizard.step5.request_preview': 'Предпросмотр запроса',
'wizard.step5.response_preview': 'Предпросмотр ответа',
'wizard.step5.errors': 'Ошибки',
'wizard.step5.grpc_descriptor_title': 'gRPC descriptor set',
'wizard.step5.grpc_descriptor_subtitle': 'Загрузите скомпилированный descriptor set и откройте unary-методы из сохраненного черновика.',
'wizard.step5.choose_descriptor': 'Выбрать descriptor set',
'wizard.step5.upload_discover': 'Загрузить и открыть сервисы',
'wizard.step5.no_descriptor': 'Descriptor set не выбран',
'wizard.step5.yaml_title': 'Импорт и экспорт YAML',
'wizard.step5.yaml_subtitle': 'Переносите черновики операций между окружениями, не выходя из мастера.',
'wizard.step5.export_yaml': 'Экспорт YAML',
'wizard.step5.load_yaml_file': 'Загрузить YAML-файл',
'wizard.step5.import_yaml': 'Импорт YAML',
'wizard.step5.yaml_document': 'YAML-документ',
'wizard.step5.yaml_placeholder': 'Вставьте экспортированный YAML сюда, чтобы создать или обновить операцию.',
'wizard.step5.publish_title': 'Публикация',
'wizard.step5.publish_subtitle': 'Сохраните текущий черновик и откройте его опубликованным агентам.',
'wizard.step5.publish_hint': 'Публикация использует текущую версию черновика из этой сессии мастера.',
'wizard.step5.publish_action': 'Опубликовать операцию',
'wizard.step5.editable_title': 'Операция остается редактируемой в этом мастере',
'wizard.step5.editable_body': 'Сохранение создает или обновляет текущий черновик на месте. Используйте live-действия выше, чтобы загрузить sample-ы, протестировать черновик, экспортировать или импортировать YAML и опубликовать контракт, когда он будет готов.',
'wizard.live.failed_title': 'Не удалось выполнить live-действие',
'wizard.live.failed_body': 'Запрос к backend завершился ошибкой. Проверьте черновик и попробуйте еще раз.',
'wizard.error.no_workspace': 'Воркспейс не выбран',
@@ -1261,6 +1563,10 @@ function applyLang() {
document.querySelectorAll('[data-i18n-title]').forEach(function(el) {
el.title = t(el.getAttribute('data-i18n-title'));
});
// aria-label
document.querySelectorAll('[data-i18n-aria-label]').forEach(function(el) {
el.setAttribute('aria-label', t(el.getAttribute('data-i18n-aria-label')));
});
// highlight active language button in settings
var lang = localStorage.getItem('crank_lang') || 'en';
document.querySelectorAll('.lang-btn').forEach(function(btn) {
+16 -2
View File
@@ -1,5 +1,19 @@
(function () {
var STORAGE_KEY = 'crank_user';
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function roleLabel(role) {
var normalized = String(role || 'viewer').toLowerCase();
if (normalized === 'operator' || normalized === 'developer') return tKey('workspace_setup.role.operator');
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
return normalized.replace(/^\w/, function (character) {
return character.toUpperCase();
});
}
function currentUser() {
try {
@@ -23,11 +37,11 @@
var roleElement = document.querySelector('.user-dropdown-role');
if (nameElement) {
nameElement.textContent = user.name || 'Operator';
nameElement.textContent = user.name || tKey('nav.user_fallback');
}
if (roleElement) {
roleElement.textContent = (user.role || 'Viewer') + ' · ' + (user.workspace || 'workspace');
roleElement.textContent = roleLabel(user.role) + ' · ' + (user.workspace || tKey('nav.workspace_fallback'));
}
}
+8 -3
View File
@@ -31,7 +31,12 @@ function setStatus(id, text, isError) {
}
function titleCaseRole(role) {
return String(role || 'viewer').replace(/^\w/, function(character) {
var normalized = String(role || 'viewer').toLowerCase();
if (normalized === 'operator') return tKey('workspace_setup.role.operator');
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
return normalized.replace(/^\w/, function(character) {
return character.toUpperCase();
});
}
@@ -56,7 +61,7 @@ function populateCurrentSession(session) {
var workspaceLabel = membership && membership.workspace
? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id)
: 'current workspace';
: tKey('settings.session.current_workspace');
var roleLabel = membership ? titleCaseRole(membership.role) : tKey('workspace_setup.role.viewer');
summary.textContent = tfKey('settings.session.summary', {
email: session.user.email,
@@ -72,7 +77,7 @@ function populateProfile(session) {
var user = session.user;
document.getElementById('profile-avatar').textContent = initials(user.display_name, user.email);
document.getElementById('profile-display-name').textContent = user.display_name || 'Operator';
document.getElementById('profile-display-name').textContent = user.display_name || 'Crank';
document.getElementById('profile-email').textContent = user.email || '';
var firstName = document.getElementById('field-firstname');
+21 -6
View File
@@ -60,6 +60,11 @@ function goToStep(n) {
}
function _doGoToStep(n) {
document.querySelectorAll('.step-number[data-step]').forEach(function(element) {
var step = Number(element.getAttribute('data-step')) || 0;
element.textContent = tfKey('wizard.step_short', { step: step });
});
// 1. hide all panes, show target (step 4 is protocol-specific)
document.querySelectorAll('.step-pane').forEach(function(p) { p.style.display = 'none'; });
var panelId = (n === 3) ? step3PanelId() : 'step-panel-' + n;
@@ -95,7 +100,7 @@ function _doGoToStep(n) {
if (pctEl) pctEl.textContent = pct + '%';
// 4. update step counter
var counter = document.querySelector('.step-counter');
var counter = document.querySelector('[data-step-counter]');
if (counter) counter.innerHTML = tfKey('wizard.step_of', { step: n, total: TOTAL_STEPS });
// 5. back button
@@ -240,9 +245,9 @@ document.addEventListener('DOMContentLoaded', async function() {
var METHOD_CALLOUTS = {
GET: null,
DELETE: null,
POST: { icon: 'info', text: '<strong>POST selected — body encoding required.</strong> In step 4 you will define the JSON schema for the body payload. Crank will automatically serialize MCP tool arguments into the format you specify.' },
PUT: { icon: 'info', text: '<strong>PUT selected — full-resource replacement.</strong> The request body must contain a complete resource representation. Define the body schema in step 4.' },
PATCH: { icon: 'info', text: '<strong>PATCH selected — partial update.</strong> Only include the fields you want to modify in the body schema defined in step 4.' },
POST: { icon: 'info', text: { en: '<strong>POST selected — body encoding required.</strong> In step 4 you will define the JSON schema for the body payload. Crank will automatically serialize MCP tool arguments into the format you specify.', ru: '<strong>Выбран POST — требуется кодирование тела запроса.</strong> На шаге 4 вы зададите JSON-схему для body payload. Crank автоматически сериализует аргументы MCP-инструмента в выбранный формат.' } },
PUT: { icon: 'info', text: { en: '<strong>PUT selected — full-resource replacement.</strong> The request body must contain a complete resource representation. Define the body schema in step 4.', ru: '<strong>Выбран PUT — полная замена ресурса.</strong> Request body должен содержать полное представление ресурса. Задайте body schema на шаге 4.' } },
PATCH: { icon: 'info', text: { en: '<strong>PATCH selected — partial update.</strong> Only include the fields you want to modify in the body schema defined in step 4.', ru: '<strong>Выбран PATCH — частичное обновление.</strong> В body schema на шаге 4 включайте только те поля, которые хотите изменить.' } },
};
function selectMethod(btn) {
@@ -253,9 +258,11 @@ function selectMethod(btn) {
if (!callout) return;
var info = METHOD_CALLOUTS[method];
if (info) {
var lang = localStorage.getItem('crank_lang') || 'en';
var text = typeof info.text === 'string' ? info.text : (info.text[lang] || info.text.en);
callout.innerHTML =
'<svg width="15" height="15" style="flex-shrink:0;color:var(--accent)"><use href="' + (window.APP_BASE||'') + 'icons/general/info-circle.svg#icon"/></svg>' +
'<span>' + info.text + '</span>';
'<span>' + text + '</span>';
callout.style.display = 'flex';
} else {
callout.style.display = 'none';
@@ -420,7 +427,7 @@ function startNewUpstream() {
// Clear combobox trigger & hide preview
var val = document.getElementById('upstream-combobox-value');
if (val) val.innerHTML = '<span class="upstream-combobox-placeholder">' + tKey('wizard.step2.title') + '</span>';
if (val) val.innerHTML = '<span class="upstream-combobox-placeholder">' + tKey('wizard.step2.upstream_placeholder') + '</span>';
var preview = document.getElementById('upstream-preview');
if (preview) preview.style.display = 'none';
selectedUpstreamId = null;
@@ -639,6 +646,10 @@ function renderServiceResults(parsed, ids) {
}
function renderParsedProto(parsed) {
var notice = document.getElementById('proto-streaming-text');
if (notice && parsed && parsed.streamingCount) {
notice.textContent = tfKey('wizard.step3.grpc.streaming_hidden', { count: parsed.streamingCount });
}
renderServiceResults(parsed, {
notice: 'proto-streaming-notice', count: 'proto-streaming-count',
summary: 'proto-services-summary', list: 'proto-services-list', view: 'proto-parsed-view',
@@ -646,6 +657,10 @@ function renderParsedProto(parsed) {
}
function renderReflectionResult(parsed) {
var reflectText = document.querySelector('#reflect-streaming-notice .info-callout-text');
if (reflectText && parsed && parsed.streamingCount) {
reflectText.textContent = tfKey('wizard.step3.grpc.streaming_hidden', { count: parsed.streamingCount });
}
renderServiceResults(parsed, {
notice: 'reflect-streaming-notice', count: 'reflect-streaming-count',
summary: 'reflect-services-summary', list: 'reflect-services-list', view: 'reflect-parsed-view',
+6 -6
View File
@@ -125,12 +125,12 @@ function addMemberRow(container, membership) {
(
canManage
? '<select class="member-role-select" data-previous="' + membership.role + '" onchange="updateRole(this, \'' + membership.user.id + '\')">' +
'<option value="owner"' + (membership.role === 'owner' ? ' selected' : '') + '>Owner</option>' +
'<option value="admin"' + (membership.role === 'admin' ? ' selected' : '') + '>Admin</option>' +
'<option value="operator"' + (membership.role === 'operator' ? ' selected' : '') + '>Developer</option>' +
'<option value="viewer"' + (membership.role === 'viewer' ? ' selected' : '') + '>Viewer</option>' +
'<option value="owner"' + (membership.role === 'owner' ? ' selected' : '') + '>' + tKey('workspace_setup.role.owner') + '</option>' +
'<option value="admin"' + (membership.role === 'admin' ? ' selected' : '') + '>' + tKey('workspace_setup.role.admin') + '</option>' +
'<option value="operator"' + (membership.role === 'operator' ? ' selected' : '') + '>' + tKey('workspace_setup.role.operator') + '</option>' +
'<option value="viewer"' + (membership.role === 'viewer' ? ' selected' : '') + '>' + tKey('workspace_setup.role.viewer') + '</option>' +
'</select>' +
'<button class="member-remove-btn" type="button" onclick="removeMember(\'' + membership.user.id + '\', \'' + membership.user.display_name.replace(/'/g, "\\'") + '\')" title="Remove member">' +
'<button class="member-remove-btn" type="button" onclick="removeMember(\'' + membership.user.id + '\', \'' + membership.user.display_name.replace(/'/g, "\\'") + '\')" title="' + tKey('workspace_setup.members.remove') + '">' +
'<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>' +
'</button>'
: '<span class="member-role-badge ' + roleClass(membership.role) + '">' + roleLabel(membership.role) + '</span>'
@@ -189,7 +189,7 @@ function renderInvitations() {
'</div>' +
'<div class="member-last-active">—</div>' +
'<span class="member-role-badge" style="background:rgba(139,148,158,0.12);color:var(--text-muted);border:1px solid var(--border);">' + roleLabel(invitation.role) + '</span>' +
'<button class="member-remove-btn" type="button" onclick="revokeInviteById(\'' + invitation.id + '\')" title="Revoke invite">' +
'<button class="member-remove-btn" type="button" onclick="revokeInviteById(\'' + invitation.id + '\')" title="' + tKey('workspace_setup.members.revoke') + '">' +
'<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="2" y1="2" x2="14" y2="14"/><line x1="14" y1="2" x2="2" y2="14"/></svg>' +
'</button>';
pending.appendChild(row);