Skip to content

Commit b9f2582

Browse files
committed
add search suggestion
1 parent 34024a0 commit b9f2582

5 files changed

Lines changed: 222 additions & 18 deletions

File tree

backend/app/main.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,68 @@ def explain_topic():
183183
), 500
184184

185185

186+
@app.route("/api/suggest-topics", methods=["GET"])
187+
def suggest_topics():
188+
try:
189+
query = request.args.get("query", "").lower().strip()
190+
if not query:
191+
return jsonify({
192+
"success": True,
193+
"suggestions": []
194+
})
195+
196+
# Use a more sophisticated query that:
197+
# 1. Matches topics containing the search term
198+
# 2. Prioritizes exact matches and high-frequency topics
199+
# 3. Uses word boundary matching for better relevance
200+
sql_query = """
201+
WITH ranked_topics AS (
202+
SELECT
203+
topic,
204+
COUNT(*) as count,
205+
CASE
206+
WHEN LOWER(topic) = ? THEN 3 -- Exact match gets highest priority
207+
WHEN LOWER(topic) LIKE ? THEN 2 -- Starts with query gets second priority
208+
ELSE 1 -- Contains query gets lowest priority
209+
END as match_priority
210+
FROM repo_topics
211+
WHERE LOWER(topic) LIKE ?
212+
GROUP BY topic
213+
)
214+
SELECT topic, count
215+
FROM ranked_topics
216+
ORDER BY match_priority DESC, count DESC
217+
LIMIT 10
218+
"""
219+
220+
# Prepare search patterns
221+
exact_match = query
222+
starts_with = f"{query}%"
223+
contains = f"%{query}%"
224+
225+
# Execute query with all patterns
226+
result = topic_service.con.execute(sql_query, [
227+
exact_match, # For exact match priority
228+
starts_with, # For starts-with priority
229+
contains # For contains match
230+
]).fetchall()
231+
232+
suggestions = [{"name": name.lower(), "count": count} for name, count in result]
233+
234+
return jsonify({
235+
"success": True,
236+
"suggestions": suggestions
237+
})
238+
239+
except Exception as e:
240+
print(f"Error in suggest-topics: {str(e)}") # Add logging
241+
return jsonify({
242+
"success": False,
243+
"error": str(e),
244+
"message": "An error occurred while getting suggestions"
245+
}), 500
246+
247+
186248
@app.route("/")
187249
def home():
188250
return "Hello World!"

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@types/classnames": "^2.3.4",
5353
"@types/file-saver": "^2.0.7",
5454
"@types/fs-extra": "^11.0.4",
55-
"@types/lodash": "^4.17.13",
55+
"@types/lodash": "^4.17.17",
5656
"@types/node": "^22.10.1",
5757
"@types/react": "^18.3.12",
5858
"@types/react-dom": "^18.3.1",

src/lib/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export const API_ENDPOINTS = {
66
PROCESS_TOPICS: `${API_BASE_URL}/api/process-topics`,
77
AI_PROCESS: `${API_BASE_URL}/api/ai-process`,
88
EXPLAIN_TOPIC: `${API_BASE_URL}/api/explain-topic`,
9+
SUGGEST_TOPICS: `${API_BASE_URL}/api/suggest-topics`,
910
} as const;

src/views/HomeView.tsx

Lines changed: 154 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1-
import React, { FC, useEffect, useState } from "react";
1+
import React, { FC, useEffect, useState, useCallback } from "react";
22
import { useLocation, useNavigate } from "react-router";
33
import { FaArrowRight, FaSearch } from "react-icons/fa";
4-
4+
import { API_ENDPOINTS } from "../lib/config";
5+
import { useNotifications } from "../lib/notifications";
56
import Footer from "../components/Footer";
67
import { getErrorMessage } from "../lib/errors";
7-
import { useNotifications } from "../lib/notifications";
8+
import debounce from 'lodash/debounce';
9+
10+
interface TopicSuggestion {
11+
name: string;
12+
count: number;
13+
}
814

