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:
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "names-poweredbyotm",
|
||||
"version": "0.5.1",
|
||||
"version": "0.6.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3000",
|
||||
|
||||
@@ -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);
|
||||
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() {
|
||||
|
||||
<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 ↑
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user