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",
|
"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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user