Skip to content

Commit 3f51fad

Browse files
committed
fix: add intentional search
1 parent 01975a3 commit 3f51fad

3 files changed

Lines changed: 67 additions & 66 deletions

File tree

apps/web/src/components/Navbar.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,17 @@ export function Navbar({ sticky = false, maxWidth = false, onSearchClick }: Navb
3434
<NavLink to="/docs" active={isDocsActive}>
3535
Reference
3636
</NavLink>
37-
<NavLink to="/spec" active={isSpecActive}>
38-
Spec
39-
</NavLink>
4037
<div className="flex items-center gap-1">
41-
<NavLink to="/mcp" active={isMcpActive}>
42-
MCP
38+
<NavLink to="/spec" active={isSpecActive}>
39+
Spec
4340
</NavLink>
4441
<span className="rounded bg-[var(--color-accent)]/15 px-1 py-0.5 text-[8px] font-medium text-[var(--color-accent)]">
4542
new
4643
</span>
4744
</div>
45+
<NavLink to="/mcp" active={isMcpActive}>
46+
MCP
47+
</NavLink>
4848
<div className="flex items-center gap-1">
4949
<NavLink to="#" active={false} disabled>
5050
Playground
@@ -84,17 +84,17 @@ export function Navbar({ sticky = false, maxWidth = false, onSearchClick }: Navb
8484
<NavLink to="/docs" active={isDocsActive} onClick={() => setMobileMenuOpen(false)}>
8585
Reference
8686
</NavLink>
87-
<NavLink to="/spec" active={isSpecActive} onClick={() => setMobileMenuOpen(false)}>
88-
Spec
89-
</NavLink>
9087
<div className="flex items-center gap-1">
91-
<NavLink to="/mcp" active={isMcpActive} onClick={() => setMobileMenuOpen(false)}>
92-
MCP
88+
<NavLink to="/spec" active={isSpecActive} onClick={() => setMobileMenuOpen(false)}>
89+
Spec
9390
</NavLink>
9491
<span className="rounded bg-[var(--color-accent)]/15 px-1 py-0.5 text-[8px] font-medium text-[var(--color-accent)]">
9592
new
9693
</span>
9794
</div>
95+
<NavLink to="/mcp" active={isMcpActive} onClick={() => setMobileMenuOpen(false)}>
96+
MCP
97+
</NavLink>
9898
<div className="flex items-center gap-1">
9999
<NavLink to="#" active={false} disabled>
100100
Playground

apps/web/src/pages/Home.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,19 @@ export function Home() {
2626
</Link>
2727
</div>
2828

29-
{/* MCP Callout */}
29+
{/* Spec Explorer Callout */}
3030
<div className="flex items-center justify-center gap-2 text-sm">
3131
<span className="bg-[var(--color-accent)]/10 text-[var(--color-accent)] text-[10px] font-medium px-1.5 py-0.5 rounded">
3232
NEW
3333
</span>
34-
<span className="text-[var(--color-text-secondary)]">Search the spec via MCP</span>
34+
<span className="text-[var(--color-text-secondary)]">
35+
AI-powered spec search with PDF viewer
36+
</span>
3537
<Link
36-
to="/mcp"
38+
to="/spec"
3739
className="text-[var(--color-accent)] hover:text-[var(--color-accent-hover)] font-medium text-xs"
3840
>
39-
Learn more
41+
Try it
4042
</Link>
4143
</div>
4244
</main>

apps/web/src/pages/SpecExplorer.tsx

Lines changed: 51 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const DEFAULT_PDF_URL = "https://cdn.ooxml.dev/ecma-376/part1.pdf";
3636

3737
export function SpecExplorer() {
3838
const [search, setSearch] = useState("");
39-
const [debouncedSearch, setDebouncedSearch] = useState("");
39+
const [submittedSearch, setSubmittedSearch] = useState("");
4040
const [results, setResults] = useState<SpecSearchResult[]>([]);
4141
const [isLoading, setIsLoading] = useState(false);
4242
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -56,60 +56,54 @@ export function SpecExplorer() {
5656
};
5757
}, []);
5858

59-
// Debounce search input
60-
useEffect(() => {
61-
const timer = setTimeout(() => {
62-
setDebouncedSearch(search);
63-
}, 300);
64-
return () => clearTimeout(timer);
65-
}, [search]);
59+
// Submit search
60+
const handleSubmit = useCallback(async () => {
61+
const query = search.trim();
62+
if (!query || query === submittedSearch) return;
6663

67-
// Search when debounced value changes
68-
useEffect(() => {
69-
if (!debouncedSearch.trim()) {
70-
setResults([]);
71-
return;
72-
}
64+
setSubmittedSearch(query);
65+
setIsLoading(true);
7366

74-
const doSearch = async () => {
75-
setIsLoading(true);
76-
try {
77-
const res = await fetch(`${import.meta.env.VITE_API_URL}/search`, {
78-
method: "POST",
79-
headers: { "Content-Type": "application/json" },
80-
body: JSON.stringify({ query: debouncedSearch, limit: 10 }),
81-
});
82-
const data: MCPSearchResponse = await res.json();
67+
try {
68+
const res = await fetch(`${import.meta.env.VITE_API_URL}/search`, {
69+
method: "POST",
70+
headers: { "Content-Type": "application/json" },
71+
body: JSON.stringify({ query, limit: 10 }),
72+
});
73+
const data: MCPSearchResponse = await res.json();
8374

84-
const transformed: SpecSearchResult[] = data.results.map((r) => ({
85-
id: `spec-${r.id}`,
86-
sectionId: r.sectionId || "",
87-
title: r.title || r.content.slice(0, 60),
88-
description: r.title ? r.content.slice(0, 120) : undefined,
89-
partNumber: r.partNumber,
90-
pageNumber: r.pageNumber,
91-
pdfUrl: null,
92-
}));
75+
const transformed: SpecSearchResult[] = data.results.map((r) => ({
76+
id: `spec-${r.id}`,
77+
sectionId: r.sectionId || "",
78+
title: r.title || r.content.slice(0, 60),
79+
description: r.title ? r.content.slice(0, 120) : undefined,
80+
partNumber: r.partNumber,
81+
pageNumber: r.pageNumber,
82+
}));
9383

94-
setResults(transformed);
95-
setSelectedIndex(0);
96-
} catch (err) {
97-
console.error("Search failed:", err);
98-
setResults([]);
99-
} finally {
100-
setIsLoading(false);
84+
setResults(transformed);
85+
setSelectedIndex(0);
86+
if (transformed.length > 0) {
87+
setSelectedResult(transformed[0]);
10188
}
102-
};
103-
104-
doSearch();
105-
}, [debouncedSearch]);
106-
107-
// Select first result when results change
108-
useEffect(() => {
109-
if (results.length > 0 && !selectedResult) {
110-
setSelectedResult(results[0]);
89+
} catch (err) {
90+
console.error("Search failed:", err);
91+
setResults([]);
92+
} finally {
93+
setIsLoading(false);
11194
}
112-
}, [results, selectedResult]);
95+
}, [search, submittedSearch]);
96+
97+
// Handle Enter key to submit
98+
const handleKeyDown = useCallback(
99+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
100+
if (e.key === "Enter" && !e.shiftKey) {
101+
e.preventDefault();
102+
handleSubmit();
103+
}
104+
},
105+
[handleSubmit],
106+
);
113107

114108
// Scroll selected item into view
115109
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally scroll when selectedIndex changes
@@ -172,13 +166,18 @@ export function SpecExplorer() {
172166
ref={inputRef}
173167
value={search}
174168
onChange={(e) => setSearch(e.target.value)}
169+
onKeyDown={handleKeyDown}
175170
placeholder="Ask about the ECMA-376 spec..."
176171
rows={2}
177172
className="w-full resize-none rounded-lg border border-[var(--color-border)] bg-transparent px-4 py-3 text-sm leading-relaxed outline-none transition placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-text-primary)] focus:shadow-[0_0_0_3px_rgba(0,0,0,0.05)]"
178173
autoFocus
179174
/>
180175
<div className="mt-2 text-[11px] text-[var(--color-text-muted)]">
181-
Try natural language like "how to set paragraph margins"
176+
Press{" "}
177+
<kbd className="rounded border border-[var(--color-border)] bg-[var(--color-bg-secondary)] px-1">
178+
Enter
179+
</kbd>{" "}
180+
to search
182181
</div>
183182
</div>
184183

@@ -199,14 +198,14 @@ export function SpecExplorer() {
199198
)}
200199

201200
{/* Empty state */}
202-
{!isLoading && !search && (
201+
{!isLoading && !submittedSearch && (
203202
<div className="px-5 py-12 text-center text-sm text-[var(--color-text-muted)]">
204203
Search the ECMA-376 specification
205204
</div>
206205
)}
207206

208207
{/* No results */}
209-
{!isLoading && search && results.length === 0 && debouncedSearch === search && (
208+
{!isLoading && submittedSearch && results.length === 0 && (
210209
<div className="px-5 py-12 text-center text-sm text-[var(--color-text-muted)]">
211210
No results found
212211
</div>

0 commit comments

Comments
 (0)