feat: Show me more appends (exclude already-shown, look up only new domains); 36 per page; start over (v0.6.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XStQKxPfEjrTvWo83KFCxG
This commit is contained in:
2026-08-17 00:10:26 -05:00
parent cce9ee3c7c
commit ff0a262c43
5 changed files with 68 additions and 17 deletions

View File

@@ -40,6 +40,12 @@ bucket, and a 10-minute result cache.
`(keywords, style, seed)` always yields the same list, so re-renders don't
reshuffle results and a shared URL reproduces what the sender saw.
The finder shows 36 names per page and **Show me more appends**: the names
already on screen are passed as `exclude`, dropped before ranking, so each page
is genuinely new (the seed alone would mostly reshuffle the same pool), and only
the new domains get looked up. Changing keywords, style or extensions starts a
fresh list; **start over** clears it. When the pool runs dry the app says so.
Eight strategies (compound, suffix-word, action, blend, coined, clipped, root,
domain hack) over the vocabulary in `src/lib/wordbank.ts`. The strategies are
cheap and dumb on purpose — **quality comes from `scoreName`**, so tune the

View File

@@ -1,6 +1,6 @@
{
"name": "names-poweredbyotm",
"version": "0.5.1",
"version": "0.6.0",
"private": true,
"scripts": {
"dev": "next dev --port 3000",

View File

@@ -21,7 +21,7 @@ import { AiControls } from "./AiControls";
/** How many names to put on screen. Each one costs `selectedTlds.length`
* registry lookups, so this is the main dial on how hard we lean on the
* registries — keep it modest. */
const NAME_COUNT = 24;
const NAME_COUNT = 36;
const STYLES: Array<{ id: Style; label: string; note: string }> = [
{ id: "balanced", label: "Balanced", note: "a bit of everything" },
@@ -52,6 +52,9 @@ export default function Finder() {
// as a one-line summary so it's obvious whether AI actually contributed
// (issue #3). null until a run completes.
const [aiCount, setAiCount] = useState<number | null>(null);
// What the current list was built from. "Show me more" APPENDS while this
// matches; changing keywords/style/extensions starts a fresh list.
const [listKey, setListKey] = useState<string | null>(null);
// Hide names whose every checked extension is taken (issue #3). Persisted.
const [hideTaken, setHideTaken] = useState(false);
const { saved, savedNames, toggleSaved, removeSaved, recheckSaved, copySaved, rechecking, copied } =
@@ -101,7 +104,7 @@ export default function Finder() {
);
const lookup = useCallback(async (domains: string[], myRun: number) => {
setPending(new Set(domains));
setPending((prev) => new Set([...prev, ...domains]));
for (let i = 0; i < domains.length; i += BATCH) {
if (runId.current !== myRun) return; // superseded by a newer search
@@ -129,17 +132,25 @@ export default function Finder() {
}, []);
const run = useCallback(
async (nextSeed: number) => {
const myRun = ++runId.current;
async (nextSeed: number, append: boolean) => {
// A fresh list abandons in-flight lookups; an append shares the current
// run so the earlier cards keep resolving.
const myRun = append ? runId.current : ++runId.current;
const base = append ? ideas : [];
setBusy(true);
setError(null);
if (!append) {
setChecks({});
setPending(new Set());
}
const shown = new Set(base.map((i) => i.name));
let list = generateNames({
keywords: parsedKeywords,
style,
count: NAME_COUNT,
seed: nextSeed,
exclude: shown,
});
// AI names: the visitor's own key if they saved one and asked us to use
@@ -154,11 +165,11 @@ export default function Finder() {
? 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 seen = new Set([...shown, ...list.map((i) => i.name)]);
const extra: Idea[] = aiNames
.filter((n) => !seen.has(n))
.map((name) => ({ name, strategy: "ai" as const, score: scoreName(name, parsedKeywords) }));
list = [...extra, ...list].slice(0, NAME_COUNT + extra.length);
list = [...extra, ...list];
gotAi = extra.length;
} catch (err) {
setError(
@@ -168,18 +179,37 @@ export default function Finder() {
}
if (runId.current !== myRun) return;
setIdeas(list);
setAiCount(wantsOwnKey || wantsPlatform ? gotAi : null);
if (list.length === 0 && append) {
setError("That's everything the generator has for these keywords — try different words or a different style.");
setBusy(false);
return;
}
setIdeas([...base, ...list]);
setAiCount((prev) => (wantsOwnKey || wantsPlatform ? (append ? (prev ?? 0) + gotAi : gotAi) : append ? prev : null));
setBusy(false);
await lookup(domainsFor(list, tlds), myRun);
},
[account, ai, brief, lookup, parsedKeywords, platformAi, style, tlds, useAi, usePlatformAi],
[account, ai, brief, ideas, lookup, parsedKeywords, platformAi, style, tlds, useAi, usePlatformAi],
);
const queryKey = `${parsedKeywords.join(",")}|${style}|${[...tlds].sort().join(",")}`;
const onGenerate = () => {
const next = seed + 1;
setSeed(next);
void run(next);
const append = listKey === queryKey && ideas.length > 0;
setListKey(queryKey);
void run(next, append);
};
const onStartOver = () => {
runId.current++;
setIdeas([]);
setChecks({});
setPending(new Set());
setAiCount(null);
setListKey(null);
setError(null);
};
const toggleTld = (tld: string) => {
@@ -256,7 +286,7 @@ export default function Finder() {
<div className="controls">
<button className="primary" onClick={onGenerate} disabled={busy || tlds.length === 0}>
{busy ? "Thinking…" : ideas.length ? "Show me more" : "Find names"}
{busy ? "Thinking…" : ideas.length && listKey === queryKey ? "Show me more" : "Find names"}
</button>
<label
@@ -355,6 +385,10 @@ export default function Finder() {
<>{ideas.length} names</>
)}
{hideTaken && hiddenCount > 0 ? <> · {hiddenCount} taken hidden</> : null}
{" · "}
<button type="button" className="linkish" onClick={onStartOver}>
start over
</button>
</span>
<label className="results-toggle">
<input type="checkbox" checked={hideTaken} onChange={(e) => toggleHideTaken(e.target.checked)} />
@@ -378,7 +412,7 @@ export default function Finder() {
</div>
<div className="results-more">
<button className="primary" onClick={onGenerate} disabled={busy || tlds.length === 0}>
{busy ? "Thinking…" : "Show me more"}
{busy ? "Thinking…" : listKey === queryKey ? "Show me more" : "Find names"}
</button>
<button type="button" className="mini" onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}>
Back to top

View File

@@ -59,6 +59,9 @@ export interface GenerateOptions {
count?: number;
/** Changing this reshuffles the whole list. Same seed ⇒ same output. */
seed?: number;
/** Names already shown — dropped before ranking so "show me more" appends
* genuinely new ideas instead of a reshuffle of the same pool. */
exclude?: ReadonlySet<string>;
}
// ---------------------------------------------------------------------------
@@ -309,7 +312,7 @@ const STYLE_WEIGHTS: Record<Style, Record<GeneratedStrategy, number>> = {
* and only the top `count` survive.
*/
export function generateNames(opts: GenerateOptions): Idea[] {
const { keywords, style = "balanced", count = 60, seed = 1 } = opts;
const { keywords, style = "balanced", count = 60, seed = 1, exclude } = opts;
const rng = makeRng(seed);
const { seeds, related } = expandKeywords(keywords);
@@ -408,7 +411,7 @@ export function generateNames(opts: GenerateOptions): Idea[] {
}
const ranked = [...pool.values()]
.filter((i) => i.score > 0)
.filter((i) => i.score > 0 && !exclude?.has(i.name))
.sort((a, b) => b.score - a.score || a.name.length - b.name.length || a.name.localeCompare(b.name));
return diversify(ranked, count);

View File

@@ -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.5.1";
export const VERSION = "0.6.0";
export interface ChangelogEntry {
version: string;
@@ -11,6 +11,14 @@ export interface ChangelogEntry {
}
export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.6.0",
date: "2026-08-17",
changes: [
"Show me more now adds to the list instead of replacing it, and only checks the new names — what you've already seen stays put and isn't looked up again. 36 names per page (was 24).",
"Each page is genuinely new: names already on screen are excluded before ranking, not just reshuffled. Changing keywords, style or extensions starts a fresh list; 'start over' clears it.",
],
},
{
version: "0.5.1",
date: "2026-08-17",