Stage: CI fixes, pricing buttons, API key provisioning #90

Merged
will.anderson merged 4 commits from fix/stage-ci-paths into stage 2026-05-11 17:49:42 +00:00
Showing only changes of commit 756f1f955e - Show all commits
+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\"",