diff --git a/client/src/infra/rest/apis/github/index.ts b/client/src/infra/rest/apis/github/index.ts new file mode 100644 index 00000000..cae971b2 --- /dev/null +++ b/client/src/infra/rest/apis/github/index.ts @@ -0,0 +1,68 @@ +// api/github/index.ts +import { patch, post } from '../..'; +import { + AnalyzeUserPayload, + AnalyzeUserResponse, + GetIssueMetricsPayload, + IssueMetricsResponse, + GetPRMetricsPayload, + PRMetricsResponse, + GetLanguagesPayload, + LanguagesResponse, + GetFilesPayload, + GetUserReposPayload, + UserReposResponse, +} from './typing'; +import { ApiResponse, BaseApiResponse } from '../../typings'; + +export const analyzeUser = async (payload: AnalyzeUserPayload) => { + return post< + AnalyzeUserPayload, + ApiResponse + >('/api/github/analyze-user', true, payload); +}; + +export const getIssueMetrics = async (payload: GetIssueMetricsPayload) => { + return post< + GetIssueMetricsPayload, + ApiResponse + >('/api/github/issue-metrics', true, payload); +}; + +export const getPRMetrics = async (payload: GetPRMetricsPayload) => { + return post< + GetPRMetricsPayload, + ApiResponse + >('/api/github/pr-metrics', true, payload); +}; + +export const getLanguages = async (payload: GetLanguagesPayload) => { + return post< + GetLanguagesPayload, + ApiResponse + >('/api/github/languages', true, payload); +}; + +export const getFiles = async (payload: GetFilesPayload) => { + return post< + GetFilesPayload, + ApiResponse + >('/api/github/files', true, payload); +}; + +export const getUserRepos = async (payload: GetUserReposPayload) => { + return post< + GetUserReposPayload, + ApiResponse + >('/api/github/user-repos', true, payload); +}; + +export const detectCapabilities = async (payload: { + files: any[]; + deps?: string[]; +}) => { + return post< + { files: any[]; deps?: string[] }, + ApiResponse + >('/api/github/detect-capabilities', true, payload); +}; diff --git a/client/src/infra/rest/apis/github/typing.ts b/client/src/infra/rest/apis/github/typing.ts new file mode 100644 index 00000000..4dff282a --- /dev/null +++ b/client/src/infra/rest/apis/github/typing.ts @@ -0,0 +1,60 @@ +export interface AnalyzeUserPayload{ + username: string; +} +export interface AnalyzeUserResponse{ + repos: Array<{ + repo:string; + languages: string[]; + capabilities: string[]; + avgPRMergeTime: number | null; + avgIssueResolutionTime: number | null; + } >; +} +export interface GetIssueMetricsPayload{ + owner: string; + repo: string; +} + +export interface IssueMetricsResponse{ + issue: Array<{ + issue: number; + resolutionHours: number; + }>; +} + +export interface GetPRMetricsPayload{ + owner : string; + repo: string ; +} + +export interface PRMetricsResponse{ + prs:Array<{ + prnumber:number; + mergetimehours:number; + }>; +} + +export interface GetLanguagesPayload{ + owner:string; + repo:string; +} + +export interface LanguagesResponse { + [language: string]: number; +} + +export interface GetFilesPayload { + owner: string; + repo: string; +} + +export interface GetUserReposPayload { + username: string; +} + +export interface UserReposResponse { + repos: Array<{ + owner: { login: string }; + name: string; + }>; +} \ No newline at end of file diff --git a/client/src/shared/components/organisms/navbar/components/recommendation.tsx b/client/src/shared/components/organisms/navbar/components/recommendation.tsx new file mode 100644 index 00000000..4b9a1e60 --- /dev/null +++ b/client/src/shared/components/organisms/navbar/components/recommendation.tsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; + +export default function Recommendation() { + const [result, setResult] = useState(""); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchRecommendation = async () => { + try { + const res = await fetch("http://localhost:5000/api/recommend", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ platform: "facebook" }) + }); + + const data = await res.json(); + setResult(data.recommendation); + } catch (err) { + setResult("Error getting recommendation"); + } finally { + setLoading(false); + } + }; + + fetchRecommendation(); + }, []); + + return ( +
+ {loading &&

Generating AI recommendation...

} + {!loading &&
{result}
} +
+ ); +} diff --git a/server/gemini.js b/server/gemini.js new file mode 100644 index 00000000..ef770ace --- /dev/null +++ b/server/gemini.js @@ -0,0 +1,38 @@ +import { GEMINI_API_KEY } from "./src/config/env.js"; + +export async function getAIRecommendation(analysisData) { + const prompt = ` +You are an AI recommendation system. +Based on the following analysis of repos of github, generate clear recommendations of what tech stack should be learned to improve skills according to tech stack and also tell some future project ideas that can be built using the current skills and the recommended skills. + +Analysis: +${JSON.stringify(analysisData, null, 2)} +`; + + const response = await fetch( + `https://generativelanguage.googleapis.com/v1/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`, + + + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + contents: [ + { + parts: [{ text: prompt }] + } + ] + }) + } + ); + + const data = await response.json(); + + console.log("Raw Gemini response:", JSON.stringify(data, null, 2)); + if (!data.candidates || data.candidates.length === 0) { + throw new Error("Gemini returned no response"); +} + +return data.candidates[0].content.parts[0].text; + +} diff --git a/server/index2.js b/server/index2.js new file mode 100644 index 00000000..19e7d417 --- /dev/null +++ b/server/index2.js @@ -0,0 +1,17 @@ +import "dotenv/config"; + +import { analyzeUser } from "./src/services/index.js"; +import { getAIRecommendation } from "./gemini.js"; + +async function run() { + try { + const analysis = await analyzeUser("facebook"); + await getAIRecommendation({ test: "hello" }); + const aiResult = await getAIRecommendation(analysis); + console.log(aiResult); + } catch (err) { + console.error(err); + } +} + +run(); \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 47524e38..c8ff6a56 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -14,7 +14,7 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.5", "crypto": "^1.0.1", - "dotenv": "^16.4.7", + "dotenv": "^16.6.1", "express": "^4.21.2", "helmet": "^8.1.0", "hpp": "^0.2.3", diff --git a/server/package.json b/server/package.json index b1d4f016..24df70ce 100644 --- a/server/package.json +++ b/server/package.json @@ -36,7 +36,7 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.5", "crypto": "^1.0.1", - "dotenv": "^16.4.7", + "dotenv": "^16.6.1", "express": "^4.21.2", "helmet": "^8.1.0", "hpp": "^0.2.3", diff --git a/server/src/config/env.js b/server/src/config/env.js index a257e3be..2cb5ee19 100644 --- a/server/src/config/env.js +++ b/server/src/config/env.js @@ -40,3 +40,6 @@ export const CLOUDINARY_API_SECRET = export const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'dev.admin@example.com'; export const RESEND_API_KEY = process.env.RESEND_API_KEY || 'dev_resend_key_abc123'; + +// AI Configuration +export const GEMINI_API_KEY = process.env.GEMINI_API_KEY; \ No newline at end of file diff --git a/server/src/services/analyze-user.js b/server/src/services/analyze-user.js new file mode 100644 index 00000000..038beae9 --- /dev/null +++ b/server/src/services/analyze-user.js @@ -0,0 +1,94 @@ +/** + * Analyze User - Comprehensive GitHub user analysis + * Gathers repositories, analyzes code patterns, and computes metrics + */ + +import { fetchGitHubRepos } from './fetch-github-repos.js'; +import { fetchRepositoryFiles } from './fetch-repository-files.js'; +import { fetchRepositoryLanguages } from './fetch-repository-languages.js'; +import { fetchPullRequestMetrics } from './fetch-pull-request-metrics.js'; +import { fetchIssueMetrics } from './fetch-issue-metrics.js'; +import { detectCapabilities } from './detect-capabilities.js'; + +/** + * Analyzes a GitHub user's repositories and generates capability metrics + * Fetches repo information, analyzes files, and computes PR/issue metrics + * @param {string} username - GitHub username to analyze + * @returns {Promise>} Analysis results per repository + * @throws {Error} If user not found or API fails + */ +export async function analyzeUser(username) { + if (!username || typeof username !== 'string') { + throw new Error('Invalid username: must be a non-empty string'); + } + + try { + const repos = await fetchGitHubRepos(username); + const analysisResults = []; + + for (const repo of repos) { + console.log('Analyzing repo:', repo.owner.login, repo.name); + + try { + // Fetch repository files + console.log('Fetching files...'); + const files = await fetchRepositoryFiles( + repo.owner.login, + repo.name + ); + + // Detect capabilities based on files + const capabilities = detectCapabilities(files, []); + + // Fetch PR metrics + console.log('Fetching PR metrics...'); + const prMetrics = await fetchPullRequestMetrics( + repo.owner.login, + repo.name + ); + + // Fetch issue metrics + console.log('Fetching issue metrics...'); + const issueMetrics = await fetchIssueMetrics( + repo.owner.login, + repo.name + ); + + // Fetch languages + console.log('Fetching languages...'); + const languages = await fetchRepositoryLanguages(repo); + + // Calculate averages + const avgPRMergeTime = + prMetrics.length + ? prMetrics.reduce((a, b) => a + b.mergeTimeHours, 0) / + prMetrics.length + : null; + + const avgIssueResolutionTime = + issueMetrics.length + ? issueMetrics.reduce((a, b) => a + b.resolutionTimeHours, 0) / + issueMetrics.length + : null; + + analysisResults.push({ + repo: repo.name, + languages: Object.keys(languages), + capabilities, + avgPRMergeTime, + avgIssueResolutionTime + }); + } catch (repoError) { + console.error(`Error analyzing repo ${repo.name}:`, repoError.message); + // Continue with next repo even if one fails + continue; + } + } + + return analysisResults; + } catch (error) { + throw new Error( + `Failed to analyze user ${username}: ${error.message}` + ); + } +} diff --git a/server/src/services/constants/feature-rules.js b/server/src/services/constants/feature-rules.js new file mode 100644 index 00000000..e201fca8 --- /dev/null +++ b/server/src/services/constants/feature-rules.js @@ -0,0 +1,105 @@ +/** + * Feature Detection Rules - Capability detection rules based on file patterns and keywords + */ + +export const FEATURE_RULES = [ + // ───────────── DevOps / Infra ───────────── + { + capability: 'Containerization', + signals: { files: ['Dockerfile', 'docker-compose.yml'] } + }, + { + capability: 'CI/CD Automation', + signals: { + paths: ['.github/workflows/', '.gitlab-ci.yml', '.circleci/'] + } + }, + { + capability: 'Infrastructure as Code', + signals: { extensions: ['.tf'], files: ['kustomization.yaml'] } + }, + { + capability: 'Kubernetes Orchestration', + signals: { keywords: ['kubernetes', 'helm', 'k8s'] } + }, + + // ───────────── Frontend ───────────── + { + capability: 'Frontend Application', + signals: { files: ['package.json', 'vite.config.js', 'next.config.js'] } + }, + { + capability: 'Frontend Performance Optimization', + signals: { keywords: ['react-window', 'react-virtualized', 'lazyload'] } + }, + { + capability: 'Static Site Generation', + signals: { keywords: ['gatsby', 'next export', 'astro'] } + }, + + // ───────────── Backend / Systems ───────────── + { + capability: 'Backend API Development', + signals: { files: ['requirements.txt', 'pom.xml', 'go.mod'] } + }, + { + capability: 'Authentication & Authorization', + signals: { keywords: ['jwt', 'oauth', 'passport', 'auth'] } + }, + { + capability: 'Caching & Performance', + signals: { keywords: ['redis', 'memcached', 'cache'] } + }, + { + capability: 'Asynchronous Processing', + signals: { keywords: ['queue', 'rabbitmq', 'kafka', 'bull'] } + }, + + // ───────────── Databases ───────────── + { + capability: 'Relational Database Usage', + signals: { keywords: ['postgres', 'mysql', 'sqlite'] } + }, + { + capability: 'NoSQL Database Usage', + signals: { keywords: ['mongodb', 'cassandra', 'dynamodb'] } + }, + + // ───────────── ML / AI ───────────── + { + capability: 'ML Model Training', + signals: { files: ['training.py'], keywords: ['scikit-learn', 'xgboost'] } + }, + { + capability: 'Deep Learning', + signals: { keywords: ['torch', 'tensorflow', 'keras'] } + }, + { + capability: 'NLP / LLM Systems', + signals: { keywords: ['transformers', 'langchain', 'openai', 'llama'] } + }, + { + capability: 'Data Analysis & Visualization', + signals: { keywords: ['pandas', 'matplotlib', 'seaborn'] } + }, + + // ───────────── Media / Streaming ───────────── + { + capability: 'Video Streaming', + signals: { keywords: ['ffmpeg', 'webrtc', 'hls', 'm3u8'] } + }, + { + capability: 'Real-time Communication', + signals: { keywords: ['socket.io', 'websocket', 'rtc'] } + }, + + // ───────────── Testing / Quality ───────────── + { + capability: 'Automated Testing', + signals: { keywords: ['jest', 'pytest', 'mocha', 'cypress'] } + }, + { + capability: 'Code Quality & Linting', + signals: { keywords: ['eslint', 'prettier', 'flake8'] } + } +]; diff --git a/server/src/services/detect-capabilities.js b/server/src/services/detect-capabilities.js new file mode 100644 index 00000000..0348cd6d --- /dev/null +++ b/server/src/services/detect-capabilities.js @@ -0,0 +1,38 @@ +/** + * Detect developer capabilities based on repository files and dependencies + * Analyzes files, paths, and keywords to identify technical skills + */ + +import { FEATURE_RULES } from './constants/feature-rules.js'; + +/** + * Analyzes files and dependencies to detect developer capabilities + * @param {Array} files - Repository files with name and path properties + * @param {Array} deps - Dependency list (optional) + * @returns {Array} List of detected capabilities + */ +export function detectCapabilities(files, deps = []) { + const detected = new Set(); + + for (const rule of FEATURE_RULES) { + let matched = false; + + if (rule.signals.files) { + matched ||= files.some((f) => rule.signals.files.includes(f.name)); + } + + if (rule.signals.paths) { + matched ||= files.some((f) => + rule.signals.paths.some((p) => f.path?.startsWith(p)) + ); + } + + if (rule.signals.keywords) { + matched ||= deps.some((d) => rule.signals.keywords.includes(d)); + } + + if (matched) detected.add(rule.capability); + } + + return [...detected]; +} diff --git a/server/src/services/fetch-github-repos.js b/server/src/services/fetch-github-repos.js new file mode 100644 index 00000000..655415ec --- /dev/null +++ b/server/src/services/fetch-github-repos.js @@ -0,0 +1,26 @@ +/** + * Fetch GitHub Repositories - Get user's repositories from GitHub API + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches all repositories for a given GitHub user + * @param {string} username - GitHub username + * @param {number} perPage - Number of repos to fetch per page (default: 1) + * @returns {Promise>} Array of repository objects + * @throws {Error} If API call fails or returns invalid data + */ +export async function fetchGitHubRepos(username, perPage = 1) { + try { + const data = await gh(`/users/${username}/repos?per_page=${perPage}`); + + if (!Array.isArray(data)) { + throw new Error('Failed to fetch repositories: Invalid response format'); + } + + return data; + } catch (error) { + throw new Error(`Failed to fetch repositories for user ${username}: ${error.message}`); + } +} diff --git a/server/src/services/fetch-issue-metrics.js b/server/src/services/fetch-issue-metrics.js new file mode 100644 index 00000000..bd32849e --- /dev/null +++ b/server/src/services/fetch-issue-metrics.js @@ -0,0 +1,35 @@ +/** + * Fetch Issue Metrics - Analyze issue resolution times and statistics + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches and analyzes issue metrics for a repository + * Calculates resolution time in hours for each closed issue + * @param {string} owner - Repository owner (GitHub username) + * @param {string} repo - Repository name + * @returns {Promise>} Array of issue metrics with resolution times + * @throws {Error} If API call fails + */ +export async function fetchIssueMetrics(owner, repo) { + try { + const issues = await gh(`/repos/${owner}/${repo}/issues?state=all`); + + if (!Array.isArray(issues)) { + throw new Error('Invalid response format: expected array of issues'); + } + + return issues + .filter((issue) => issue.closed_at) + .map((issue) => ({ + issueNumber: issue.number, + resolutionTimeHours: + (new Date(issue.closed_at) - new Date(issue.created_at)) / 36e5 + })); + } catch (error) { + throw new Error( + `Failed to fetch issue metrics for ${owner}/${repo}: ${error.message}` + ); + } +} diff --git a/server/src/services/fetch-pull-request-metrics.js b/server/src/services/fetch-pull-request-metrics.js new file mode 100644 index 00000000..356edd65 --- /dev/null +++ b/server/src/services/fetch-pull-request-metrics.js @@ -0,0 +1,35 @@ +/** + * Fetch Pull Request Metrics - Analyze PR merge times and statistics + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches and analyzes pull request metrics for a repository + * Calculates merge time in hours for each merged PR + * @param {string} owner - Repository owner (GitHub username) + * @param {string} repo - Repository name + * @returns {Promise>} Array of PR metrics with merge times + * @throws {Error} If API call fails + */ +export async function fetchPullRequestMetrics(owner, repo) { + try { + const prs = await gh(`/repos/${owner}/${repo}/pulls?state=all`); + + if (!Array.isArray(prs)) { + throw new Error('Invalid response format: expected array of PRs'); + } + + return prs + .filter((pr) => pr.merged_at) + .map((pr) => ({ + prNumber: pr.number, + mergeTimeHours: + (new Date(pr.merged_at) - new Date(pr.created_at)) / 36e5 + })); + } catch (error) { + throw new Error( + `Failed to fetch PR metrics for ${owner}/${repo}: ${error.message}` + ); + } +} diff --git a/server/src/services/fetch-repository-files.js b/server/src/services/fetch-repository-files.js new file mode 100644 index 00000000..9ffcdf18 --- /dev/null +++ b/server/src/services/fetch-repository-files.js @@ -0,0 +1,26 @@ +/** + * Fetch Repository Files - Get file structure from a GitHub repository + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches the file contents structure of a GitHub repository + * @param {string} owner - Repository owner (GitHub username) + * @param {string} repo - Repository name + * @param {string} path - File path to fetch (optional, default: root) + * @returns {Promise>} Array of file objects with name and path + * @throws {Error} If API call fails + */ +export async function fetchRepositoryFiles(owner, repo, path = '') { + try { + const url = path + ? `/repos/${owner}/${repo}/contents/${path}` + : `/repos/${owner}/${repo}/contents`; + + const data = await gh(url); + return Array.isArray(data) ? data : [data]; + } catch (error) { + throw new Error(`Failed to fetch files from ${owner}/${repo}: ${error.message}`); + } +} diff --git a/server/src/services/fetch-repository-languages.js b/server/src/services/fetch-repository-languages.js new file mode 100644 index 00000000..3b0f56d7 --- /dev/null +++ b/server/src/services/fetch-repository-languages.js @@ -0,0 +1,30 @@ +/** + * Fetch Repository Languages - Get programming languages used in a repository + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches the programming languages used in a GitHub repository + * @param {Object} repo - Repository object with owner and name properties + * @param {string} repo.owner.login - Repository owner username + * @param {string} repo.name - Repository name + * @returns {Promise} Object with language names as keys and bytes as values + * @throws {Error} If API call fails + */ +export async function fetchRepositoryLanguages(repo) { + try { + if (!repo || !repo.owner || !repo.owner.login || !repo.name) { + throw new Error('Invalid repository object: missing owner or name'); + } + + const data = await gh( + `/repos/${repo.owner.login}/${repo.name}/languages` + ); + return data || {}; + } catch (error) { + throw new Error( + `Failed to fetch languages from ${repo.owner?.login}/${repo.name}: ${error.message}` + ); + } +} diff --git a/server/src/services/index.js b/server/src/services/index.js new file mode 100644 index 00000000..00c1d80f --- /dev/null +++ b/server/src/services/index.js @@ -0,0 +1,12 @@ +/** + * GitHub Analysis Services + * Exports all service functions for external use + */ + +export { analyzeUser } from './analyze-user.js'; +export { fetchGitHubRepos } from './fetch-github-repos.js'; +export { fetchRepositoryFiles } from './fetch-repository-files.js'; +export { fetchRepositoryLanguages } from './fetch-repository-languages.js'; +export { fetchPullRequestMetrics } from './fetch-pull-request-metrics.js'; +export { fetchIssueMetrics } from './fetch-issue-metrics.js'; +export { detectCapabilities } from './detect-capabilities.js'; diff --git a/server/src/services/utils/github-api.js b/server/src/services/utils/github-api.js new file mode 100644 index 00000000..90e668dd --- /dev/null +++ b/server/src/services/utils/github-api.js @@ -0,0 +1,23 @@ +/** + * GitHub API Helper - Base function for all GitHub API calls + */ + +export async function gh(url) { + // Ensure leading slash + if (!url.startsWith('/')) { + url = '/' + url; + } + + const res = await fetch(`https://api.github.com${url}`, { + headers: { + Accept: 'application/vnd.github+json' + } + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.message); + } + + return res.json(); +} diff --git a/server/src/services/utils/index.js b/server/src/services/utils/index.js new file mode 100644 index 00000000..30cf98b7 --- /dev/null +++ b/server/src/services/utils/index.js @@ -0,0 +1 @@ +export { gh } from './github-api.js';