Skip to content

Commit ed6afaa

Browse files
authored
connect frontend and backend (#12)
* connect frontend and backend * add dependency for spacy
1 parent 085dbda commit ed6afaa

6 files changed

Lines changed: 2698515 additions & 22 deletions

File tree

‎local.sh‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@ set -euo pipefail
33

44
PIDS=()
55

6-
pushd src/backend
7-
uv run uvicorn api.v1.app:app --host 127.0.0.1 --port 8000 "$@" &
6+
cd src
7+
uv run --project backend uvicorn backend.api.v1.app:app --host 127.0.0.1 --port 8000 "$@" &
88
PIDS+=($!)
99
echo "Uvicorn server started with PID ${PIDS[0]}"
10-
popd
1110

12-
pushd src/frontend
11+
cd frontend
1312
npm run dev &
1413
PIDS+=($!)
1514
echo "Vite server started with PID ${PIDS[1]}"
16-
popd
1715

1816
cleanup() {
1917
echo "Stopping all services..."

‎src/backend/api/v1/app.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import Annotated
2+
from fastapi import FastAPI, Query, HTTPException, status
3+
from fastapi.middleware.cors import CORSMiddleware
4+
from pydantic import BaseModel
5+
6+
from backend.search_engine.models.index import SearchResult
7+
from backend.search_engine.query.query_engine import QueryEngine
8+
9+
app = FastAPI()
10+
11+
app.add_middleware(
12+
CORSMiddleware,
13+
allow_origins=["*"],
14+
allow_credentials=True,
15+
allow_methods=["*"],
16+
allow_headers=["*"],
17+
)
18+
19+
# TODO change once not in json anymore
20+
from backend.search_engine.query.query_engine import inverted_index
21+
from backend.search_engine.index.inverted_index import InvertedIndex
22+
23+
inverted_index_loaded = InvertedIndex.from_json(
24+
"PATH"
25+
)
26+
inverted_index.index = inverted_index_loaded.index
27+
inverted_index.doc_store = inverted_index_loaded.doc_store
28+
inverted_index.all_doc_ids = inverted_index_loaded.all_doc_ids
29+
# ---------------------
30+
31+
32+
@app.get("/search", response_model=list[SearchResult])
33+
async def search(
34+
q: Annotated[
35+
str, Query(min_length=1, max_length=50, description="Search query")
36+
] = ...,
37+
limit: Annotated[
38+
int, Query(ge=1, le=100, description="Maximum number of results")
39+
] = 10,
40+
) -> list[SearchResult]:
41+
if inverted_index is None:
42+
raise HTTPException(
43+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
44+
detail="Search index not loaded",
45+
)
46+
47+
try:
48+
qe = QueryEngine(q)
49+
50+
return qe.search_results(limit)
51+
except ValueError as e:
52+
raise HTTPException(
53+
status_code=status.HTTP_400_BAD_REQUEST,
54+
detail=f"Invalid query syntax: {str(e)}",
55+
)
56+
except Exception as e:
57+
raise HTTPException(
58+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
59+
detail="Search operation failed",
60+
)

‎src/backend/pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies = [
1313
"pydantic>=2.12.3",
1414
"requests>=2.32.5",
1515
"tqdm>=4.67.1",
16+
"typer>=0.20.0",
1617
]
1718

1819
[dependency-groups]

‎src/backend/uv.lock‎

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

‎src/frontend/src/pages/Index.tsx‎

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { SearchResults } from "@/components/SearchResults";
44
import { LoadingState } from "@/components/LoadingState";
55
import { ErrorState } from "@/components/ErrorState";
66
import { useToast } from "@/hooks/use-toast";
7-
import { ChevronLeft, ChevronRight } from "lucide-react";
7+
import { ChevronLeft, ChevronRight, Settings } from "lucide-react";
88
import seekrLogo from "@/assets/seekr-logo.png";
99
import { mockResults } from "@/mock/searchResults";
1010

@@ -23,6 +23,8 @@ const Index = () => {
2323
const [currentQuery, setCurrentQuery] = useState("");
2424
const { toast } = useToast();
2525
const [currentText, setCurrentText] = useState("");
26+
const [limit, setLimit] = useState(10);
27+
const [showSettings, setShowSettings] = useState(false);
2628

2729
const RESULTS_PER_PAGE = 10;
2830

@@ -40,29 +42,34 @@ const Index = () => {
4042
const params = new URLSearchParams(window.location.search);
4143
const page = parseInt(params.get("page") || "1");
4244
const query = params.get("q") || "";
45+
const urlLimit = parseInt(params.get("limit") || "10");
4346

4447
if (page > 1) setCurrentPage(page);
48+
if (urlLimit) setLimit(urlLimit);
4549
if (query) {
4650
setCurrentQuery(query);
4751
setHasSearched(true);
48-
handleSearch(query);
52+
handleSearch(query, urlLimit);
4953
}
5054
}, []);
5155

52-
// Update URL when page changes
56+
// Update URL when page or limit changes
5357
useEffect(() => {
5458
if (hasSearched && currentQuery) {
5559
const params = new URLSearchParams();
5660
params.set("q", currentQuery);
5761
if (currentPage > 1) {
5862
params.set("page", currentPage.toString());
5963
}
64+
if (limit !== 10) {
65+
params.set("limit", limit.toString());
66+
}
6067
window.history.pushState({}, "", `?${params.toString()}`);
6168

6269
// Scroll to top when page changes
6370
window.scrollTo({ top: 0, behavior: "smooth" });
6471
}
65-
}, [currentPage, hasSearched, currentQuery]);
72+
}, [currentPage, limit, hasSearched, currentQuery]);
6673

