@@ -36,7 +36,7 @@ const DEFAULT_PDF_URL = "https://cdn.ooxml.dev/ecma-376/part1.pdf";
3636
3737export 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