From 53b9eb317760385bd8e188faf6f480ab0cb0d16d Mon Sep 17 00:00:00 2001 From: Hrusikesh123 Date: Sun, 24 Aug 2025 15:12:15 +0530 Subject: [PATCH 1/3] Changes Updated --- frontend/app/skills/page.tsx | 4 +-- frontend/components/skills/skills-test.tsx | 26 ++++++++++--------- frontend/components/skills/welcome-screen.tsx | 8 +++--- package-lock.json | 6 +++++ 4 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 package-lock.json diff --git a/frontend/app/skills/page.tsx b/frontend/app/skills/page.tsx index b889250..07b4487 100644 --- a/frontend/app/skills/page.tsx +++ b/frontend/app/skills/page.tsx @@ -136,7 +136,7 @@ export default function SkillsPage() { // Loading state - show while checking auth or loading user data if (loading || authLoading) { return ( -
+

Loading...

@@ -146,7 +146,7 @@ export default function SkillsPage() { } return ( -
+
{currentScreen === "welcome" && } {currentScreen === "test" && } diff --git a/frontend/components/skills/skills-test.tsx b/frontend/components/skills/skills-test.tsx index e8556a8..56d1f91 100644 --- a/frontend/components/skills/skills-test.tsx +++ b/frontend/components/skills/skills-test.tsx @@ -164,16 +164,16 @@ export default function SkillsTest({ onComplete }: SkillsTestProps) { const canProceed = selectedOptions[currentQuestionIndex] !== -1 return ( -
+
-
+
Question {currentQuestionIndex + 1} of {questions.length} {Math.round(progress)}% Complete
- -
+ +
@@ -186,7 +186,7 @@ export default function SkillsTest({ onComplete }: SkillsTestProps) { transition={{ duration: 0.3 }} className={`${isTransitioning ? "opacity-0" : "opacity-100"} transition-opacity duration-300`} > -

{currentQuestion.question}

+

{currentQuestion.question}

{currentQuestion.options.map((option, index) => ( @@ -194,22 +194,24 @@ export default function SkillsTest({ onComplete }: SkillsTestProps) { key={index} className={`p-4 rounded-lg border-2 cursor-pointer transition-all duration-200 ${ selectedOptions[currentQuestionIndex] === index - ? "border-purple-500 bg-purple-900/30" - : "border-gray-700 hover:border-purple-400 bg-gray-800/30" + ? "border-blue-500 dark:border-purple-500 bg-blue-50/50 dark:bg-purple-900/30" + : "border-gray-300 dark:border-gray-700 hover:border-blue-400 dark:hover:border-purple-400 bg-white/50 dark:bg-gray-800/30" }`} onClick={() => handleOptionSelect(index)} >
{selectedOptions[currentQuestionIndex] === index && (
)}
- {option.text} + {option.text}
))} @@ -220,7 +222,7 @@ export default function SkillsTest({ onComplete }: SkillsTestProps) { variant="outline" onClick={handleBack} disabled={isFirstQuestion} - className={`${isFirstQuestion ? "opacity-0" : "opacity-100"} border-gray-600 text-white hover:bg-purple-900/30 hover:text-purple-300`} + className={`${isFirstQuestion ? "opacity-0" : "opacity-100"} border-gray-300 dark:border-gray-600 text-gray-700 dark:text-white hover:bg-blue-50 dark:hover:bg-purple-900/30 hover:text-blue-700 dark:hover:text-purple-300`} > Back @@ -230,7 +232,7 @@ export default function SkillsTest({ onComplete }: SkillsTestProps) { + + +
+
+

Keywords

+
+ {allSkills.map(skill => ( +
+ handleSkillToggle(skill)} + /> + +
+ ))} +
+
+ +
+ + +
+
+
+ + ) +} diff --git a/frontend/components/issues-list.tsx b/frontend/components/issues-list.tsx index f783005..7dab26f 100644 --- a/frontend/components/issues-list.tsx +++ b/frontend/components/issues-list.tsx @@ -2,6 +2,8 @@ import { useEffect, useState, useRef, useCallback } from "react" import { IssueCard } from "@/components/issue-card" +import { useAuth } from "@/context/auth-context" +import { getUserByGithubId } from "@/lib/firebase-utils" const MATCH_COLORS = ["#8b5cf6", "#ec4899", "#f97316", "#10b981", "#3b82f6"] @@ -21,14 +23,52 @@ interface IssuesListProps { type: "recommended" | "trending" | "favorite" | "recent" } +import { IssueFilter } from "@/components/issue-filter" + export function IssuesList({ type }: IssuesListProps) { const [issues, setIssues] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [userSkills, setUserSkills] = useState([]) + const [filters, setFilters] = useState<{ keywords: string[] }>({ + keywords: [], + }) const timeoutRef = useRef(null) + const { user, isLoading: authLoading } = useAuth() + + // Fetch user skills from Firebase + const fetchUserSkills = useCallback(async () => { + if (!user) { + setUserSkills([]) + return + } + try { + const result = await getUserByGithubId(user.id) + if (result.success && result.data?.skill_keywords) { + setUserSkills(result.data.skill_keywords) + // Initialize filters.keywords with user skills + setFilters((prev) => ({ ...prev, keywords: result.data.skill_keywords })) + } else { + setUserSkills([]) + } + } catch (error) { + console.error("Error fetching user skills:", error) + setUserSkills([]) + } + }, [user]) // Create a fetchIssues function that can be called both in useEffect and by the retry button const fetchIssues = useCallback(async () => { + if (authLoading) { + return + } + + if (!user) { + setError("Please log in to view issues") + setLoading(false) + return + } + // Clear any existing timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current) @@ -38,7 +78,7 @@ export function IssuesList({ type }: IssuesListProps) { timeoutRef.current = setTimeout(() => { setLoading(false) setError("Request timed out. Please try again later.") - }, 25000) // 15 seconds timeout + }, 25000) // 25 seconds timeout setLoading(true) setError(null) @@ -46,32 +86,45 @@ export function IssuesList({ type }: IssuesListProps) { try { console.log(`Fetching ${type} issues...`) - // Different endpoints based on issue type + // Build endpoint with filters for recommended type let endpoint = "" - switch (type) { - case "recommended": - endpoint = "http://localhost:8000/api/v1/match/match-issue?keywords=machine-learning&keywords=java&max_results=5" - break - case "trending": - endpoint = "http://localhost:8000/api/v1/match/trending-issues" - break - case "favorite": - endpoint = "http://localhost:8000/api/v1/match/favorite-issues" - break - case "recent": - endpoint = "http://localhost:8000/api/v1/match/recent-issues" - break - default: - endpoint = "http://localhost:8000/api/v1/match/match-issue?keywords=machine-learning&keywords=java&max_results=5" + if (type === "recommended") { + const params = new URLSearchParams() + if (filters.keywords.length > 0) { + filters.keywords.forEach((skill) => { + params.append("keywords", skill) + }) + } else { + // fallback keywords if no user skills + params.append("keywords", "machine-learning") + params.append("keywords", "java") + } + + params.append("max_results", "5") + endpoint = `http://localhost:8000/api/v1/match/match-issue?${params.toString()}` + } else { + switch (type) { + case "trending": + endpoint = "http://localhost:8000/api/v1/match/trending-issues" + break + case "favorite": + endpoint = "http://localhost:8000/api/v1/match/favorite-issues" + break + case "recent": + endpoint = "http://localhost:8000/api/v1/match/recent-issues" + break + default: + endpoint = "http://localhost:8000/api/v1/match/match-issue?keywords=machine-learning&keywords=java&max_results=5" + } } const response = await fetch(endpoint, { - method: 'GET', + method: "GET", headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', + "Content-Type": "application/json", + Accept: "application/json", }, - credentials: 'include' + credentials: "include", }) // Clear the timeout since we got a response @@ -82,7 +135,7 @@ export function IssuesList({ type }: IssuesListProps) { if (!response.ok) { const errorText = await response.text() - throw new Error(`API error: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ''}`) + throw new Error(`API error: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`) } const data = await response.json() @@ -104,9 +157,14 @@ export function IssuesList({ type }: IssuesListProps) { setLoading(false) setError(err instanceof Error ? err.message : "Failed to fetch issues") } - }, [type]) + }, [type, filters, authLoading, user]) - // Effect to fetch issues when the component mounts or type changes + // Effect to fetch user skills when user changes + useEffect(() => { + fetchUserSkills() + }, [fetchUserSkills]) + + // Effect to fetch issues when the component mounts or type, filters change useEffect(() => { fetchIssues() @@ -116,7 +174,7 @@ export function IssuesList({ type }: IssuesListProps) { clearTimeout(timeoutRef.current) } } - }, [fetchIssues]) // fetchIssues already depends on type + }, [fetchIssues]) // fetchIssues already depends on type, filters, authLoading // Transform API issues to the format expected by IssueCard const transformedIssues = issues.map((issue, index) => ({ @@ -127,44 +185,56 @@ export function IssuesList({ type }: IssuesListProps) { skillMatch: Math.min(Math.round(issue.similarity_score * 100 + 30), 99), // Cap at 99% skills: issue.labels?.slice(0, 5) || ["No labels"], matchColor: MATCH_COLORS[index % MATCH_COLORS.length], - issueUrl: issue.issue_url + issueUrl: issue.issue_url, })) return ( -
- {loading ? ( -
-
-
-
-
-
-
-
+ <> + +
+ {loading ? ( +
+
+
+
+
+
+
+
+
-
- ) : error ? ( -
-
Error loading issues
-
{error}
- -
- ) : transformedIssues.length === 0 ? ( -
-
No issues found
-
Try adjusting your search criteria or check back later
-
- ) : ( - transformedIssues.map((issue) => ( - - )) - )} -
+ ) : error ? ( +
+
Error loading issues
+
{error}
+ {error === "Please log in to view issues" ? ( + + Log In + + ) : ( + + )} +
+ ) : transformedIssues.length === 0 ? ( +
+
No issues found
+
Try adjusting your search criteria or check back later
+
+ ) : ( + transformedIssues.map((issue) => ( + + )) + )} +
+ ) -} \ No newline at end of file +} From 2e68a101ed5f71412ac5380c4cbf5da50948baa3 Mon Sep 17 00:00:00 2001 From: Hrusikesh123 Date: Sun, 7 Sep 2025 09:43:30 +0530 Subject: [PATCH 3/3] Updated --- backend/app/api/v1/endpoints/match.py | 2 +- backend/app/services/faiss_search.py | 68 ++++++++++++--------------- backend/requirements.txt | 1 + frontend/app/match/page.tsx | 21 +++++++-- frontend/components/issue-filter.tsx | 11 +++-- frontend/components/issues-list.tsx | 8 +--- 6 files changed, 57 insertions(+), 54 deletions(-) diff --git a/backend/app/api/v1/endpoints/match.py b/backend/app/api/v1/endpoints/match.py index 0f49ec4..090ac36 100644 --- a/backend/app/api/v1/endpoints/match.py +++ b/backend/app/api/v1/endpoints/match.py @@ -112,7 +112,7 @@ async def match_issues( all_keywords.extend(topics) # Get top matched issues - result = get_top_matched_issues( + result = await get_top_matched_issues( query_text=text_blob, keywords=all_keywords, languages=languages, diff --git a/backend/app/services/faiss_search.py b/backend/app/services/faiss_search.py index 2cee421..9b96273 100644 --- a/backend/app/services/faiss_search.py +++ b/backend/app/services/faiss_search.py @@ -1,4 +1,5 @@ -import requests +import asyncio +import aiohttp from sentence_transformers import SentenceTransformer import faiss, re import numpy as np @@ -30,44 +31,35 @@ def load_model(): load_model() -def fetch_github_issues(keywords: List[str], top_k: int = TOP_PER_KEYWORD, github_token: Optional[str] = None) -> List[ - Dict[str, Any]]: - """ - Fetch GitHub issues based on keywords. - - Args: - keywords: List of keywords to search for - top_k: Number of issues to fetch per keyword - github_token: GitHub API token for authentication +async def fetch_issues_for_keyword(session: aiohttp.ClientSession, keyword: str, top_k: int, headers: Dict[str, str]) -> List[Dict[str, Any]]: + query = f'{keyword}+state:open+type:issue' + url = f"https://api.github.com/search/issues?q={query}&per_page={top_k}" + logger.info(f"Fetching issues for keyword: {keyword}") + async with session.get(url, headers=headers) as response: + if response.status == 200: + json_response = await response.json() + items = json_response.get('items', []) + logger.info(f"Found {len(items)} issues for keyword: {keyword}") + return items[:top_k] + else: + logger.error(f"Error for keyword: {keyword}, Status Code: {response.status}") + if response.status == 403: + logger.error("Rate limit exceeded or authentication required") + elif response.status == 401: + logger.error("Unauthorized - check your GitHub token") + return [] - Returns: - List of GitHub issues - """ +async def fetch_github_issues(keywords: List[str], top_k: int = TOP_PER_KEYWORD, github_token: Optional[str] = None) -> List[Dict[str, Any]]: logger.info(f"Fetching GitHub issues for keywords: {keywords}") - headers = {"Accept": "application/vnd.github+json"} if github_token: headers["Authorization"] = f"Bearer {github_token}" - all_issues = [] - for keyword in keywords: - # Search for keyword in title, body, and comments instead of just labels - query = f'{keyword}+state:open+type:issue' - url = f"https://api.github.com/search/issues?q={query}&per_page={top_k}" - - logger.info(f"Fetching issues for keyword: {keyword}") - response = requests.get(url, headers=headers) - - if response.status_code == 200: - items = response.json().get('items', []) - logger.info(f"Found {len(items)} issues for keyword: {keyword}") - all_issues.extend(items[:top_k]) # Take top N only - else: - logger.error(f"Error for keyword: {keyword}, Status Code: {response.status_code}") - if response.status_code == 403: - logger.error("Rate limit exceeded or authentication required") - elif response.status_code == 401: - logger.error("Unauthorized - check your GitHub token") + async with aiohttp.ClientSession() as session: + tasks = [fetch_issues_for_keyword(session, keyword, top_k, headers) for keyword in keywords] + results = await asyncio.gather(*tasks) + + all_issues = [issue for sublist in results for issue in sublist] # Deduplicate by URL unique_issues = list({issue['html_url']: issue for issue in all_issues}.values()) @@ -180,7 +172,7 @@ def format_issues_json(issues: List[Dict[str, Any]]) -> List[Dict[str, Any]]: return results -def get_top_matched_issues( +async def get_top_matched_issues( query_text: str, keywords: List[str], languages: List[str] = None, @@ -225,14 +217,14 @@ def get_top_matched_issues( search_keywords = list(set(search_keywords)) logger.info(f"Search keywords: {search_keywords}") - # Fetch issues - issues = fetch_github_issues(search_keywords, top_k=TOP_PER_KEYWORD, github_token=github_token) + # Fetch issues asynchronously + issues = await fetch_github_issues(search_keywords, top_k=TOP_PER_KEYWORD, github_token=github_token) # Fallback: If no issues found with specific keywords, try with general programming keywords if not issues: logger.warning("No issues fetched with specific keywords, trying fallback keywords") fallback_keywords = ["python", "javascript", "java", "react", "node", "good first issue"] - issues = fetch_github_issues(fallback_keywords, top_k=TOP_PER_KEYWORD, github_token=github_token) + issues = await fetch_github_issues(fallback_keywords, top_k=TOP_PER_KEYWORD, github_token=github_token) if not issues: logger.warning("No issues fetched even with fallback keywords") @@ -274,4 +266,4 @@ def get_top_matched_issues( "issues_fetched": 0, "issues_indexed": 0, "message": f"Error matching issues: {str(e)}" - } \ No newline at end of file + } diff --git a/backend/requirements.txt b/backend/requirements.txt index f650cf9..9d5e374 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -16,3 +16,4 @@ beautifulsoup4==4.13.0 requests numpy vertexai +aiohttp==3.9.1 diff --git a/frontend/app/match/page.tsx b/frontend/app/match/page.tsx index e5dc225..724f12c 100644 --- a/frontend/app/match/page.tsx +++ b/frontend/app/match/page.tsx @@ -1,3 +1,6 @@ +"use client" + +import { useState } from "react" import { Slider } from "@/components/slider" import { IssuesList } from "@/components/issues-list" import { SortDropdown } from "@/components/sort-dropdown" @@ -5,8 +8,13 @@ import { SortOptions } from "@/components/sort-options" import { InspirationSidebar } from "@/components/inspiration-sidebar" import { NavigationButtons } from "@/components/navigation-buttons" import { Footer } from "@/components/footer" +import { IssueFilter } from "@/components/issue-filter" export default function Home() { + const [filters, setFilters] = useState<{ keywords: string[] }>({ + keywords: [], + }) + return (
{/* Hero Slider Section */} @@ -28,11 +36,16 @@ export default function Home() { {/*
*/}
-
-

Recommended

- +
+
+

Recommended

+
+ + +
+
- +
diff --git a/frontend/components/issue-filter.tsx b/frontend/components/issue-filter.tsx index 4e5b7bb..fed71e0 100644 --- a/frontend/components/issue-filter.tsx +++ b/frontend/components/issue-filter.tsx @@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Badge } from "@/components/ui/badge" -import { Filter } from "lucide-react" +import { Filter, ChevronDown } from "lucide-react" interface IssueFilterProps { onFilterChange: (filters: { keywords: string[] }) => void @@ -69,15 +69,18 @@ export function IssueFilter({ onFilterChange }: IssueFilterProps) { return ( - + +
diff --git a/frontend/components/issues-list.tsx b/frontend/components/issues-list.tsx index 7dab26f..1d57e75 100644 --- a/frontend/components/issues-list.tsx +++ b/frontend/components/issues-list.tsx @@ -25,14 +25,11 @@ interface IssuesListProps { import { IssueFilter } from "@/components/issue-filter" -export function IssuesList({ type }: IssuesListProps) { +export function IssuesList({ type, filters = { keywords: [] } }: IssuesListProps & { filters?: { keywords: string[] } }) { const [issues, setIssues] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [userSkills, setUserSkills] = useState([]) - const [filters, setFilters] = useState<{ keywords: string[] }>({ - keywords: [], - }) const timeoutRef = useRef(null) const { user, isLoading: authLoading } = useAuth() @@ -46,8 +43,6 @@ export function IssuesList({ type }: IssuesListProps) { const result = await getUserByGithubId(user.id) if (result.success && result.data?.skill_keywords) { setUserSkills(result.data.skill_keywords) - // Initialize filters.keywords with user skills - setFilters((prev) => ({ ...prev, keywords: result.data.skill_keywords })) } else { setUserSkills([]) } @@ -190,7 +185,6 @@ export function IssuesList({ type }: IssuesListProps) { return ( <> -
{loading ? (