Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions backend/routes/aiRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const offTopicCache = new NodeCache({ stdTTL: 3600 });
* 200 {"text": "...", "model": "models/gemini-2.5-flash"}
*/
async function generateHandler(req, res) {
const { prompt, history = [] } = req.body || {};
const { prompt, history = [], systemInstruction } = req.body || {};
if (!prompt || !prompt.trim()) {
return res.status(400).json({ error: "Missing prompt" });
}
Expand Down Expand Up @@ -73,7 +73,7 @@ async function generateHandler(req, res) {
try {
const model = genAI.getGenerativeModel({
model: m,
systemInstruction: `You are PrepPilot AI Mentor.
systemInstruction: systemInstruction || `You are PrepPilot AI Mentor.
1. Allow friendly greetings and casual onboarding conversation.
2. Focus primarily on PrepPilot-related domains: interview preparation, coding interviews, aptitude, resumes, career guidance, mock interviews, and platform usage.
3. Politely redirect unrelated conversations.
Expand Down Expand Up @@ -106,6 +106,10 @@ async function generateHandler(req, res) {
if (!result) throw lastErr || new Error("All Gemini models failed");

const rawText = await result.response.text();
console.log("Incoming Prompt:", prompt);
console.log("Model Used:", usedModel);
console.log("Raw Gemini Response:", rawText);

let cleanedText = rawText
.replace(/^[\s`]*json\s*/i, "")
.replace(/^\s*```/i, "")
Expand Down
9 changes: 8 additions & 1 deletion backend/validation/aiPromptSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,16 @@ const safePrompt = (value, helpers) => {
const aiPromptSchema = Joi.object({
prompt: Joi.string()
.min(1)
.max(500)
.max(5000)
.required()
.custom(safePrompt, "Prompt Injection Protection"),
systemInstruction: Joi.string().optional(),
history: Joi.array().items(
Joi.object({
role: Joi.string().valid("user", "model").required(),
text: Joi.string().allow("").required()
})
).optional()

role: Joi.string().min(2).max(50).optional().custom(safePrompt, "Role Injection Protection"),

Expand Down
134 changes: 104 additions & 30 deletions frontend/src/pages/ProjectIdeas/ProjectIdeas.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
BookOpen,
Star,
Zap,
X,
} from "lucide-react";

// ─────────────────────────────────────────────────────────
Expand Down Expand Up @@ -99,13 +100,39 @@ const LEVELS = [
// ─────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────
function buildRequestBody(domainLabel, level) {
// Backend schema: prompt(max 500), role(max 50), topic(max 100) — all required
return {
role: `${level} ${domainLabel} developer`,
topic: `${domainLabel} project ideas for ${level} level`,
prompt: `List 3 ${level} ${domainLabel} project ideas as JSON array. Each: {title, tagline, techStack:[], features:[], githubSearch, difficulty, domain}. Return only valid JSON array.`,
};
function buildPrompt(domainLabel, level) {
return `Generate 3 to 5 unique project ideas for a ${level} level ${domainLabel} developer.
Avoid generic ideas like Todo App, Calculator, or Weather App.

For Frontend, prefer things like: Real-time collaborative whiteboard, Design system builder, Accessibility auditing dashboard.
For Backend, prefer things like: Distributed job scheduler, Event-driven notification platform.
For Full Stack, prefer things like: AI-powered hiring platform, Skill exchange marketplace.
For AI / ML, prefer things like: Resume scoring engine, Interview performance analyzer.
For DevOps / Cloud, prefer things like: Kubernetes deployment assistant, Infrastructure monitoring platform.
For Java, prefer things like: Banking transaction simulator, Distributed inventory system.

For each project, provide:
- "title": Project Title
- "description": Short Description
- "techStack": Array of technologies
- "features": Array of key features
- "learningOutcomes": Array of learning outcomes
- "difficulty": "${level}"
- "resumeValue": String explaining resume value

Return ONLY a valid JSON array of objects with the exact keys:
[
{
"title": "...",
"description": "...",
"techStack": ["..."],
"features": ["..."],
"learningOutcomes": ["..."],
"difficulty": "...",
"resumeValue": "..."
}
]
`;
}

function tryParseJSON(text) {
Expand Down Expand Up @@ -165,7 +192,7 @@ const IdeaCard = ({ idea, domain, index }) => {
{idea.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{idea.tagline}
{idea.description || idea.tagline}
</p>
</div>
</div>
Expand Down Expand Up @@ -205,7 +232,7 @@ const IdeaCard = ({ idea, domain, index }) => {
</div>

{/* Features */}
<div className="space-y-2 flex-1">
<div className="space-y-2">
<div className="flex items-center gap-2">
<Zap
size={14}
Expand All @@ -217,7 +244,7 @@ const IdeaCard = ({ idea, domain, index }) => {
</span>
</div>
<ul className="space-y-1.5 text-sm">
{(idea.features || []).slice(0, 3).map((f, i) => (
{(idea.features || []).slice(0, 2).map((f, i) => (
<li
key={i}
className="flex items-start gap-2 text-gray-700 dark:text-gray-300"
Expand All @@ -233,6 +260,52 @@ const IdeaCard = ({ idea, domain, index }) => {
</ul>
</div>

{/* Learning Outcomes */}
<div className="space-y-2 flex-1">
<div className="flex items-center gap-2">
<BookOpen
size={14}
className="text-gray-600 dark:text-gray-400"
strokeWidth={1.5}
/>
<span className="text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-400">
Learning Outcomes
</span>
</div>
<ul className="space-y-1.5 text-sm">
{(idea.learningOutcomes || []).slice(0, 2).map((l, i) => (
<li
key={i}
className="flex items-start gap-2 text-gray-700 dark:text-gray-300"
>
<ChevronRight
size={14}
className="text-gray-400 dark:text-gray-600 shrink-0 mt-0.5"
strokeWidth={1.5}
/>
<span className="text-sm">{l}</span>
</li>
))}
</ul>
</div>

{/* Resume Value */}
<div className="space-y-2 mb-2">
<div className="flex items-center gap-2">
<Star
size={14}
className="text-gray-600 dark:text-gray-400"
strokeWidth={1.5}
/>
<span className="text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-400">
Resume Value
</span>
</div>
<p className="text-sm text-gray-700 dark:text-gray-300 italic">
"{idea.resumeValue || "Great addition to your portfolio."}"
</p>
</div>

{/* GitHub Link */}
<a
href={`https://github.com/search?q=${encodeURIComponent(idea.githubSearch || idea.title)}&type=repositories`}
Expand Down Expand Up @@ -275,39 +348,40 @@ const ProjectIdeas = () => {
selectedLevel,
);

console.log("BASE_URL:", BASE_URL);
console.log("Prompt:", prompt);

const res = await fetch(`${BASE_URL}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
body: JSON.stringify({
prompt,
systemInstruction: "You are an API that ONLY returns valid JSON arrays. Do not include any conversational text, greetings, or formatting outside the JSON array."
}),
});

console.log("Response Status:", res.status);

if (!res.ok) throw new Error(`Server error ${res.status}`);

// collect the full streamed response
let fullText = "";
if (res.body && typeof res.body.getReader === "function") {
const reader = res.body.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
const { value, done: d } = await reader.read();
done = d;
if (value) fullText += decoder.decode(value, { stream: !done });
}
} else {
const data = await res.json();
fullText = data.text || "";
}
const data = await res.json();
const fullText = data.text || "";

const parsed = tryParseJSON(fullText);
if (parsed && Array.isArray(parsed) && parsed.length > 0) {
setIdeas(parsed);
setGenerated(true);
console.log("Parsed Response:", parsed);
if (parsed && Array.isArray(parsed)) {
if (parsed.length > 0) {
setIdeas(parsed);
setGenerated(true);
} else {
setError("The AI could not generate any ideas. Please try different options.");
}
} else {
setError("The AI returned an unexpected format. Please try again.");
}
} catch (err) {
setError(err.message || "Something went wrong. Please try again.");
console.error("Project Ideas Generator Error:", err);
setError(err.message || "Unknown error");
} finally {
setLoading(false);
}
Expand Down
Loading