6774
// Typing animation effect
6875
useEffect(() => {
@@ -104,31 +111,30 @@ const Index = () => {
104111
};
105112
}, [hasSearched]);
106113

107-
const handleSearch = async (query: string) => {
114+
const handleSearch = async (query: string, customLimit = limit) => {
108115
setIsLoading(true);
109116
setError(null);
110117
setHasSearched(true);
111118
setCurrentQuery(query);
112-
setCurrentPage(1); // Reset to page 1 on new search
119+
setCurrentPage(1);
113120
setAllResults([]);
114-
115-
// Scroll to top when searching
121+
setLimit(customLimit);
122+
116123
window.scrollTo({ top: 0, behavior: "smooth" });
117-
124+
118125
try {
119126
const response = await fetch(
120-
`https://127.0.0.1:8000/search?q=${encodeURIComponent(query)}`
127+
`http://127.0.0.1:8000/search?q=${encodeURIComponent(query)}&limit=${customLimit}`
121128
);
122-
129+
123130
if (!response.ok) {
124131
throw new Error(`Search failed: ${response.statusText}`);
125132
}
126-
133+
127134
const data = await response.json();
128-
129135
const searchResults = Array.isArray(data) ? data : data.results || [];
130136
setAllResults(searchResults);
131-
137+
132138
if (searchResults.length === 0) {
133139
toast({
134140
title: "No results found",
@@ -138,12 +144,11 @@ const Index = () => {
138144
} catch (err) {
139145
const errorMessage =
140146
err instanceof Error ? err.message : "An unknown error occurred while searching";
141-
147+
142148
console.error("Search error:", errorMessage);
143-
144149
setError(errorMessage);
145150
setAllResults(mockResults);
146-
151+
147152
toast({
148153
title: "Search failed — showing mock results",
149154
description: "Backend request failed. Displaying demo data instead.",
@@ -154,6 +159,14 @@ const Index = () => {
154159
}
155160
};
156161

162+
const handleLimitChange = (newLimit: number) => {
163+
setLimit(newLimit);
164+
if (hasSearched && currentQuery) {
165+
handleSearch(currentQuery, newLimit);
166+
}
167+
setShowSettings(false);
168+
};
169+
157170
const handlePageChange = (newPage: number) => {
158171
if (newPage < 1 || newPage > totalPages) return;
159172
setCurrentPage(newPage);
@@ -197,6 +210,57 @@ const Index = () => {
197210

198211
return (
199212
<div className="min-h-screen bg-background">
213+
{/* Settings Button - Fixed Position */}
214+
{hasSearched && (
215+
<div className="fixed top-4 right-4 z-50">
216+
<button
217+
onClick={() => setShowSettings(!showSettings)}
218+
className="p-2.5 rounded-lg bg-background border border-input hover:bg-accent transition-colors shadow-sm"
219+
aria-label="Settings"
220+
>
221+
<Settings className="h-5 w-5" />
222+
</button>
223+
224+
{/* Settings Dropdown */}
225+
{showSettings && (
226+
<>
227+
{/* Backdrop */}
228+
<div
229+
className="fixed inset-0 z-40"
230+
onClick={() => setShowSettings(false)}
231+
/>
232+
233+
{/* Settings Panel */}
234+
<div className="absolute right-0 mt-2 w-64 bg-background border border-input rounded-lg shadow-lg p-4 z-50">
235+
<h3 className="font-semibold text-sm mb-3">Search Settings</h3>
236+
237+
<div className="space-y-2">
238+
<label className="text-sm text-muted-foreground">
239+
Max. total results
240+
</label>
241+
<div className="grid grid-cols-2 gap-2">
242+
{[10, 25, 50, 100].map((l) => (
243+
<button
244+
key={l}
245+
onClick={() => handleLimitChange(l)}
246+
disabled={isLoading}
247+
className={`px-3 py-2 text-sm rounded-lg border transition-colors ${
248+
limit === l
249+
? "bg-primary text-primary-foreground border-primary"
250+
: "border-input hover:bg-accent"
251+
} disabled:opacity-50 disabled:cursor-not-allowed`}
252+
>
253+
{l}
254+
</button>
255+
))}
256+
</div>
257+
</div>
258+
</div>
259+
</>
260+
)}
261+
</div>
262+
)}
263+
200264
{/* Centered Landing View */}
201265
{!hasSearched && (
202266
<div className="flex items-center justify-center min-h-screen px-4">

0 commit comments

Comments
 (0)