915
const HomeView: FC = () => {
1016
const navigate = useNavigate();
1117
const location = useLocation();
1218
const { notify } = useNotifications();
1319
const error = ((location.state as { error?: unknown } | undefined)?.error || "") + "";
1420

15-
// Add state for the search term
21+
// Add state for the search term and suggestions
1622
const [searchTerm, setSearchTerm] = useState("");
23+
const [suggestions, setSuggestions] = useState<TopicSuggestion[]>([]);
24+
const [showSuggestions, setShowSuggestions] = useState(false);
25+
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
1726

1827
useEffect(() => {
1928
if (error)
@@ -23,21 +32,65 @@ const HomeView: FC = () => {
2332
});
2433
}, [error, notify]);
2534

35+
// Debounced function to fetch suggestions
36+
const fetchSuggestions = useCallback(
37+
debounce(async (query: string) => {
38+
if (!query.trim()) {
39+
setSuggestions([]);
40+
return;
41+
}
42+
43+
setIsLoadingSuggestions(true);
44+
try {
45+
const response = await fetch(`${API_ENDPOINTS.SUGGEST_TOPICS}?query=${encodeURIComponent(query)}`);
46+
const data = await response.json();
47+
if (data.success) {
48+
setSuggestions(data.suggestions);
49+
} else {
50+
throw new Error(data.message || 'Failed to get suggestions');
51+
}
52+
} catch (error) {
53+
console.error('Error fetching suggestions:', error);
54+
setSuggestions([]);
55+
} finally {
56+
setIsLoadingSuggestions(false);
57+
}
58+
}, 300),
59+
[]
60+
);
61+
62+
// Update suggestions when search term changes
63+
useEffect(() => {
64+
fetchSuggestions(searchTerm);
65+
}, [searchTerm, fetchSuggestions]);
66+
2667
// Function to handle search submission
2768
const handleSearch = (e?: React.FormEvent) => {
2869
if (e) e.preventDefault();
70+
setShowSuggestions(false);
2971

3072
if (searchTerm.trim()) {
31-
// Navigate to frequency page with both search term and topic
3273
navigate('/topics', {
3374
state: {
3475
searchTerm: searchTerm.trim(),
35-
userTopic: searchTerm.trim() // Add the user topic
76+
userTopic: searchTerm.trim()
3677
}
3778
});
3879
}
3980
};
4081

