feat: platform AI names behind Sign in with OTM (v0.3.0)
- 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
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
// Optional AI name generation, using the VISITOR'S OWN API key.
|
||||
// Optional AI name generation, using the VISITOR'S OWN API key. (The other AI
|
||||
// path — the platform's metered gateway for signed-in OTM accounts — is
|
||||
// src/lib/platform-ai.ts + app/api/ai-names; both share src/lib/ai-prompt.ts.)
|
||||
//
|
||||
// The key lives in this browser's localStorage and the request goes straight
|
||||
// from the browser to Anthropic/Google. It never touches our server, never
|
||||
@@ -11,6 +13,8 @@
|
||||
// that owns the key — the distinction is whose key it is. The user pasted their
|
||||
// own, knowingly, into their own browser.
|
||||
|
||||
import { SYSTEM_PROMPT, userPrompt, parseNames } from "@/lib/ai-prompt";
|
||||
|
||||
export type AiProvider = "anthropic" | "gemini";
|
||||
|
||||
export interface AiSettings {
|
||||
@@ -39,46 +43,6 @@ export function saveAiSettings(settings: AiSettings | null): void {
|
||||
else window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT = [
|
||||
"You name businesses. Given a description, return brandable company names.",
|
||||
"",
|
||||
"Rules:",
|
||||
"- 4 to 12 letters, lowercase, letters only (no spaces, digits, or hyphens).",
|
||||
"- Easy to say out loud and to spell after hearing it once.",
|
||||
"- Prefer real words, evocative compounds, and clean coinages.",
|
||||
"- Avoid: the literal category word alone, startup filler (-ly on everything,",
|
||||
" 'solutions', 'synergy', 'hub' overuse), and misspellings of common words.",
|
||||
"- Vary the register: some grounded and concrete, some abstract and premium.",
|
||||
"",
|
||||
'Respond with ONLY a JSON array of strings. No prose, no code fence.',
|
||||
].join("\n");
|
||||
|
||||
function userPrompt(keywords: string[], style: string, count: number): string {
|
||||
return [
|
||||
`Business description / keywords: ${keywords.join(", ")}`,
|
||||
`Preferred style: ${style}`,
|
||||
`Return exactly ${count} names as a JSON array of lowercase strings.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/** Pull a JSON array of names out of a model response that may still have prose
|
||||
* or a code fence around it. */
|
||||
function parseNames(text: string): string[] {
|
||||
const start = text.indexOf("[");
|
||||
const end = text.lastIndexOf("]");
|
||||
if (start === -1 || end === -1 || end <= start) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(text.slice(start, end + 1)) as unknown;
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed
|
||||
.filter((n): n is string => typeof n === "string")
|
||||
.map((n) => n.toLowerCase().replace(/[^a-z]/g, ""))
|
||||
.filter((n) => n.length >= 3 && n.length <= 18);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function callAnthropic(key: string, prompt: string): Promise<string> {
|
||||
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
|
||||
Reference in New Issue
Block a user