Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 53 additions & 5 deletions src/components/SearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ import {
} from "@/lib/search.shared";
import { createStaticSearchClient, preloadStaticSearch } from "@/lib/static-search-client";
import { closeSuperchat, isSuperchatOpen, openSuperchat } from "@/lib/superchat";
import { cn } from "@/lib/cn";
import { buildDocsApiPath } from "@/lib/url-base";

const SEARCH_DELAY_MS = 100;

function scheduleIdle(callback: () => void) {
if (typeof window === "undefined") return () => undefined;

Expand All @@ -41,6 +44,8 @@ function scheduleIdle(callback: () => void) {

export function CustomSearchDialog(props: SharedProps) {
const [scope, setScope] = useState<SearchScope | undefined>();
const [debouncedSearch, setDebouncedSearch] = useState("");
const [isAskAIHintActive, setIsAskAIHintActive] = useState(false);
const tags = getSearchGroupsForScope(scope);
const client =
process.env.NODE_ENV === "production"
Expand All @@ -54,14 +59,48 @@ export function CustomSearchDialog(props: SharedProps) {
});
const { search, setSearch, query } = useDocsSearch({
client,
delayMs: 100,
delayMs: SEARCH_DELAY_MS,
});
const isSearchSettled = search.trim() === debouncedSearch.trim();
const hasConfirmedNoResults =
debouncedSearch.trim().length > 0 &&
isSearchSettled &&
!query.isLoading &&
!query.error &&
Array.isArray(query.data) &&
query.data.length === 0;
const hasConfirmedResults =
debouncedSearch.trim().length > 0 &&
isSearchSettled &&
!query.isLoading &&
!query.error &&
Array.isArray(query.data) &&
query.data.length > 0;
const launchSuperchat = useEffectEvent((message = search) => {
const seededMessage = message.trim();
if (!openSuperchat(seededMessage)) return;
props.onOpenChange(false);
});

useEffect(() => {
const handle = window.setTimeout(() => {
setDebouncedSearch(search);
}, SEARCH_DELAY_MS);

return () => window.clearTimeout(handle);
}, [search]);

useEffect(() => {
if (hasConfirmedNoResults) {
setIsAskAIHintActive(true);
return;
}

if (debouncedSearch.trim().length === 0 || query.error || hasConfirmedResults) {
setIsAskAIHintActive(false);
}
}, [debouncedSearch, hasConfirmedNoResults, hasConfirmedResults, query.error]);

useEffect(() => {
if (process.env.NODE_ENV !== "production") return;
const saveData = Boolean(
Expand Down Expand Up @@ -113,13 +152,22 @@ export function CustomSearchDialog(props: SharedProps) {
<button
type="button"
onClick={() => launchSuperchat()}
className="group relative inline-flex items-center gap-2 rounded-full border border-[#76f4f226] bg-[#111828] px-3 py-1.5 text-sm font-medium text-[#76f4f2] shadow-[0_16px_28px_-16px_rgba(0,0,0,0.3),0_0_0_1px_rgba(118,244,242,0.08)] transition-[transform,box-shadow] duration-200 ease-[cubic-bezier(.16,1,.3,1)] hover:scale-[1.02] hover:shadow-[0_20px_30px_-14px_rgba(0,0,0,0.3),0_0_20px_-4px_rgba(118,244,242,0.25),0_0_0_1px_rgba(118,244,242,0.15)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#76f4f240] focus-visible:ring-offset-2 focus-visible:ring-offset-fd-background active:scale-[0.98]"
data-ai-hint={isAskAIHintActive ? "true" : "false"}
className={cn(
"ask-ai-button group relative inline-flex items-center gap-2 rounded-full border border-[#76f4f226] bg-[#111828] px-3 py-1.5 text-sm font-medium text-[#76f4f2] shadow-[0_16px_28px_-16px_rgba(0,0,0,0.3),0_0_0_1px_rgba(118,244,242,0.08)] transition-[transform,box-shadow,border-color] duration-200 ease-[cubic-bezier(.16,1,.3,1)] hover:scale-[1.02] hover:shadow-[0_20px_30px_-14px_rgba(0,0,0,0.3),0_0_20px_-4px_rgba(118,244,242,0.25),0_0_0_1px_rgba(118,244,242,0.15)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#76f4f240] focus-visible:ring-offset-2 focus-visible:ring-offset-fd-background active:scale-[0.98]",
isAskAIHintActive &&
"border-[#76f4f24d] shadow-[0_20px_34px_-16px_rgba(0,0,0,0.34),0_0_26px_-6px_rgba(118,244,242,0.22),0_0_0_1px_rgba(118,244,242,0.16)]",
)}
title="Ask AI (Cmd/Ctrl+I)"
aria-label="Ask AI"
>
<MessageCircle className="size-4" strokeWidth={1.8} />
<span>Ask AI</span>
<span className="pointer-events-none absolute left-1/2 top-full z-10 mt-2 inline-flex -translate-x-1/2 translate-y-1 items-center gap-1 opacity-0 transition-all duration-150 group-hover:-translate-x-1/2 group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:-translate-x-1/2 group-focus-visible:translate-y-0 group-focus-visible:opacity-100">
<span
aria-hidden="true"
className="ask-ai-button-shimmer pointer-events-none absolute inset-0 rounded-full overflow-hidden"
/>
<MessageCircle className="relative z-10 size-4" strokeWidth={1.8} />
<span className="relative z-10">Ask AI</span>
<span className="pointer-events-none absolute left-1/2 top-full z-20 mt-2 inline-flex -translate-x-1/2 translate-y-1 items-center gap-1 opacity-0 transition-all duration-150 group-hover:-translate-x-1/2 group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:-translate-x-1/2 group-focus-visible:translate-y-0 group-focus-visible:opacity-100">
<kbd className="inline-flex min-w-5 items-center justify-center rounded border border-fd-border/80 bg-fd-secondary px-1.5 py-0.5 font-mono text-[11px] text-fd-muted-foreground shadow-sm">
</kbd>
Expand Down
42 changes: 42 additions & 0 deletions src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,48 @@
display: none;
}

@keyframes ask-ai-empty-shimmer {
0% {
transform: translateX(-140%);
}
100% {
transform: translateX(140%);
}
}

.ask-ai-button-shimmer::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
pointer-events: none;
background:
linear-gradient(
112deg,
transparent 0%,
transparent 38%,
rgba(118, 244, 242, 0.04) 46%,
rgba(118, 244, 242, 0.22) 50%,
rgba(118, 244, 242, 0.04) 54%,
transparent 62%,
transparent 100%
);
transform: translateX(-140%);
opacity: 0;
transition: opacity 220ms ease;
}

.ask-ai-button[data-ai-hint="true"] .ask-ai-button-shimmer::after {
opacity: 1;
animation: ask-ai-empty-shimmer 1.85s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
.ask-ai-button[data-ai-hint="true"] .ask-ai-button-shimmer::after {
animation: none;
}
}

/* Fix mobile dropdown icon sizing - make SVGs fill their container.
The popover renders via a Radix portal at <body>, not inside #nd-sidebar,
so we target both the sidebar trigger and the portal popover separately.
Expand Down
Loading