diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index 93930bf..5a6adf0 100644 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -42,8 +42,10 @@ async def github_callback_handler(request: Request, code: str = None, state: str """ # Verify state parameter to prevent CSRF attacks stored_state = request.session.get('oauth_state') + print(f"DEBUG: Received state: {state}, Stored state: {stored_state}") if not state or not stored_state or state != stored_state: request.session.pop('oauth_state', None) # Clean up state + print(f"DEBUG: State mismatch - received: {state}, stored: {stored_state}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Invalid state parameter. CSRF check failed." diff --git a/backend/app/main.py b/backend/app/main.py index f1608ae..85b003d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,3 +1,4 @@ +import os from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from starlette.middleware.sessions import SessionMiddleware @@ -15,11 +16,11 @@ app.add_middleware( SessionMiddleware, secret_key=settings.SECRET_KEY, - # --- Optional Session Cookie Parameters (consider for production) --- - # session_cookie="your_app_session", # Customize the cookie name - # max_age=7 * 24 * 60 * 60, # Example: Cookie expires after 7 days - # same_site="lax", # Or "strict" for more security, but test carefully - # https_only=True, # Recommended: Send cookie only over HTTPS + # --- Session Cookie Parameters --- + session_cookie="issuematch_session", # Customize the cookie name + max_age=7 * 24 * 60 * 60, # Example: Cookie expires after 7 days + same_site="lax", # Use lax for localhost development to avoid CSRF issues + https_only=False, # No HTTPS in development ) @@ -32,7 +33,6 @@ ] # In production, allow requests from any origin -import os if os.environ.get("RENDER", False): origins = ["*"] # Allow all origins in production @@ -62,14 +62,16 @@ async def read_root(): # --- Optional: Startup/Shutdown Event Handlers --- # These functions can run code when the server starts or stops. # Useful for loading resources (like a FAISS index) or cleaning up. -# @app.on_event("startup") -# async def startup_event(): -# """ -# Code to run when the application starts up. -# Example: Load ML models, FAISS index, connect to databases. -# """ -# print("Backend server starting up...") -# # load_faiss_index() # Example placeholder +from .services.faiss_search import load_model + +@app.on_event("startup") +async def startup_event(): + """ + Code to run when the application starts up. + Example: Load ML models, FAISS index, connect to databases. + """ + print("Backend server starting up...") + load_model() # @app.on_event("shutdown") # async def shutdown_event(): diff --git a/backend/app/services/faiss_search.py b/backend/app/services/faiss_search.py index afab5f3..2cee421 100644 --- a/backend/app/services/faiss_search.py +++ b/backend/app/services/faiss_search.py @@ -17,15 +17,18 @@ # Global variables model = None -# Initialize the model -try: - logger.info(f"Loading sentence transformer model: {MODEL_NAME}") - model = SentenceTransformer(MODEL_NAME) - logger.info("Model loaded successfully") -except Exception as e: - logger.error(f"Error loading model: {str(e)}") - model = None +def load_model(): + global model + if model is None: + try: + logger.info(f"Loading sentence transformer model: {MODEL_NAME}") + model = SentenceTransformer(MODEL_NAME) + logger.info("Model loaded successfully") + except Exception as e: + logger.error(f"Error loading model: {str(e)}") + model = None +load_model() def fetch_github_issues(keywords: List[str], top_k: int = TOP_PER_KEYWORD, github_token: Optional[str] = None) -> List[ Dict[str, Any]]: @@ -48,15 +51,16 @@ def fetch_github_issues(keywords: List[str], top_k: int = TOP_PER_KEYWORD, githu all_issues = [] for keyword in keywords: - query = f'label:"{keyword}"+state:open+type:issue' + # 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}") + 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}") + 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}") @@ -199,7 +203,7 @@ def get_top_matched_issues( global model try: - # logger.info(f"Getting top matched issues for query: {query_text[:100]}...") + logger.info(f"Getting top matched issues for query: {query_text[:100]}...") # Check if model is loaded if model is None: @@ -224,13 +228,19 @@ def get_top_matched_issues( # Fetch issues issues = 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) + if not issues: - logger.warning("No issues fetched") + logger.warning("No issues fetched even with fallback keywords") return { "recommendations": [], "issues_fetched": 0, "issues_indexed": 0, - "message": "No issues found for the given keywords" + "message": "No issues found for the given keywords. Try adjusting your search criteria or check back later." } # Prepare issue texts for embedding 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/issue-filter.tsx b/frontend/components/issue-filter.tsx new file mode 100644 index 0000000..4e5b7bb --- /dev/null +++ b/frontend/components/issue-filter.tsx @@ -0,0 +1,112 @@ +"use client" + +import { useState, useEffect } from "react" +import { useAuth } from "@/context/auth-context" +import { getUserByGithubId } from "@/lib/firebase-utils" +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" + +interface IssueFilterProps { + onFilterChange: (filters: { keywords: string[] }) => void +} + +const PREDEFINED_SKILLS = [ + "JavaScript", "TypeScript", "Python", "Java", "C++", "C#", "Go", "Rust", + "React", "Vue", "Angular", "Node.js", "Express", "Django", "Flask", "Spring", + "Machine Learning", "Data Science", "AI", "Web Development", "Mobile Development", + "DevOps", "Cloud Computing", "Database", "API", "Testing" +] + +export function IssueFilter({ onFilterChange }: IssueFilterProps) { + const [selectedKeywords, setSelectedKeywords] = useState([]) + const [userSkills, setUserSkills] = useState([]) + const [isOpen, setIsOpen] = useState(false) + const { user } = useAuth() + + // Fetch user skills + useEffect(() => { + const fetchUserSkills = async () => { + if (!user) return + try { + const result = await getUserByGithubId(user.id) + if (result.success && result.data?.skill_keywords) { + setUserSkills(result.data.skill_keywords) + } + } catch (error) { + console.error("Error fetching user skills:", error) + } + } + fetchUserSkills() + }, [user]) + + // Combine user skills with predefined skills + const allSkills = Array.from(new Set([...userSkills, ...PREDEFINED_SKILLS])) + + const handleSkillToggle = (skill: string) => { + const updated = selectedKeywords.includes(skill) + ? selectedKeywords.filter(s => s !== skill) + : [...selectedKeywords, skill] + setSelectedKeywords(updated) + } + + const applyFilters = () => { + onFilterChange({ + keywords: selectedKeywords + }) + setIsOpen(false) + } + + const clearFilters = () => { + setSelectedKeywords([]) + onFilterChange({ keywords: [] }) + } + + const totalSelected = selectedKeywords.length + + return ( + + + + + +
+
+

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 +} 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) {