From 4da05ff569da1535abfe9f1b75f2757134de5df1 Mon Sep 17 00:00:00 2001 From: A-Choudhari Date: Mon, 2 Feb 2026 16:42:55 -0800 Subject: [PATCH 1/3] implemented workflow for email gen --- backend/package-lock.json | 147 ++++++ backend/package.json | 1 + backend/src/app.ts | 2 + backend/src/controllers/groqController.ts | 166 +++++++ backend/src/routes/groqRoutes.ts | 9 + backend/src/validators/groqValidator.ts | 25 + frontend/src/api/email.ts | 31 ++ frontend/src/pages/Alumni.tsx | 579 ++++++++++++++-------- 8 files changed, 740 insertions(+), 220 deletions(-) create mode 100644 backend/src/controllers/groqController.ts create mode 100644 backend/src/routes/groqRoutes.ts create mode 100644 backend/src/validators/groqValidator.ts create mode 100644 frontend/src/api/email.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index ea43016..4f112ad 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ "express-async-handler": "^1.2.0", "express-validator": "^7.0.1", "firebase": "^12.5.0", + "groq-sdk": "^0.37.0", "http-errors": "^2.0.0", "module-alias": "^2.2.3", "mongodb": "^5.9.2", @@ -3402,6 +3403,18 @@ "ts-morph": "12.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3449,6 +3462,18 @@ "node": ">=0.4.0" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "8.6.3", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", @@ -5495,6 +5520,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/exit-hook": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", @@ -5845,6 +5879,25 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6131,6 +6184,62 @@ "dev": true, "license": "MIT" }, + "node_modules/groq-sdk": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.37.0.tgz", + "integrity": "sha512-lT72pcT8b/X5XrzdKf+rWVzUGW1OQSKESmL8fFN5cTbsf02gq6oFam4SVeNtzELt9cYE2Pt3pdGgSImuTbHFDg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/groq-sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/groq-sdk/node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/groq-sdk/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/groq-sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6248,6 +6357,15 @@ "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", "license": "MIT" }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -7286,6 +7404,26 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -9124,6 +9262,15 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/web-vitals": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", diff --git a/backend/package.json b/backend/package.json index 6831b42..e8578a6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,7 @@ "express-async-handler": "^1.2.0", "express-validator": "^7.0.1", "firebase": "^12.5.0", + "groq-sdk": "^0.37.0", "http-errors": "^2.0.0", "module-alias": "^2.2.3", "mongodb": "^5.9.2", diff --git a/backend/src/app.ts b/backend/src/app.ts index 3e832e5..2546eb2 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -13,6 +13,7 @@ import errorHandler from "../src/middlewares/errorHandler"; import { logger } from "../src/middlewares/logger"; import tipRouter from "../src/routes/tipRoutes"; import profileRoutes from "../src/routes/profileRoutes"; +import groqRoutes from "../src/routes/groqRoutes"; const app = express(); @@ -42,6 +43,7 @@ app.use("/api/questions/leetcode", leetcodeQuestionRouter); app.use("/api/questions/interview", interviewQuestionRouter); app.use("/api/tips", tipRouter); app.use("/api/articles", articleRouter); +app.use("/api/email", groqRoutes); /** * Error handler; all errors thrown by server are handled here. diff --git a/backend/src/controllers/groqController.ts b/backend/src/controllers/groqController.ts new file mode 100644 index 0000000..2d863ea --- /dev/null +++ b/backend/src/controllers/groqController.ts @@ -0,0 +1,166 @@ +import { RequestHandler } from "express"; +import asyncHandler from "express-async-handler"; +import createHttpError from "http-errors"; +import { validationResult, matchedData } from "express-validator"; +import { Groq } from "groq-sdk"; +import validationErrorParser from "../util/validationErrorParser"; +import User from "../models/User"; +import Student from "../models/Student"; +import Alumni from "../models/Alumni"; + +const groq = new Groq({ + apiKey: process.env.GROQ_API_KEY, +}); + +// Helper to calculate shared interests +const calculateSharedInterests = ( + student: any, // Typed as any for simplicity in this helper, could be strict + alumni: any +): string[] => { + const shared: string[] = []; + + // 1. Compare Student Field of Interest vs Alumni Specializations/Industry + if (student.fieldOfInterest && alumni.specializations) { + const studentFields = student.fieldOfInterest.map((s: string) => s.toLowerCase()); + const alumniSpecs = alumni.specializations.map((s: string) => s.toLowerCase()); + + const common = studentFields.filter((f: string) => alumniSpecs.some((s: string) => s.includes(f) || f.includes(s))); + shared.push(...common); + } + + // 2. Compare Hobbies + if (student.hobbies && alumni.hobbies) { + const studentHobbies = student.hobbies.map((s: string) => s.toLowerCase()); + const alumniHobbies = alumni.hobbies.map((s: string) => s.toLowerCase()); + + const common = studentHobbies.filter((h: string) => alumniHobbies.includes(h)); + shared.push(...common); + } + + // 3. Compare Skills + if (student.skills && alumni.skills) { + const studentSkills = student.skills.map((s: string) => s.toLowerCase()); + const alumniSkills = alumni.skills.map((s: string) => s.toLowerCase()); + + const common = studentSkills.filter((s: string) => alumniSkills.includes(s)); + shared.push(...common); + } + + // 4. Compare Organizations (if student organizations existed in model, but for now we look at general matches if name mentions logic) + // Since User model has organizations for both, check that + if (student.organizations && alumni.organizations) { + const studentOrgs = student.organizations.map((s: string) => s.toLowerCase()); + const alumniOrgs = alumni.organizations.map((s: string) => s.toLowerCase()); + + const common = studentOrgs.filter((o: string) => alumniOrgs.includes(o)); + shared.push(...common); + } + + return [...new Set(shared)]; // unique items +}; + + +export const generateEmail: RequestHandler = asyncHandler(async (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return next(createHttpError(400, validationErrorParser(errors))); + } + + const { studentId, alumniId, tone, purpose } = matchedData(req); + + // 1. Fetch Student and Alumni Data + // We need both the User document (for name, etc) and the specific Student/Alumni document (for details) + // However, the User model contains most things now based on my earlier read, but Student/Alumni specific models exist too. + // Let's rely on the User model as the base, and fetch specific profiles if needed. + // Actually, User.ts seems to contain most fields (organizations, specializations, etc) for Alumni, and (major, school) for Student. + // But Student.ts has fieldOfInterest. Let's fetch both to be safe. + + const studentUser = await User.findById(studentId); + const alumniUser = await User.findById(alumniId).populate("company"); + + if (!studentUser || !alumniUser) { + return next(createHttpError(404, "Student or Alumni not found")); + } + + // Fetch specialized docs if needed (Student model has fieldOfInterest) + const studentProfile = await Student.findOne({ userId: studentId }); + const alumniProfile = await Alumni.findOne({ userId: alumniId }); + + // Merge data for processing + const studentData = { + ...studentUser.toObject(), + ...studentProfile?.toObject(), + // Ensure arrays exist + fieldOfInterest: studentProfile?.fieldOfInterest || studentUser.fieldOfInterest || [], + hobbies: studentProfile?.hobbies || studentUser.hobbies || [], + skills: studentProfile?.skills || studentUser.skills || [], + projects: studentProfile?.projects || studentUser.projects || [], + }; + + const alumniData = { + ...alumniUser.toObject(), + ...alumniProfile?.toObject(), + organizations: alumniProfile?.organizations || alumniUser.organizations || [], + specializations: alumniProfile?.specializations || alumniUser.specializations || [], + hobbies: alumniProfile?.hobbies || alumniUser.hobbies || [], + skills: alumniProfile?.skills || alumniUser.skills || [] + }; + + // 2. Calculate Shared Interests + const sharedInterests = calculateSharedInterests(studentData, alumniData); + + // 3. Construct Prompt + const prompt = ` + Write a personalized email from a student to an alumnus. + + **Student Details:** + - Name: ${studentData.name} + - Major: ${studentData.major || "Undecided"} + - School: ${studentData.school || "University"} + + **Alumni Details:** + - Name: ${alumniData.name} + - Position: ${alumniData.position || "Professional"} + - Company: ${(alumniData.company as any)?.name || "their company"} + + **Shared Interests/Common Ground:** + ${sharedInterests.length > 0 ? sharedInterests.join(", ") : "None specifically found, focus on their career path."} + + **User Options:** + - Tone: ${tone || "Professional"} + - Purpose: ${purpose || "To ask for a coffee chat to learn more about their career."} + + **Instructions:** + - Keep it concise (under 150 words). + - Use the shared interests to build rapport if available. + - Be polite and respectful. + - Output ONLY the email body text. Do not include subject line or placeholders like "[Insert Name]". + `; + + // 4. Call Groq + try { + const chatCompletion = await groq.chat.completions.create({ + messages: [ + { + role: "system", + content: "You are a helpful career assistant helping students network with alumni." + }, + { + role: "user", + content: prompt + } + ], + model: "openai/gpt-oss-120b", + temperature: 0.7, + max_tokens: 300 + }); + + const emailContent = chatCompletion.choices[0]?.message?.content || ""; + + res.status(200).json({ email: emailContent, sharedInterests }); + + } catch (error) { + console.error("Groq API Error:", error); + return next(createHttpError(500, "Failed to generate email. Please try again later.")); + } +}); diff --git a/backend/src/routes/groqRoutes.ts b/backend/src/routes/groqRoutes.ts new file mode 100644 index 0000000..b1c8e10 --- /dev/null +++ b/backend/src/routes/groqRoutes.ts @@ -0,0 +1,9 @@ +import express from "express"; +import * as GroqController from "../controllers/groqController"; +import { generateEmailValidator } from "../validators/groqValidator"; + +const router = express.Router(); + +router.post("/generate-email", generateEmailValidator, GroqController.generateEmail); + +export default router; diff --git a/backend/src/validators/groqValidator.ts b/backend/src/validators/groqValidator.ts new file mode 100644 index 0000000..6adad1f --- /dev/null +++ b/backend/src/validators/groqValidator.ts @@ -0,0 +1,25 @@ +import { body } from "express-validator"; + +export const generateEmailValidator = [ + body("studentId") + .exists() + .withMessage("Student ID is required.") + .isString() + .withMessage("Invalid Student ID format."), + body("alumniId") + .exists() + .withMessage("Alumni ID is required.") + .isString() + .withMessage("Invalid Alumni ID format."), + body("tone") + .optional() + .isString() + .isIn(["Professional", "Friendly", "Enthusiastic", "Coffee Chat"]) + .withMessage("Invalid tone."), + body("purpose") + .optional() + .isString() + .trim() + .isLength({ max: 500 }) + .withMessage("Purpose must be less than 500 characters."), +]; diff --git a/frontend/src/api/email.ts b/frontend/src/api/email.ts new file mode 100644 index 0000000..7aaeb05 --- /dev/null +++ b/frontend/src/api/email.ts @@ -0,0 +1,31 @@ +import { APIResult, post, handleAPIError } from "./requests"; + +export interface GenerateEmailRequest { + studentId: string; + alumniId: string; + tone?: string; + purpose?: string; +} + +export interface GenerateEmailResponse { + email: string; + sharedInterests: string[]; +} + +/** + * Generate a personalized email to an alumni. + * + * @param data Request data (studentId, alumniId, tone, purpose) + * @returns Generated email and shared interests + */ +export async function generateEmail( + data: GenerateEmailRequest +): Promise> { + try { + const response = await post("/api/email/generate-email", data); + const json = (await response.json()) as GenerateEmailResponse; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/pages/Alumni.tsx b/frontend/src/pages/Alumni.tsx index b108b72..e1eedae 100644 --- a/frontend/src/pages/Alumni.tsx +++ b/frontend/src/pages/Alumni.tsx @@ -4,9 +4,13 @@ import { FaArrowLeft, FaLinkedin } from "react-icons/fa"; -import { LuMail, LuBuilding2, LuBriefcase } from "react-icons/lu"; +import { LuMail, LuBuilding2, LuBriefcase, LuWand } from "react-icons/lu"; import { FiPhone } from "react-icons/fi"; +import { FaRegCopy } from "react-icons/fa"; import { getAlumniById } from "../api/users"; +import { generateEmail } from "../api/email"; +import { useAuth } from "../contexts/useAuth"; +import Modal from "../components/public/Modal"; import { APIResult } from "../api/requests"; import { Alumni } from "../types/User"; import { ProgressSpinner } from "primereact/progressspinner"; @@ -21,269 +25,404 @@ const AlumniProfile: React.FC = () => { const [alumni, setAlumni] = useState(null); const [loading, setLoading] = useState(true); + // Email Generation State + const { user } = useAuth(); + const [isEmailModalOpen, setIsEmailModalOpen] = useState(false); + const [tone, setTone] = useState("Professional"); + const [purpose, setPurpose] = useState(""); + const [generatedEmail, setGeneratedEmail] = useState(""); + const [sharedInterests, setSharedInterests] = useState([]); + const [generating, setGenerating] = useState(false); + + const handleGenerate = async () => { + if (!user?._id || !id) return; + setGenerating(true); + setGeneratedEmail(""); + + const result = await generateEmail({ + studentId: user._id, + alumniId: id, + tone, + purpose + }); + + if (result.success) { + setGeneratedEmail(result.data.email); + setSharedInterests(result.data.sharedInterests); + } else { + toast.current?.show({ + severity: "error", + summary: "Error", + detail: result.error || "Failed to generate email" + }); + } + setGenerating(false); + }; + + const copyToClipboard = () => { + navigator.clipboard.writeText(generatedEmail); + toast.current?.show({ + severity: "success", + summary: "Copied", + detail: "Email text copied to clipboard" + }); + }; + const handleAlumniUpdate = useCallback(() => { - if (!id) return; - setLoading(true); - getAlumniById(id) - .then((result: APIResult) => { - if (result.success) { - setAlumni(result.data); - } else { - toast.current?.clear(); - toast.current?.show({ - severity: "error", - summary: "Error", - detail: "Failed to fetch alumni profile: " + result.error, - }); - } - }) - .catch(() => toast.current?.show({ - severity: "error", - summary: "Error", - detail: "Unexpected error occurred.", - })) - .finally(() => setLoading(false)); - }, [id]); + if (!id) return; + setLoading(true); + getAlumniById(id) + .then((result: APIResult) => { + if (result.success) { + setAlumni(result.data); + } else { + toast.current?.clear(); + toast.current?.show({ + severity: "error", + summary: "Error", + detail: "Failed to fetch alumni profile: " + result.error, + }); + } + }) + .catch(() => toast.current?.show({ + severity: "error", + summary: "Error", + detail: "Unexpected error occurred.", + })) + .finally(() => setLoading(false)); + }, [id]); // Initial company fetch useEffect(() => { handleAlumniUpdate(); }, [handleAlumniUpdate]); return ( -
- {!alumni ? ( -
- Alumni not found. -
- ) : ( - <> - {/* Display Spinner While Loading */} - {loading && ( -
- -
- )} - {/* When Finished Loading */} - {!loading && ( -
-
- {/* Back Button to Exit Profile */} - +
+ {!alumni ? ( +
+ Alumni not found. +
+ ) : ( + <> + {/* Display Spinner While Loading */} + {loading && ( +
+
-
-
+ )} + {/* When Finished Loading */} + {!loading && ( +
+
+ {/* Back Button to Exit Profile */} + +
+
+
- {/* Left Column - Profile Image */} -
-
- {alumni.profilePicture ? ( - {alumni.name} - ) : ( -
- {alumni.name - .split(" ") - .map((n) => n[0]) - .join("") - .toUpperCase()} -
- )} + {/* Left Column - Profile Image */} +
+
+ {alumni.profilePicture ? ( + {alumni.name} + ) : ( +
+ {alumni.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase()} +
+ )} +
+
+ + {/* Right Column - User Information */} +
+ {/* Basic Information */} +
+

+ Basic Information +

+
+
+ +

{alumni.name}

+
+
+ +

{alumni.email}

+
+ +
+ +

+ {alumni.phoneNumber || "Not provided"} +

+
- - {/* Right Column - User Information */} -
- {/* Basic Information */} + + {/* LinkedIn */} +
+ + {alumni.linkedIn ? ( + + {alumni.linkedIn} + + ) : ( +

Not provided

+ )} +
+ + + {/* Alumni-specific Information */} +

- Basic Information + Professional Information

-
- -

{alumni.name}

-
-

{alumni.email}

+

+ {alumni.company?.name || "Not specified"} +

- +

- {alumni.phoneNumber || "Not provided"} + {alumni.position || "Not specified"}

+ +
- - {/* LinkedIn */} -
+
- {alumni.linkedIn ? ( - - {alumni.linkedIn} - + {Array.isArray(alumni.organizations) && alumni.organizations.length > 0 ? ( +
+ {alumni.organizations.map((organization, index) => ( + + {organization.charAt(0).toUpperCase() + organization.slice(1).toLowerCase()} + + ))} +
) : ( -

Not provided

+

Not specified

)}
- - - {/* Alumni-specific Information */} -
-
-

- Professional Information -

-
-
- -

- { alumni.company?.name || "Not specified"} -

-
- -
- -

- {alumni.position || "Not specified"} -

-
-
-
-
- - {Array.isArray(alumni.organizations) && alumni.organizations.length > 0 ? ( -
- {alumni.organizations.map((organization, index) => ( - - {organization.charAt(0).toUpperCase() + organization.slice(1).toLowerCase()} - - ))} -
- ) : ( -

Not specified

- )} -
- -
- - {Array.isArray(alumni.specializations) && alumni.specializations.length > 0 ? ( -
- {alumni.specializations.map((specialization, index) => ( - - {specialization.charAt(0).toUpperCase() + specialization.slice(1).toLowerCase()} - - ))} -
- ) : ( -

Not specified

- )} + +
+ + {Array.isArray(alumni.specializations) && alumni.specializations.length > 0 ? ( +
+ {alumni.specializations.map((specialization, index) => ( + + {specialization.charAt(0).toUpperCase() + specialization.slice(1).toLowerCase()} + + ))}
- -
- - {Array.isArray(alumni.hobbies) && alumni.hobbies.length > 0 ? ( -
- {alumni.hobbies.map((hobby, index) => ( - - {hobby.charAt(0).toUpperCase() + hobby.slice(1).toLowerCase()} - - ))} -
- ) : ( -

Not specified

- )} + ) : ( +

Not specified

+ )} +
+ +
+ + {Array.isArray(alumni.hobbies) && alumni.hobbies.length > 0 ? ( +
+ {alumni.hobbies.map((hobby, index) => ( + + {hobby.charAt(0).toUpperCase() + hobby.slice(1).toLowerCase()} + + ))}
- -
- - {Array.isArray(alumni.skills) && alumni.skills.length > 0 ? ( -
- {alumni.skills.map((skill, index) => ( - - {skill.charAt(0).toUpperCase() + skill.slice(1).toLowerCase()} - - ))} -
- ) : ( -

Not specified

- )} + ) : ( +

Not specified

+ )} +
+ +
+ + {Array.isArray(alumni.skills) && alumni.skills.length > 0 ? ( +
+ {alumni.skills.map((skill, index) => ( + + {skill.charAt(0).toUpperCase() + skill.slice(1).toLowerCase()} + + ))}
-
+ ) : ( +

Not specified

+ )} +
+ +
+
+
+ )} + + )} + + {/* Email Generation Modal */} + setIsEmailModalOpen(false)} + className="w-full max-w-2xl p-6 rounded-xl" + useOverlay + > +
+

+ + Personalize Email +

+

+ Generate a personalized outreach email to {alumni?.name} based on your shared interests. +

+ +
+
+ + +
+
+ + setPurpose(e.target.value)} + placeholder="e.g. Ask for resume advice" + className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none" + /> +
- )} - - )} + + + + {generatedEmail && ( +
+
+ Generated Draft + +
+