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", "name": "names-poweredbyotm",
"version": "0.3.1", "version": "0.4.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --port 3000", "dev": "next dev --port 3000",

View File

@@ -292,6 +292,44 @@ h2 {
border-color: #4a3f1c; 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 ---------- */
.wishlist { .wishlist {

View File

@@ -53,6 +53,12 @@ export default function Finder() {
const [platformAi, setPlatformAi] = useState(false); const [platformAi, setPlatformAi] = useState(false);
const [usePlatformAi, setUsePlatformAi] = useState(true); const [usePlatformAi, setUsePlatformAi] = useState(true);
const [signinNote, setSigninNote] = useState<string | null>(null); 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 [saved, setSaved] = useState<SavedName[]>([]);
const [savedLoaded, setSavedLoaded] = useState(false); const [savedLoaded, setSavedLoaded] = useState(false);
const [rechecking, setRechecking] = useState(false); const [rechecking, setRechecking] = useState(false);
@@ -165,8 +171,22 @@ export default function Finder() {
// can't change underneath us mid-session. // can't change underneath us mid-session.
useEffect(() => { useEffect(() => {
setAi(loadAiSettings()); 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(() => { useEffect(() => {
let alive = true; let alive = true;
fetch("/api/auth/session", { cache: "no-store" }) 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. // here is never fatal — the built-in list is already computed.
const wantsOwnKey = useAi && ai; const wantsOwnKey = useAi && ai;
const wantsPlatform = !ai && platformAi && account && usePlatformAi; const wantsPlatform = !ai && platformAi && account && usePlatformAi;
let gotAi = 0;
if (wantsOwnKey || wantsPlatform) { if (wantsOwnKey || wantsPlatform) {
try { try {
const aiNames = wantsOwnKey const aiNames = wantsOwnKey
@@ -298,6 +319,7 @@ export default function Finder() {
.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].slice(0, NAME_COUNT + extra.length);
gotAi = extra.length;
} catch (err) { } catch (err) {
setError( setError(
`AI names unavailable (${err instanceof Error ? err.message : "unknown error"}). Showing built-in suggestions.`, `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; if (runId.current !== myRun) return;
setIdeas(list); setIdeas(list);
setAiCount(wantsOwnKey || wantsPlatform ? gotAi : null);
setBusy(false); setBusy(false);
await lookup(domainsFor(list, tlds), myRun); await lookup(domainsFor(list, tlds), myRun);
}, },
@@ -325,6 +348,18 @@ export default function Finder() {
const showsUnverifiable = tlds.some((t) => UNVERIFIABLE_TLDS.includes(t)); 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 ( return (
<> <>
<div className="field"> <div className="field">
@@ -489,20 +524,40 @@ export default function Finder() {
{busy ? "Working…" : "Describe the business above and press Find names."} {busy ? "Working…" : "Describe the business above and press Find names."}
</p> </p>
) : ( ) : (
<div className="results"> <>
{ideas.map((idea) => ( <div className="results-bar">
<NameCard <span className="results-summary">
key={idea.name} {aiCount != null ? (
idea={idea} <>
tlds={tlds} <span className="ai-badge">AI</span> {aiCount} {aiCount === 1 ? "name" : "names"} ·{" "}
checks={checks} {ideas.length - aiCount} built-in
pending={pending} </>
registrar={registrar} ) : (
saved={savedNames.has(idea.hackTld && idea.hackStem ? `${idea.hackStem}.${idea.hackTld}` : idea.name)} <>{ideas.length} names</>
onToggleSave={toggleSaved} )}
/> {hideTaken && hiddenCount > 0 ? <> · {hiddenCount} taken hidden</> : null}
))} </span>
</div> <label className="results-toggle">
<input type="checkbox" checked={hideTaken} onChange={(e) => toggleHideTaken(e.target.checked)} />
Hide taken
</label>
</div>
<div className="results">
{visibleIdeas.map((idea) => (
<NameCard
key={idea.name}
idea={idea}
tlds={tlds}
checks={checks}
pending={pending}
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, registrar,
saved, saved,
onToggleSave, onToggleSave,
hideTaken,
}: { }: {
idea: Idea; idea: Idea;
tlds: string[]; tlds: string[];
@@ -524,6 +580,7 @@ function NameCard({
registrar: string; registrar: string;
saved: boolean; saved: boolean;
onToggleSave: (name: string, domains: string[]) => void; onToggleSave: (name: string, domains: string[]) => void;
hideTaken: boolean;
}) { }) {
const isHack = Boolean(idea.hackTld && idea.hackStem); const isHack = Boolean(idea.hackTld && idea.hackStem);
const domains = isHack ? [`${idea.hackStem}.${idea.hackTld}`] : tlds.map((t) => `${idea.name}.${t}`); 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 isPending = pending.has(domain) && !check;
const status: Availability | "checking" = isPending ? "checking" : (check?.status ?? "unknown"); const status: Availability | "checking" = isPending ? "checking" : (check?.status ?? "unknown");
const free = status === "available" || status === "unverified-available"; const free = status === "available" || status === "unverified-available";
if (hideTaken && status === "taken") return null;
return ( return (
<div className="card-meta" key={domain}> <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> </div>
); );
} }
@@ -687,7 +747,7 @@ function strategyLabel(strategy: Idea["strategy"]): string {
case "hack": case "hack":
return "domain hack"; return "domain hack";
case "ai": case "ai":
return "your AI key"; return "generated";
default: default:
return ""; return "";
} }

View File

@@ -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.3.1"; export const VERSION = "0.4.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.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", version: "0.3.1",
date: "2026-08-16", date: "2026-08-16",