Add per-provider key provisioning instructions to API Keys card

Each provider row now has a collapsible details panel with accurate
step-by-step instructions and a direct link to the key creation page.
Includes billing notes for OpenAI and Anthropic (easy to miss gotchas),
free tier note for Gemini, and credits note for Grok.
This commit is contained in:
2026-05-11 12:47:12 -05:00
parent 18350761c5
commit 756f1f955e
+84 -16
View File
@@ -494,12 +494,26 @@ fn account_css() -> String {
.roadmap-items li { font-family: var(--body); font-size: .875rem; font-weight: 300; color: var(--t2); line-height: 1.6; padding-left: 1rem; position: relative; }
.roadmap-items li::before { content: \"-\"; position: absolute; left: 0; color: var(--navy-65); }
.signout-section { padding-top: 1rem; display: flex; justify-content: flex-end; }
.api-key-list { display: flex; flex-direction: column; gap: 1rem; }
.api-key-row { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.api-key-provider { display: flex; flex-direction: column; gap: .2rem; min-width: 120px; }
.api-key-list { display: flex; flex-direction: column; gap: 1.5rem; }
.api-key-entry { border-bottom: 1px solid var(--border); padding-bottom: 1.25rem; }
.api-key-entry:last-child { border-bottom: none; padding-bottom: 0; }
.api-key-header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: .625rem; }
.api-key-name { font-size: .875rem; font-weight: 500; color: var(--t1); }
.api-key-masked { font-size: .75rem; font-family: monospace; color: var(--t3); }
.api-key-actions { display: flex; gap: .5rem; }"
.api-key-row { display: flex; align-items: center; gap: .75rem; }
.api-key-actions { display: flex; gap: .5rem; flex-shrink: 0; }
.api-key-help { margin-top: .625rem; }
.api-key-help summary { font-size: .75rem; font-weight: 500; letter-spacing: .03em; color: var(--navy-65); cursor: pointer; list-style: none; padding: .25rem 0; user-select: none; }
.api-key-help summary::-webkit-details-marker { display: none; }
.api-key-help summary::before { content: \"\\25B8 \"; font-size: .6rem; }
.api-key-help[open] summary::before { content: \"\\25BE \"; }
.api-key-help-body { padding: .75rem 0 .125rem; }
.api-key-help-body ol { padding-left: 1.25rem; display: flex; flex-direction: column; gap: .375rem; }
.api-key-help-body li { font-size: .8125rem; font-weight: 300; color: var(--t2); line-height: 1.55; }
.api-key-help-body a { color: var(--navy-65); text-decoration: underline; text-underline-offset: 2px; }
.api-key-help-body a:hover { color: var(--navy); }
.api-key-note { font-size: .75rem; font-weight: 300; color: var(--t3); margin-top: .625rem; line-height: 1.55; padding: .5rem .75rem; background: var(--bg2); border-left: 2px solid var(--navy-b); }
.api-key-note a { color: var(--navy-65); text-decoration: underline; text-underline-offset: 2px; }"
"<style>" + css + "</style>"
}
@@ -810,30 +824,84 @@ fn account_devices_card() -> String {
)
}
fn api_key_provider_row(provider_id: String, provider_name: String, placeholder: String) -> String {
fn api_key_provider_row(provider_id: String, provider_name: String, placeholder: String, instructions: String) -> String {
el_div(
"class=\"api-key-row\"",
"class=\"api-key-entry\"",
el_div(
"class=\"api-key-provider\"",
"class=\"api-key-header\"",
el_span("class=\"api-key-name\"", provider_name) +
el_span("class=\"api-key-masked\" id=\"apikey-masked-" + provider_id + "\"", "Not configured")
) +
"<input type=\"password\" id=\"apikey-input-" + provider_id + "\" class=\"acct-input\" placeholder=\"" + placeholder + "\" autocomplete=\"off\" style=\"margin-bottom:0;flex:1\">" +
el_div(
"class=\"api-key-actions\"",
el_button("type=\"button\" class=\"btn-primary\" style=\"padding:.5rem 1rem;font-size:.75rem\" onclick=\"saveApiKey('" + provider_id + "')\"", "Save") +
el_button("type=\"button\" class=\"btn-ghost\" style=\"padding:.5rem 1rem;font-size:.75rem;display:none\" id=\"apikey-del-" + provider_id + "\" onclick=\"deleteApiKey('" + provider_id + "')\"", "Remove")
)
"class=\"api-key-row\"",
"<input type=\"password\" id=\"apikey-input-" + provider_id + "\" class=\"acct-input\" placeholder=\"" + placeholder + "\" autocomplete=\"off\" style=\"margin-bottom:0;flex:1\">" +
el_div(
"class=\"api-key-actions\"",
el_button("type=\"button\" class=\"btn-primary\" style=\"padding:.5rem 1rem;font-size:.75rem\" onclick=\"saveApiKey('" + provider_id + "')\"", "Save") +
el_button("type=\"button\" class=\"btn-ghost\" style=\"padding:.5rem 1rem;font-size:.75rem;display:none\" id=\"apikey-del-" + provider_id + "\" onclick=\"deleteApiKey('" + provider_id + "')\"", "Remove")
)
) +
instructions
)
}
fn account_api_keys_section() -> String {
let openai_help: String =
"<details class=\"api-key-help\">" +
"<summary>How to get an OpenAI key</summary>" +
"<div class=\"api-key-help-body\"><ol>" +
"<li>Go to <a href=\"https://platform.openai.com/api-keys\" target=\"_blank\" rel=\"noopener\">platform.openai.com/api-keys &#8599;</a></li>" +
"<li>Click <strong>Create new secret key</strong></li>" +
"<li>Give it a name (e.g. &#8220;Neuron&#8221;), then click <strong>Create secret key</strong></li>" +
"<li>Copy the key immediately &#8212; it is only shown once</li>" +
"</ol>" +
"<p class=\"api-key-note\">You need billing set up before making API calls. Add a payment method at <a href=\"https://platform.openai.com/settings/organization/billing\" target=\"_blank\" rel=\"noopener\">platform.openai.com &#8599;</a></p>" +
"</div></details>"
let anthropic_help: String =
"<details class=\"api-key-help\">" +
"<summary>How to get an Anthropic key</summary>" +
"<div class=\"api-key-help-body\"><ol>" +
"<li>Go to <a href=\"https://console.anthropic.com/settings/keys\" target=\"_blank\" rel=\"noopener\">console.anthropic.com/settings/keys &#8599;</a></li>" +
"<li>Click <strong>Create Key</strong></li>" +
"<li>Give it a name (e.g. &#8220;Neuron&#8221;), then click <strong>Create Key</strong></li>" +
"<li>Copy the key immediately &#8212; it is only shown once</li>" +
"</ol>" +
"<p class=\"api-key-note\">You need to add credits before making API calls. Go to <a href=\"https://console.anthropic.com/settings/plans\" target=\"_blank\" rel=\"noopener\">console.anthropic.com &#8594; Plans &amp; Billing &#8599;</a> to add credits.</p>" +
"</div></details>"
let gemini_help: String =
"<details class=\"api-key-help\">" +
"<summary>How to get a Gemini key</summary>" +
"<div class=\"api-key-help-body\"><ol>" +
"<li>Go to <a href=\"https://aistudio.google.com/apikey\" target=\"_blank\" rel=\"noopener\">aistudio.google.com/apikey &#8599;</a></li>" +
"<li>Sign in with your Google account if prompted</li>" +
"<li>Click <strong>Create API key</strong></li>" +
"<li>Select an existing Google Cloud project or create a new one</li>" +
"<li>Copy the key</li>" +
"</ol>" +
"<p class=\"api-key-note\">The free tier (Gemini 1.5 Flash) has generous rate limits. Paid usage is billed through your Google Cloud account.</p>" +
"</div></details>"
let grok_help: String =
"<details class=\"api-key-help\">" +
"<summary>How to get a Grok key</summary>" +
"<div class=\"api-key-help-body\"><ol>" +
"<li>Go to <a href=\"https://console.x.ai/\" target=\"_blank\" rel=\"noopener\">console.x.ai &#8599;</a></li>" +
"<li>Sign in with your X (Twitter) account</li>" +
"<li>In the left sidebar, click <strong>API Keys</strong></li>" +
"<li>Click <strong>Create API Key</strong>, give it a name</li>" +
"<li>Copy the key immediately &#8212; it is only shown once</li>" +
"</ol>" +
"<p class=\"api-key-note\">xAI offers free monthly credits to new accounts. Usage beyond the free tier is billed per token.</p>" +
"</div></details>"
let providers: String = el_div(
"class=\"api-key-list\"",
api_key_provider_row("openai", "OpenAI", "sk-...") +
api_key_provider_row("anthropic", "Anthropic", "sk-ant-...") +
api_key_provider_row("gemini", "Gemini", "AIza...") +
api_key_provider_row("grok", "Grok", "xai-...")
api_key_provider_row("openai", "OpenAI", "sk-...", openai_help) +
api_key_provider_row("anthropic", "Anthropic", "sk-ant-...", anthropic_help) +
api_key_provider_row("gemini", "Gemini", "AIza...", gemini_help) +
api_key_provider_row("grok", "Grok", "xai-...", grok_help)
)
el_div(
"id=\"api-keys-section\" style=\"display:none\"",