diff --git a/README.md b/README.md index f1d9f26..c867c7c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index a3d3b41..3171b96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "names-poweredbyotm", - "version": "0.5.1", + "version": "0.6.0", "private": true, "scripts": { "dev": "next dev --port 3000", diff --git a/src/components/Finder.tsx b/src/components/Finder.tsx index 4b1d8e6..8a71545 100644 --- a/src/components/Finder.tsx +++ b/src/components/Finder.tsx @@ -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(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(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); - setChecks({}); + 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() {