- OTM SSO: /api/auth/otm-sso verifies the control-plane ticket (vendored verifier), in-memory jti claim, mints an HMAC cookie (src/lib/session.ts); /api/auth/session + /api/auth/signout. No accounts, no DB. - /api/ai-names: server-side call to the platform's metered gateway (ANTHROPIC_BASE_URL + per-app gateway token), 401 without a session, ~20 req/h per OTM account. Prompt/parser shared with the BYO-key path via src/lib/ai-prompt.ts. - Finder: 'Sign in with OTM for AI names' link → 'AI names' toggle when signed in; own key still overrides. Pairs with platform 0.116.0 (needsAnthropic + needsAuthSecret on names). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XStQKxPfEjrTvWo83KFCxG
109 lines
3.6 KiB
TypeScript
109 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { loadAiSettings, saveAiSettings, type AiProvider } from "@/lib/ai";
|
|
|
|
export default function SettingsPage() {
|
|
const [provider, setProvider] = useState<AiProvider>("anthropic");
|
|
const [key, setKey] = useState("");
|
|
const [saved, setSaved] = useState(false);
|
|
const [hasStored, setHasStored] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const existing = loadAiSettings();
|
|
if (existing) {
|
|
setProvider(existing.provider);
|
|
setKey(existing.key);
|
|
setHasStored(true);
|
|
}
|
|
}, []);
|
|
|
|
const onSave = () => {
|
|
const trimmed = key.trim();
|
|
saveAiSettings(trimmed ? { provider, key: trimmed } : null);
|
|
setHasStored(Boolean(trimmed));
|
|
setSaved(true);
|
|
window.setTimeout(() => setSaved(false), 2500);
|
|
};
|
|
|
|
const onClear = () => {
|
|
saveAiSettings(null);
|
|
setKey("");
|
|
setHasStored(false);
|
|
setSaved(true);
|
|
window.setTimeout(() => setSaved(false), 2500);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<h1>Settings</h1>
|
|
<p className="tagline">
|
|
<Link href="/">← Back to the name finder</Link>
|
|
</p>
|
|
|
|
<h2>Bring your own AI key (optional)</h2>
|
|
<p className="tagline">
|
|
The name finder works fully offline without this — the built-in generator needs no
|
|
key and costs nothing. Adding your own API key gets you a second, usually more
|
|
imaginative set of suggestions alongside it. Don’t have a key? Sign in with a free
|
|
OTM account on the main page and AI names are included — a key here overrides that.
|
|
</p>
|
|
|
|
<p className="notice info">
|
|
Your key is stored in <strong>this browser only</strong> and the request goes straight
|
|
from your browser to {provider === "anthropic" ? "Anthropic" : "Google"}. It is never sent
|
|
to our server and never appears in our logs. Usage is billed to your own account, so use
|
|
a key with a spending limit set.
|
|
</p>
|
|
|
|
<div className="field">
|
|
<label htmlFor="provider">Provider</label>
|
|
<select id="provider" value={provider} onChange={(e) => setProvider(e.target.value as AiProvider)}>
|
|
<option value="anthropic">Anthropic (Claude)</option>
|
|
<option value="gemini">Google (Gemini)</option>
|
|
</select>
|
|
<p className="hint">
|
|
{provider === "anthropic" ? (
|
|
<>
|
|
Create a key at{" "}
|
|
<a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener noreferrer">
|
|
console.anthropic.com
|
|
</a>
|
|
. Starts with <code>sk-ant-</code>.
|
|
</>
|
|
) : (
|
|
<>
|
|
Create a key at{" "}
|
|
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer">
|
|
aistudio.google.com
|
|
</a>
|
|
. Gemini has a free tier that is plenty for this.
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label htmlFor="key">API key</label>
|
|
<input
|
|
id="key"
|
|
type="password"
|
|
value={key}
|
|
autoComplete="off"
|
|
placeholder={provider === "anthropic" ? "sk-ant-…" : "AIza…"}
|
|
onChange={(e) => setKey(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="controls">
|
|
<button className="primary" onClick={onSave}>
|
|
Save key
|
|
</button>
|
|
{hasStored ? <button onClick={onClear}>Remove key</button> : null}
|
|
{saved ? <span style={{ color: "var(--ok)", fontSize: 14 }}>Saved to this browser.</span> : null}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|