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
|
`(keywords, style, seed)` always yields the same list, so re-renders don't
|
||||||
reshuffle results and a shared URL reproduces what the sender saw.
|
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,
|
Eight strategies (compound, suffix-word, action, blend, coined, clipped, root,
|
||||||
domain hack) over the vocabulary in `src/lib/wordbank.ts`. The strategies are
|
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
|
cheap and dumb on purpose — **quality comes from `scoreName`**, so tune the
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "names-poweredbyotm",
|
"name": "names-poweredbyotm",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --port 3000",
|
"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`
|
/** 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
|
* registry lookups, so this is the main dial on how hard we lean on the
|
||||||
* registries — keep it modest. */
|
* registries — keep it modest. */
|
||||||
const NAME_COUNT = 24;
|
const NAME_COUNT = 36;
|
||||||
|
|
||||||
const STYLES: Array<{ id: Style; label: string; note: string }> = [
|
const STYLES: Array<{ id: Style; label: string; note: string }> = [
|
||||||
{ id: "balanced", label: "Balanced", note: "a bit of everything" },
|
{ 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
|
// as a one-line summary so it's obvious whether AI actually contributed
|
||||||
// (issue #3). null until a run completes.
|
// (issue #3). null until a run completes.
|
||||||
const [aiCount, setAiCount] = useState<number | null>(null);
|
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.
|
// Hide names whose every checked extension is taken (issue #3). Persisted.
|
||||||
const [hideTaken, setHideTaken] = useState(false);
|
const [hideTaken, setHideTaken] = useState(false);
|
||||||
const { saved, savedNames, toggleSaved, removeSaved, recheckSaved, copySaved, rechecking, copied } =
|
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) => {
|
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) {
|
for (let i = 0; i < domains.length; i += BATCH) {
|
||||||
if (runId.current !== myRun) return; // superseded by a newer search
|
if (runId.current !== myRun) return; // superseded by a newer search
|
||||||
@@ -129,17 +132,25 @@ export default function Finder() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const run = useCallback(
|
const run = useCallback(
|
||||||
async (nextSeed: number) => {
|
async (nextSeed: number, append: boolean) => {
|
||||||
const myRun = ++runId.current;
|
// 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);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setChecks({});
|
if (!append) {
|
||||||
|
setChecks({});
|
||||||
|
setPending(new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
const shown = new Set(base.map((i) => i.name));
|
||||||
let list = generateNames({
|
let list = generateNames({
|
||||||
keywords: parsedKeywords,
|
keywords: parsedKeywords,
|
||||||
style,
|
style,
|
||||||
count: NAME_COUNT,
|
count: NAME_COUNT,
|
||||||
seed: nextSeed,
|
seed: nextSeed,
|
||||||
|
exclude: shown,
|
||||||
});
|
});
|
||||||
|
|
||||||
// AI names: the visitor's own key if they saved one and asked us to use
|
// 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 generateWithAi(ai, parsedKeywords, style, 16, brief)
|
||||||
: await generateWithPlatformAi(parsedKeywords, style, 16, brief);
|
: await generateWithPlatformAi(parsedKeywords, style, 16, brief);
|
||||||
if (runId.current !== myRun) return;
|
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
|
const extra: Idea[] = aiNames
|
||||||
.filter((n) => !seen.has(n))
|
.filter((n) => !seen.has(n))
|
||||||
.map((name) => ({ name, strategy: "ai" as const, score: scoreName(name, parsedKeywords) }));
|
.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;
|
gotAi = extra.length;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
@@ -168,18 +179,37 @@ export default function Finder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (runId.current !== myRun) return;
|
if (runId.current !== myRun) return;
|
||||||
setIdeas(list);
|
if (list.length === 0 && append) {
|
||||||
setAiCount(wantsOwnKey || wantsPlatform ? gotAi : null);
|
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);
|
setBusy(false);
|
||||||
await lookup(domainsFor(list, tlds), myRun);
|
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 onGenerate = () => {
|
||||||
const next = seed + 1;
|
const next = seed + 1;
|
||||||
setSeed(next);
|
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) => {
|
const toggleTld = (tld: string) => {
|
||||||
@@ -256,7 +286,7 @@ export default function Finder() {
|
|||||||
|
|
||||||
<div className="controls">
|
<div className="controls">
|
||||||
<button className="primary" onClick={onGenerate} disabled={busy || tlds.length === 0}>
|
<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>
|
</button>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
@@ -355,6 +385,10 @@ export default function Finder() {
|
|||||||
<>{ideas.length} names</>
|
<>{ideas.length} names</>
|
||||||
)}
|
)}
|
||||||
{hideTaken && hiddenCount > 0 ? <> · {hiddenCount} taken hidden</> : null}
|
{hideTaken && hiddenCount > 0 ? <> · {hiddenCount} taken hidden</> : null}
|
||||||
|
{" · "}
|
||||||
|
<button type="button" className="linkish" onClick={onStartOver}>
|
||||||
|
start over
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<label className="results-toggle">
|
<label className="results-toggle">
|
||||||
<input type="checkbox" checked={hideTaken} onChange={(e) => toggleHideTaken(e.target.checked)} />
|
<input type="checkbox" checked={hideTaken} onChange={(e) => toggleHideTaken(e.target.checked)} />
|
||||||
@@ -378,7 +412,7 @@ export default function Finder() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="results-more">
|
<div className="results-more">
|
||||||
<button className="primary" onClick={onGenerate} disabled={busy || tlds.length === 0}>
|
<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>
|
||||||
<button type="button" className="mini" onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}>
|
<button type="button" className="mini" onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}>
|
||||||
Back to top ↑
|
Back to top ↑
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ export interface GenerateOptions {
|
|||||||
count?: number;
|
count?: number;
|
||||||
/** Changing this reshuffles the whole list. Same seed ⇒ same output. */
|
/** Changing this reshuffles the whole list. Same seed ⇒ same output. */
|
||||||
seed?: number;
|
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.
|
* and only the top `count` survive.
|
||||||
*/
|
*/
|
||||||
export function generateNames(opts: GenerateOptions): Idea[] {
|
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 rng = makeRng(seed);
|
||||||
const { seeds, related } = expandKeywords(keywords);
|
const { seeds, related } = expandKeywords(keywords);
|
||||||
|
|
||||||
@@ -408,7 +411,7 @@ export function generateNames(opts: GenerateOptions): Idea[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ranked = [...pool.values()]
|
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));
|
.sort((a, b) => b.score - a.score || a.name.length - b.name.length || a.name.localeCompare(b.name));
|
||||||
|
|
||||||
return diversify(ranked, count);
|
return diversify(ranked, count);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// /changelog. Bump VERSION and prepend an entry when shipping a user-facing
|
// /changelog. Bump VERSION and prepend an entry when shipping a user-facing
|
||||||
// change. Same convention as rpo.
|
// change. Same convention as rpo.
|
||||||
|
|
||||||
export const VERSION = "0.5.1";
|
export const VERSION = "0.6.0";
|
||||||
|
|
||||||
export interface ChangelogEntry {
|
export interface ChangelogEntry {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -11,6 +11,14 @@ export interface ChangelogEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CHANGELOG: 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",
|
version: "0.5.1",
|
||||||
date: "2026-08-17",
|
date: "2026-08-17",
|
||||||
|
|||||||
Reference in New Issue
Block a user