82+
// Function to handle suggestion click
83+
const handleSuggestionClick = (suggestion: TopicSuggestion) => {
84+
setSearchTerm(suggestion.name);
85+
setShowSuggestions(false);
86+
navigate('/topics', {
87+
state: {
88+
searchTerm: suggestion.name,
89+
userTopic: suggestion.name
90+
}
91+
});
92+
};
93+
4194
return (
4295
<main className="home-view d-flex flex-column justify-content-center" style={{ padding: "0 2rem", minHeight: "100vh", paddingTop: "10vh" }}>
4396
<div className="title-block text-center">
@@ -59,17 +112,18 @@ const HomeView: FC = () => {
59112
Discover and explore research software using large scale graphs
60113
</h2>
61114

62-
{/* Wrap the search bar in a form to handle Enter key submission */}
63-
<form onSubmit={handleSearch} className="search-bar mb-4 d-flex justify-content-center">
115+
{/* Search form with suggestions */}
116+
<form onSubmit={handleSearch} className="search-bar mb-4 d-flex justify-content-center position-relative">
64117
<div
65118
className="input-group align-items-center"
66119
style={{
67120
border: "1px solid #ddd",
68121
borderRadius: "20px",
69122
overflow: "hidden",
70-
width: "800px", // Fixed width
123+
width: "800px",
71124
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)",
72-
backgroundColor: "#fff", // Unified background
125+
backgroundColor: "#fff",
126+
position: "relative",
73127
}}
74128
>
75129
<span
@@ -78,7 +132,7 @@ const HomeView: FC = () => {
78132
padding: "0.75rem",
79133
fontSize: "1rem",
80134
color: "#6c757d",
81-
backgroundColor: "transparent", // Transparent icon background
135+
backgroundColor: "transparent",
82136
}}
83137
>
84138
<FaSearch />
@@ -91,10 +145,16 @@ const HomeView: FC = () => {
91145
boxShadow: "none",
92146
fontSize: "1rem",
93147
padding: "0.75rem",
94-
backgroundColor: "transparent", // Transparent input background
148+
backgroundColor: "transparent",
95149
}}
96150
value={searchTerm}
97-
onChange={(e) => setSearchTerm(e.target.value.replace(/\s+/g, '-'))}
151+
onChange={(e) => {
152+
const value = e.target.value.replace(/\s+/g, '-');
153+
setSearchTerm(value);
154+
setShowSuggestions(true);
155+
}}
156+
onFocus={() => setShowSuggestions(true)}
157+
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
98158
/>
99159
<button
100160
type="submit"
@@ -113,6 +173,87 @@ const HomeView: FC = () => {
113173
<FaArrowRight />
114174
</button>
115175
</div>
176+
177+
{/* Suggestions dropdown */}
178+
{showSuggestions && (searchTerm.trim() || isLoadingSuggestions) && (
179+
<div
180+
className="position-absolute bg-white rounded-3 shadow-lg"
181+
style={{
182+
top: "calc(100% + 8px)",
183+
left: "50%",
184+
transform: "translateX(-50%)",
185+
width: "700px",
186+
maxHeight: "300px",
187+
overflowY: "auto",
188+
zIndex: 1000,
189+
border: "1px solid rgba(0,0,0,0.08)",
190+
backdropFilter: "blur(8px)",
191+
backgroundColor: "rgba(255, 255, 255, 0.98)",
192+
}}
193+
>
194+
{isLoadingSuggestions ? (
195+
<div className="p-4 text-center">
196+
<div className="spinner-border spinner-border-sm text-primary me-2" role="status">
197+
<span className="visually-hidden">Loading...</span>
198+
</div>
199+
<span className="text-muted">Finding relevant topics...</span>
200+
</div>
201+
) : suggestions.length > 0 ? (
202+
<div className="list-group list-group-flush">
203+
{suggestions.map((suggestion, index) => (
204+
<button
205+
key={suggestion.name}
206+
className="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3 px-4"
207+
onClick={() => handleSuggestionClick(suggestion)}
208+
style={{
209+
border: "none",
210+
cursor: "pointer",
211+
transition: "all 0.2s ease",
212+
backgroundColor: "transparent",
213+
borderBottom: index !== suggestions.length - 1 ? "1px solid rgba(0,0,0,0.05)" : "none",
214+
}}
215+
onMouseEnter={(e) => {
216+
e.currentTarget.style.backgroundColor = "rgba(13, 110, 253, 0.05)";
217+
}}
218+
onMouseLeave={(e) => {
219+
e.currentTarget.style.backgroundColor = "transparent";
220+
}}
221+
>
222+
<div className="d-flex align-items-center">
223+
<span className="me-2" style={{ fontSize: "1.1rem" }}>{suggestion.name}</span>
224+
{index === 0 && suggestion.name.toLowerCase() === searchTerm.toLowerCase() && (
225+
<span className="badge bg-success rounded-pill" style={{ fontSize: "0.7rem" }}>
226+
Exact match
227+
</span>
228+
)}
229+
</div>
230+
<div className="d-flex align-items-center">
231+
<span
232+
className="badge rounded-pill px-3 py-2"
233+
style={{
234+
backgroundColor: "rgba(108, 117, 125, 0.1)",
235+
color: "#495057",
236+
fontSize: "0.9rem",
237+
fontWeight: "500"
238+
}}
239+
>
240+
{suggestion.count.toLocaleString()} repos
241+
</span>
242+
</div>
243+
</button>
244+
))}
245+
</div>
246+
) : (
247+
<div className="p-4 text-center">
248+
<div className="text-muted mb-2">
249+
<i className="fas fa-search me-2"></i>
250+
No matching topics found
251+
</div>
252+
<small className="text-muted">Try a different search term</small>
253+
</div>
254+
)}
255+
</div>
256+
)}
116257
</form>
117258

118259
<div className="tags d-flex flex-wrap justify-content-center mb-4" style={{ maxWidth: "600px", margin: "0 auto" }}>

0 commit comments

Comments
 (0)