11"use client"
22
3- import React , { FC , useState , useEffect } from "react" ;
3+ import React , { FC , useState , useEffect , useCallback } from "react" ;
44import {
55 Sparkles ,
66 Plus ,
77 Edit ,
88 ThumbsUp ,
99 Settings ,
1010 Minus ,
11- Check
11+ Check ,
12+ Search
1213} from "lucide-react" ;
1314import { FaArrowLeft } from "react-icons/fa" ;
15+ import { API_ENDPOINTS } from "../lib/config" ;
16+ import debounce from 'lodash/debounce' ;
1417
1518interface AIModel {
1619 id : string ;
1720 name : string ;
1821}
1922
23+ interface TopicSuggestion {
24+ name : string ;
25+ count : number ;
26+ }
27+
2028const AI_MODELS : AIModel [ ] = [
2129 {
2230 id : 'gpt-3.5-turbo' ,
@@ -61,6 +69,11 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
6169 const [ selectedModel , setSelectedModel ] = useState ( 'gpt-4' ) ;
6270 const [ apiKey , setApiKey ] = useState ( '' ) ;
6371 const [ finalizedTopics , setFinalizedTopics ] = useState < string [ ] > ( [ ] ) ;
72+ const [ showSuggestions , setShowSuggestions ] = useState ( false ) ;
73+ const [ suggestions , setSuggestions ] = useState < TopicSuggestion [ ] > ( [ ] ) ;
74+ const [ isLoadingSuggestions , setIsLoadingSuggestions ] = useState ( false ) ;
75+ const [ inputValue , setInputValue ] = useState ( "" ) ;
76+ const [ canAddTopic , setCanAddTopic ] = useState ( false ) ;
6477
6578 const moveToRightColumn = ( topic : string ) => {
6679 setFinalizedTopics ( prev => [ ...prev , topic ] ) ;
@@ -100,20 +113,76 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
100113 } ;
101114
102115 const handleAddNewTopic = ( ) => {
103- if ( ! newTopic . trim ( ) ) return ; // Don't add empty topics
116+ if ( ! inputValue . trim ( ) || ! canAddTopic ) return ; // Don't add if empty or not a valid topic
104117
118+ const trimmedValue = inputValue . trim ( ) ;
105119 // Add to selected topics if not already present
106- if ( ! selectedTopics . includes ( newTopic . trim ( ) ) ) {
107- setSelectedTopics ( [ ...selectedTopics , newTopic . trim ( ) ] ) ;
120+ if ( ! selectedTopics . includes ( trimmedValue ) ) {
121+ setSelectedTopics ( [ ...selectedTopics , trimmedValue ] ) ;
108122 }
109123
110124 // Add to finalized topics if not already present
111- if ( ! finalizedTopics . includes ( newTopic . trim ( ) ) ) {
112- setFinalizedTopics ( prev => [ ...prev , newTopic . trim ( ) ] ) ;
125+ if ( ! finalizedTopics . includes ( trimmedValue ) ) {
126+ setFinalizedTopics ( [ ...finalizedTopics , trimmedValue ] ) ;
113127 }
114128
115129 // Clear the input
116- setNewTopic ( "" ) ;
130+ setInputValue ( "" ) ;
131+ setCanAddTopic ( false ) ;
132+ } ;
133+
134+ // Update canAddTopic when input or suggestions change
135+ useEffect ( ( ) => {
136+ const trimmedInput = inputValue . trim ( ) . toLowerCase ( ) ;
137+ const isValidTopic = suggestions . some ( suggestion =>
138+ suggestion . name . toLowerCase ( ) === trimmedInput
139+ ) ;
140+ setCanAddTopic ( isValidTopic ) ;
141+ } , [ inputValue , suggestions ] ) ;
142+
143+ // Debounced function to fetch suggestions
144+ const fetchSuggestions = useCallback (
145+ debounce ( async ( query : string ) => {
146+ if ( ! query . trim ( ) ) {
147+ setSuggestions ( [ ] ) ;
148+ return ;
149+ }
150+
151+ setIsLoadingSuggestions ( true ) ;
152+ try {
153+ const response = await fetch ( `${ API_ENDPOINTS . SUGGEST_TOPICS } ?query=${ encodeURIComponent ( query ) } ` ) ;
154+ const data = await response . json ( ) ;
155+ if ( data . success ) {
156+ setSuggestions ( data . suggestions ) ;
157+ } else {
158+ throw new Error ( data . message || 'Failed to get suggestions' ) ;
159+ }
160+ } catch ( error ) {
161+ console . error ( 'Error fetching suggestions:' , error ) ;
162+ setSuggestions ( [ ] ) ;
163+ } finally {
164+ setIsLoadingSuggestions ( false ) ;
165+ }
166+ } , 300 ) ,
167+ [ ]
168+ ) ;
169+
170+ // Update suggestions when inputValue changes
171+ useEffect ( ( ) => {
172+ fetchSuggestions ( inputValue ) ;
173+ } , [ inputValue , fetchSuggestions ] ) ;
174+
175+ const handleSuggestionClick = ( suggestion : TopicSuggestion ) => {
176+ setInputValue ( suggestion . name ) ;
177+ setCanAddTopic ( true ) ;
178+ setShowSuggestions ( false ) ;
179+ // Remove auto-adding to finalized and selected topics
180+ // if (!finalizedTopics.includes(suggestion.name)) {
181+ // setFinalizedTopics([...finalizedTopics, suggestion.name]);
182+ // }
183+ // if (!selectedTopics.includes(suggestion.name)) {
184+ // setSelectedTopics([...selectedTopics, suggestion.name]);
185+ // }
117186 } ;
118187
119188 return (
@@ -178,8 +247,8 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
178247 < div className = "row g-4" >
179248 { /* Left column - Available Topics */ }
180249 < div className = "col-md-6" >
181- < div className = "card h-100" style = { { minHeight : 420 } } >
182- < div className = "card-body" style = { { minHeight : 420 , maxHeight : 420 } } >
250+ < div className = "card h-100" style = { { minHeight : 600 } } >
251+ < div className = "card-body" style = { { minHeight : 600 , maxHeight : 600 } } >
183252 < div className = "d-flex justify-content-between align-items-center mb-4" >
184253 < h3 className = "h5 mb-0 d-flex align-items-center" >
185254 < Sparkles className = "text-warning me-2" size = { 20 } />
@@ -200,7 +269,7 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
200269 </ div >
201270 </ div >
202271 < div className = "d-flex flex-column h-100" style = { { minHeight : 250 , justifyContent : 'flex-start' } } >
203- < div className = "list-group w-100 mb-0" style = { { flex : 1 , overflowY : 'auto' , maxHeight : 300 , marginBottom : 0 , paddingBottom : 0 } } >
272+ < div className = "list-group w-100 mb-0" style = { { flex : 1 , overflowY : 'auto' , maxHeight : 480 , marginBottom : 0 , paddingBottom : 0 } } >
204273 { selectedTopics . map ( ( topic ) => {
205274 const isAI = llmSuggestions . includes ( topic ) ;
206275 const isAdded = finalizedTopics . includes ( topic ) ;
@@ -240,52 +309,150 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
240309
241310 { /* Right column - Finalized Topics */ }
242311 < div className = "col-md-6" >
243- < div className = "card h-100" style = { { minHeight : 420 } } >
244- < div className = "card-body" style = { { minHeight : 420 , maxHeight : 420 } } >
312+ < div className = "card h-100" style = { { minHeight : 600 } } >
313+ < div className = "card-body" style = { { minHeight : 600 , maxHeight : 600 } } >
245314 < h3 className = "h5 mb-4 d-flex align-items-center" >
246315 < Edit size = { 20 } className = "text-primary me-2" />
247316 Finalized Topics
248317 < span className = "badge bg-secondary ms-2" style = { { fontSize : '0.9rem' } } >
249318 { finalizedTopics . length } topics
250319 </ span >
251320 </ h3 >
252- < div className = "mb-4" >
321+ < p className = "text-muted mb-3" style = { { fontSize : '0.85rem' } } >
322+ Search and add topics based on your expertise, only existing GitHub topics are allowed.
323+ </ p >
324+ < div className = "mb-4 position-relative" >
253325 < div className = "input-group" >
326+ < span className = "input-group-text border-end-0 bg-transparent" >
327+ < Search size = { 16 } className = "text-muted" />
328+ </ span >
254329 < input
255330 type = "text"
256- className = " form-control"
331+ className = { ` form-control border-start-0 ${ ! canAddTopic && inputValue . trim ( ) ? 'is-invalid' : '' } ` }
257332 placeholder = "Add a custom topic"
258- value = { newTopic }
259- onChange = { ( e ) => setNewTopic ( e . target . value ) }
333+ value = { inputValue }
334+ onChange = { ( e ) => {
335+ const value = e . target . value . replace ( / \s + / g, '-' ) ;
336+ setInputValue ( value ) ;
337+ setShowSuggestions ( true ) ;
338+ } }
339+ onFocus = { ( ) => setShowSuggestions ( true ) }
340+ onBlur = { ( ) => setTimeout ( ( ) => setShowSuggestions ( false ) , 200 ) }
260341 onKeyDown = { ( e ) => {
261- if ( e . key === "Enter" && newTopic . trim ( ) ) {
342+ if ( e . key === "Enter" && inputValue . trim ( ) && canAddTopic ) {
343+ e . preventDefault ( ) ;
262344 handleAddNewTopic ( ) ;
263345 }
264346 } }
265347 />
266348 < button
267349 className = "btn btn-primary"
268350 onClick = { handleAddNewTopic }
269- disabled = { ! newTopic . trim ( ) }
351+ disabled = { ! inputValue . trim ( ) || ! canAddTopic }
352+ title = { ! canAddTopic && inputValue . trim ( ) ? "Topic must be selected from suggestions" : "Add topic" }
270353 >
271354 < Plus size = { 16 } />
272355 </ button >
273356 </ div >
357+ { ! canAddTopic && inputValue . trim ( ) && (
358+ < div className = "invalid-feedback d-block mt-1" >
359+ Please select a topic from the suggestions list
360+ </ div >
361+ ) }
362+
363+ { /* Suggestions dropdown */ }
364+ { showSuggestions && ( inputValue . trim ( ) || isLoadingSuggestions ) && (
365+ < div
366+ className = "position-absolute bg-white rounded-3 shadow-lg w-100"
367+ style = { {
368+ top : "calc(100% + 4px)" ,
369+ left : 0 ,
370+ zIndex : 1000 ,
371+ maxHeight : "200px" ,
372+ overflowY : "auto" ,
373+ border : "1px solid rgba(0,0,0,0.08)" ,
374+ backdropFilter : "blur(8px)" ,
375+ backgroundColor : "rgba(255, 255, 255, 0.98)" ,
376+ } }
377+ >
378+ { isLoadingSuggestions ? (
379+ < div className = "p-3 text-center" >
380+ < div className = "spinner-border spinner-border-sm text-primary me-2" role = "status" >
381+ < span className = "visually-hidden" > Loading...</ span >
382+ </ div >
383+ < span className = "text-muted" > Finding relevant topics...</ span >
384+ </ div >
385+ ) : suggestions . length > 0 ? (
386+ < div className = "list-group list-group-flush" >
387+ { suggestions . map ( ( suggestion , index ) => (
388+ < button
389+ key = { suggestion . name }
390+ className = "list-group-item list-group-item-action d-flex justify-content-between align-items-center py-2 px-3"
391+ onClick = { ( ) => handleSuggestionClick ( suggestion ) }
392+ style = { {
393+ border : "none" ,
394+ cursor : "pointer" ,
395+ transition : "all 0.2s ease" ,
396+ backgroundColor : "transparent" ,
397+ borderBottom : index !== suggestions . length - 1 ? "1px solid rgba(0,0,0,0.05)" : "none" ,
398+ } }
399+ onMouseEnter = { ( e ) => {
400+ e . currentTarget . style . backgroundColor = "rgba(13, 110, 253, 0.05)" ;
401+ } }
402+ onMouseLeave = { ( e ) => {
403+ e . currentTarget . style . backgroundColor = "transparent" ;
404+ } }
405+ >
406+ < div className = "d-flex align-items-center" >
407+ < span className = "me-2" style = { { fontSize : "0.95rem" } } > { suggestion . name } </ span >
408+ { index === 0 && suggestion . name . toLowerCase ( ) === inputValue . toLowerCase ( ) && (
409+ < span className = "badge bg-success rounded-pill" style = { { fontSize : "0.7rem" } } >
410+ Selected
411+ </ span >
412+ ) }
413+ </ div >
414+ < div className = "d-flex align-items-center" >
415+ < span
416+ className = "badge rounded-pill px-2 py-1"
417+ style = { {
418+ backgroundColor : "rgba(108, 117, 125, 0.1)" ,
419+ color : "#495057" ,
420+ fontSize : "0.85rem" ,
421+ fontWeight : "500"
422+ } }
423+ >
424+ { suggestion . count . toLocaleString ( ) } repos
425+ </ span >
426+ </ div >
427+ </ button >
428+ ) ) }
429+ </ div >
430+ ) : (
431+ < div className = "p-3 text-center" >
432+ < div className = "text-muted mb-1" >
433+ < i className = "fas fa-search me-2" > </ i >
434+ No matching topics found
435+ </ div >
436+ < small className = "text-muted" > Please select from existing topics</ small >
437+ </ div >
438+ ) }
439+ </ div >
440+ ) }
274441 </ div >
275- < div className = "list-group" style = { { maxHeight : '250px ' , overflowY : 'auto' } } >
442+ < div className = "list-group" style = { { maxHeight : '480px ' , overflowY : 'auto' } } >
276443 { finalizedTopics . map ( ( topic ) => (
277- < div key = { topic } className = "list-group-item d-flex justify-content-between align-items-center" >
278- < span className = "d-flex align-items-center" >
444+ < div key = { topic } className = "list-group-item py-2 px-3 d-flex justify-content-between align-items-center" >
445+ < span className = "d-flex align-items-center" style = { { fontSize : '0.9rem' } } >
279446 { topic }
280- { llmSuggestions . includes ( topic ) && < span className = "badge bg-info ms-2" > AI</ span > }
447+ { llmSuggestions . includes ( topic ) && < span className = "badge bg-info ms-2" style = { { fontSize : '0.75rem' } } > AI</ span > }
281448 </ span >
282449 < button
283450 className = "btn btn-sm btn-outline-danger ms-2"
284- style = { { width : 32 , height : 32 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , padding : 0 } }
451+ style = { { width : 28 , height : 28 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , padding : 0 } }
285452 onClick = { ( ) => moveToLeftColumn ( topic ) }
286453 title = "Remove from finalized topics"
287454 >
288- < Minus size = { 16 } />
455+ < Minus size = { 14 } />
289456 </ button >
290457 </ div >
291458 ) ) }
0 commit comments