feat: hide-taken toggle + visible AI contribution (#3, v0.4.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-16 22:10:48 -05:00
parent 0088b7f8ac
commit d7e3a4cd8e
4 changed files with 124 additions and 18 deletions

View File

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

View File

@@ -292,6 +292,44 @@ h2 {
border-color: #4a3f1c;
}
.results-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-top: 16px;
font-size: 13px;
color: var(--muted);
}
.results-bar + .results {
margin-top: 8px;
}
.results-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.results-toggle input {
width: auto;
}
.ai-badge {
display: inline-block;
padding: 1px 6px;
border-radius: 999px;
background: #1f2a3f;
color: var(--accent);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
vertical-align: middle;
}
/* ---------- wishlist ---------- */
.wishlist {

View File

@@ -53,6 +53,12 @@ export default function Finder() {
const [platformAi, setPlatformAi] = useState(false);
const [usePlatformAi, setUsePlatformAi] = useState(true);
const [signinNote, setSigninNote] = useState<string | null>(null);
// How many names the last run got from AI vs the built-in generator — shown
// 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);
// Hide names whose every checked extension is taken (issue #3). Persisted.
const [hideTaken, setHideTaken] = useState(false);
const [saved, setSaved] = useState<SavedName[]>([]);
const [savedLoaded, setSavedLoaded] = useState(false);
const [rechecking, setRechecking] = useState(false);
@@ -165,8 +171,22 @@ export default function Finder() {
// can't change underneath us mid-session.
useEffect(() => {
setAi(loadAiSettings());
try {
setHideTaken(window.localStorage.getItem("names.hideTaken") === "1");
} catch {
/* private mode */
}
}, []);
const toggleHideTaken = (v: boolean) => {
setHideTaken(v);
try {
window.localStorage.setItem("names.hideTaken", v ? "1" : "0");
} catch {
/* private mode */
}
};
useEffect(() => {
let alive = true;
fetch("/api/auth/session", { cache: "no-store" })
@@ -287,6 +307,7 @@ export default function Finder() {
// here is never fatal — the built-in list is already computed.
const wantsOwnKey = useAi && ai;
const wantsPlatform = !ai && platformAi && account && usePlatformAi;
let gotAi = 0;
if (wantsOwnKey || wantsPlatform) {
try {
const aiNames = wantsOwnKey
@@ -298,6 +319,7 @@ export default function Finder() {
.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);
gotAi = extra.length;
} catch (err) {
setError(
`AI names unavailable (${err instanceof Error ? err.message : "unknown error"}). Showing built-in suggestions.`,
@@ -307,6 +329,7 @@ export default function Finder() {
if (runId.current !== myRun) return;
setIdeas(list);
setAiCount(wantsOwnKey || wantsPlatform ? gotAi : null);
setBusy(false);
await lookup(domainsFor(list, tlds), myRun);
},
@@ -325,6 +348,18 @@ export default function Finder() {
const showsUnverifiable = tlds.some((t) => UNVERIFIABLE_TLDS.includes(t));
/** A name is "all taken" once every domain we checked for it resolved taken.
* Still-checking or unknown names stay visible — hiding is for certainty. */
const isAllTaken = useCallback(
(idea: Idea): boolean => {
const ds = domainsFor([idea], tlds);
return ds.length > 0 && ds.every((d) => checks[d]?.status === "taken");
},
[checks, domainsFor, tlds],
);
const visibleIdeas = hideTaken ? ideas.filter((i) => !isAllTaken(i)) : ideas;
const hiddenCount = ideas.length - visibleIdeas.length;
return (
<>
<div className="field">
@@ -489,8 +524,26 @@ export default function Finder() {
{busy ? "Working…" : "Describe the business above and press Find names."}
</p>
) : (
<>
<div className="results-bar">
<span className="results-summary">
{aiCount != null ? (
<>
<span className="ai-badge">AI</span> {aiCount} {aiCount === 1 ? "name" : "names"} ·{" "}
{ideas.length - aiCount} built-in
</>
) : (
<>{ideas.length} names</>
)}
{hideTaken && hiddenCount > 0 ? <> · {hiddenCount} taken hidden</> : null}
</span>
<label className="results-toggle">
<input type="checkbox" checked={hideTaken} onChange={(e) => toggleHideTaken(e.target.checked)} />
Hide taken
</label>
</div>
<div className="results">
{ideas.map((idea) => (
{visibleIdeas.map((idea) => (
<NameCard
key={idea.name}
idea={idea}
@@ -500,9 +553,11 @@ export default function Finder() {
registrar={registrar}
saved={savedNames.has(idea.hackTld && idea.hackStem ? `${idea.hackStem}.${idea.hackTld}` : idea.name)}
onToggleSave={toggleSaved}
hideTaken={hideTaken}
/>
))}
</div>
</>
)}
</>
);
@@ -516,6 +571,7 @@ function NameCard({
registrar,
saved,
onToggleSave,
hideTaken,
}: {
idea: Idea;
tlds: string[];
@@ -524,6 +580,7 @@ function NameCard({
registrar: string;
saved: boolean;
onToggleSave: (name: string, domains: string[]) => void;
hideTaken: boolean;
}) {
const isHack = Boolean(idea.hackTld && idea.hackStem);
const domains = isHack ? [`${idea.hackStem}.${idea.hackTld}`] : tlds.map((t) => `${idea.name}.${t}`);
@@ -563,6 +620,7 @@ function NameCard({
const isPending = pending.has(domain) && !check;
const status: Availability | "checking" = isPending ? "checking" : (check?.status ?? "unknown");
const free = status === "available" || status === "unverified-available";
if (hideTaken && status === "taken") return null;
return (
<div className="card-meta" key={domain}>
@@ -584,7 +642,9 @@ function NameCard({
);
})}
<span className="strategy">{strategyLabel(idea.strategy)}</span>
<span className="strategy">
{idea.strategy === "ai" ? <span className="ai-badge">AI</span> : null} {strategyLabel(idea.strategy)}
</span>
</div>
);
}
@@ -687,7 +747,7 @@ function strategyLabel(strategy: Idea["strategy"]): string {
case "hack":
return "domain hack";
case "ai":
return "your AI key";
return "generated";
default:
return "";
}

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.3.1";
export const VERSION = "0.4.0";
export interface ChangelogEntry {
version: string;
@@ -11,6 +11,14 @@ export interface ChangelogEntry {
}
export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.4.0",
date: "2026-08-16",
changes: [
"Hide taken: a toggle above the results hides names whose every checked extension is taken, and taken rows inside mixed cards. Remembered between visits.",
"You can now see what AI contributed: AI-generated cards carry an AI badge and a summary line above the results reads e.g. 'AI 9 names · 24 built-in' (or 'AI 0 names' when the AI call failed — the error explains why).",
],
},
{
version: "0.3.1",
date: "2026-08-16",