feat: 'Tell the AI more' free-text brief for AI names (v0.5.0)
Appended to the shared prompt (lib/ai-prompt.ts) on both AI paths; capped at 300 chars client- and server-side; shown only when AI is on; kept in sessionStorage so it survives 'Show me more'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XStQKxPfEjrTvWo83KFCxG
This commit is contained in:
@@ -70,6 +70,13 @@ holds a per-app gateway token, never a real Anthropic key, and the route refuses
|
||||
anonymous requests and caps each account at roughly 20 requests an hour. The
|
||||
platform's monthly AI cap is the backstop.
|
||||
|
||||
There's no chat: the AI is briefed from the same keywords box and Style chip
|
||||
as the built-in generator (`src/lib/ai-prompt.ts` is the rulebook), and its
|
||||
names are merged in front of the built-in list, tagged **AI**, and checked like
|
||||
everything else. When AI is on, a **Tell the AI more** field appears — tone,
|
||||
words to include or avoid, audience — which is appended to that brief (capped
|
||||
at 300 chars, enforced client- and server-side). The built-in generator ignores it.
|
||||
|
||||
Sign-in is OTM SSO: `/sso/authorize?aud=app:names` on the control plane hands
|
||||
back a 60-second single-use ticket to `/api/auth/otm-sso`, which mints a small
|
||||
HMAC-signed cookie (`names.session`). There are no user accounts in this app.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "names-poweredbyotm",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3000",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { readSession, platformAiEnabled } from "@/lib/session";
|
||||
import { SYSTEM_PROMPT, userPrompt, parseNames } from "@/lib/ai-prompt";
|
||||
import { SYSTEM_PROMPT, userPrompt, parseNames, BRIEF_MAX } from "@/lib/ai-prompt";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -47,6 +47,7 @@ interface Body {
|
||||
keywords?: unknown;
|
||||
style?: unknown;
|
||||
count?: unknown;
|
||||
brief?: unknown;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
@@ -79,6 +80,7 @@ export async function POST(req: NextRequest) {
|
||||
: [];
|
||||
const style = typeof body.style === "string" ? body.style.slice(0, 20) : "balanced";
|
||||
const count = Math.max(4, Math.min(MAX_COUNT, typeof body.count === "number" ? Math.floor(body.count) : 16));
|
||||
const brief = typeof body.brief === "string" ? body.brief.slice(0, BRIEF_MAX) : "";
|
||||
|
||||
const base = process.env.ANTHROPIC_BASE_URL!.replace(/\/+$/, "");
|
||||
let upstream: Response;
|
||||
@@ -94,7 +96,7 @@ export async function POST(req: NextRequest) {
|
||||
model: MODEL,
|
||||
max_tokens: 1024,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: [{ role: "user", content: userPrompt(keywords.length ? keywords : ["a new business"], style, count) }],
|
||||
messages: [{ role: "user", content: userPrompt(keywords.length ? keywords : ["a new business"], style, count, brief) }],
|
||||
}),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
|
||||
@@ -398,6 +398,10 @@ h2 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.field.brief {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.signin {
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DEFAULT_TLDS, TLDS, UNVERIFIABLE_TLDS } from "@/lib/tlds";
|
||||
import { DEFAULT_REGISTRAR, REGISTRARS } from "@/lib/registrars";
|
||||
import { generateWithAi, loadAiSettings, type AiSettings } from "@/lib/ai";
|
||||
import { generateWithPlatformAi } from "@/lib/platform-ai";
|
||||
import { BRIEF_MAX } from "@/lib/ai-prompt";
|
||||
import { BATCH, domainsFor, fetchAvailability, type DomainCheck } from "@/lib/availability-client";
|
||||
import { useOtmSession } from "@/hooks/useOtmSession";
|
||||
import { useWishlist } from "@/hooks/useWishlist";
|
||||
@@ -43,6 +44,9 @@ export default function Finder() {
|
||||
const [ai, setAi] = useState<AiSettings | null>(null);
|
||||
const [useAi, setUseAi] = useState(false);
|
||||
const [usePlatformAi, setUsePlatformAi] = useState(true);
|
||||
// Free-text guidance for the AI ("premium, no puns"). Kept in sessionStorage
|
||||
// so it survives "Show me more" and a reload, but not forever.
|
||||
const [brief, setBrief] = useState("");
|
||||
const { account, platformAi, signinNote, signOut } = useOtmSession();
|
||||
// How many names the last run got from AI vs the built-in generator — shown
|
||||
// as a one-line summary so it's obvious whether AI actually contributed
|
||||
@@ -59,11 +63,21 @@ export default function Finder() {
|
||||
setAi(loadAiSettings());
|
||||
try {
|
||||
setHideTaken(window.localStorage.getItem("names.hideTaken") === "1");
|
||||
setBrief(window.sessionStorage.getItem("names.brief") ?? "");
|
||||
} catch {
|
||||
/* private mode */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onBrief = (v: string) => {
|
||||
setBrief(v);
|
||||
try {
|
||||
window.sessionStorage.setItem("names.brief", v);
|
||||
} catch {
|
||||
/* private mode */
|
||||
}
|
||||
};
|
||||
|
||||
const toggleHideTaken = (v: boolean) => {
|
||||
setHideTaken(v);
|
||||
try {
|
||||
@@ -137,8 +151,8 @@ export default function Finder() {
|
||||
if (wantsOwnKey || wantsPlatform) {
|
||||
try {
|
||||
const aiNames = wantsOwnKey
|
||||
? await generateWithAi(ai, parsedKeywords, style, 16)
|
||||
: await generateWithPlatformAi(parsedKeywords, style, 16);
|
||||
? await generateWithAi(ai, parsedKeywords, style, 16, brief)
|
||||
: await generateWithPlatformAi(parsedKeywords, style, 16, brief);
|
||||
if (runId.current !== myRun) return;
|
||||
const seen = new Set(list.map((i) => i.name));
|
||||
const extra: Idea[] = aiNames
|
||||
@@ -159,7 +173,7 @@ export default function Finder() {
|
||||
setBusy(false);
|
||||
await lookup(domainsFor(list, tlds), myRun);
|
||||
},
|
||||
[account, ai, lookup, parsedKeywords, platformAi, style, tlds, useAi, usePlatformAi],
|
||||
[account, ai, brief, lookup, parsedKeywords, platformAi, style, tlds, useAi, usePlatformAi],
|
||||
);
|
||||
|
||||
const onGenerate = () => {
|
||||
@@ -173,6 +187,7 @@ export default function Finder() {
|
||||
};
|
||||
|
||||
const showsUnverifiable = tlds.some((t) => UNVERIFIABLE_TLDS.includes(t));
|
||||
const aiOn = ai ? useAi : Boolean(platformAi && account && usePlatformAi);
|
||||
|
||||
/** A name is "all taken" once every domain we checked for it resolved taken.
|
||||
* Still-checking or unknown names stay visible — hiding is for certainty. */
|
||||
@@ -273,6 +288,27 @@ export default function Finder() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{aiOn ? (
|
||||
<div className="field brief">
|
||||
<label htmlFor="brief">Tell the AI more (optional)</label>
|
||||
<input
|
||||
id="brief"
|
||||
type="text"
|
||||
value={brief}
|
||||
maxLength={BRIEF_MAX}
|
||||
placeholder="sounds premium, no puns, avoid the word auto, could work in Spanish too"
|
||||
onChange={(e) => onBrief(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onGenerate();
|
||||
}}
|
||||
/>
|
||||
<p className="hint">
|
||||
Tone, words to include or avoid, who it’s for — anything the keywords don’t say.
|
||||
Only the AI sees this; the built-in generator ignores it.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tlds.length === 0 ? (
|
||||
<p className="notice info">Pick at least one extension to check.</p>
|
||||
) : null}
|
||||
|
||||
@@ -17,12 +17,20 @@ export const SYSTEM_PROMPT = [
|
||||
'Respond with ONLY a JSON array of strings. No prose, no code fence.',
|
||||
].join("\n");
|
||||
|
||||
export function userPrompt(keywords: string[], style: string, count: number): string {
|
||||
return [
|
||||
/** Free-text guidance the visitor can add ("premium, no puns, avoid 'auto'").
|
||||
* Hard cap so a pasted essay can't balloon the request — enforced here AND in
|
||||
* the server route, since the own-key path calls this straight from the browser. */
|
||||
export const BRIEF_MAX = 300;
|
||||
|
||||
export function userPrompt(keywords: string[], style: string, count: number, brief = ""): string {
|
||||
const lines = [
|
||||
`Business description / keywords: ${keywords.join(", ")}`,
|
||||
`Preferred style: ${style}`,
|
||||
`Return exactly ${count} names as a JSON array of lowercase strings.`,
|
||||
].join("\n");
|
||||
];
|
||||
const extra = brief.trim().slice(0, BRIEF_MAX);
|
||||
if (extra) lines.push(`Extra guidance from the person naming the business: ${extra}`);
|
||||
lines.push(`Return exactly ${count} names as a JSON array of lowercase strings.`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** Pull a JSON array of names out of a model response that may still have prose
|
||||
|
||||
@@ -98,8 +98,9 @@ export async function generateWithAi(
|
||||
keywords: string[],
|
||||
style: string,
|
||||
count = 24,
|
||||
brief = "",
|
||||
): Promise<string[]> {
|
||||
const prompt = userPrompt(keywords, style, count);
|
||||
const prompt = userPrompt(keywords, style, count, brief);
|
||||
const text =
|
||||
settings.provider === "anthropic"
|
||||
? await callAnthropic(settings.key, prompt)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// Client helper for the platform-gateway AI path: POST /api/ai-names and get
|
||||
// names back. Requires the OTM sign-in cookie; the route does the gating.
|
||||
|
||||
export async function generateWithPlatformAi(keywords: string[], style: string, count = 16): Promise<string[]> {
|
||||
export async function generateWithPlatformAi(
|
||||
keywords: string[],
|
||||
style: string,
|
||||
count = 16,
|
||||
brief = "",
|
||||
): Promise<string[]> {
|
||||
const res = await fetch("/api/ai-names", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ keywords, style, count }),
|
||||
body: JSON.stringify({ keywords, style, count, brief }),
|
||||
});
|
||||
const body = (await res.json().catch(() => ({}))) as { names?: string[]; error?: string };
|
||||
if (!res.ok) throw new Error(body.error ?? `AI request failed (${res.status}).`);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// /changelog. Bump VERSION and prepend an entry when shipping a user-facing
|
||||
// change. Same convention as rpo.
|
||||
|
||||
export const VERSION = "0.4.1";
|
||||
export const VERSION = "0.5.0";
|
||||
|
||||
export interface ChangelogEntry {
|
||||
version: string;
|
||||
@@ -11,6 +11,13 @@ export interface ChangelogEntry {
|
||||
}
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
version: "0.5.0",
|
||||
date: "2026-08-16",
|
||||
changes: [
|
||||
"Tell the AI more: when AI names are on, an extra line lets you steer them — tone, words to include or avoid, who it's for. It's added to the AI brief (the keywords and style still lead); the built-in generator ignores it.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "0.4.1",
|
||||
date: "2026-08-16",
|
||||
|
||||
Reference in New Issue
Block a user