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