Skip to content

Commit 3ddb1fb

Browse files
committed
fix the layout
1 parent b9f2582 commit 3ddb1fb

1 file changed

Lines changed: 192 additions & 25 deletions

File tree

src/components/TopicRefiner.tsx

Lines changed: 192 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
"use client"
22

3-
import React, { FC, useState, useEffect } from "react";
3+
import React, { FC, useState, useEffect, useCallback } from "react";
44
import {
55
Sparkles,
66
Plus,
77
Edit,
88
ThumbsUp,
99
Settings,
1010
Minus,
11-
Check
11+
Check,
12+
Search
1213
} from "lucide-react";
1314
import { FaArrowLeft } from "react-icons/fa";
15+
import { API_ENDPOINTS } from "../lib/config";
16+
import debounce from 'lodash/debounce';
1417

1518
interface AIModel {
1619
id: string;
1720
name: string;
1821
}
1922

23+
interface TopicSuggestion {
24+
name: string;
25+
count: number;
26+
}
27+
2028
const 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

Comments
 (0)