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
co-authored by Claude Fable 5
parent cce9ee3c7c
commit ff0a262c43
5 changed files with 68 additions and 17 deletions
+47 -13
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);
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 ↑
+5 -2
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);
+9 -1
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",