Skip to content
Open
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
48 changes: 11 additions & 37 deletions src/app/api/ai/roast/route.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,27 @@
import { NextResponse } from 'next/server';
import { GoogleGenerativeAI } from '@google/generative-ai';

// Initialize the Google Generative AI SDK
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');

export async function POST(req: Request) {
export async function POST(request: Request) {
try {
const body = await req.json();
const { mode, stats } = body;
const { commits, prsMerged, issuesClosed } = await request.json();

if (!mode || !stats) {
return NextResponse.json(
{ error: 'Mode (roast/hype) and user stats are required.' },
{ status: 400 }
);
}

const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' });

let systemInstruction = '';
if (mode === 'roast') {
systemInstruction = `You are a hilariously brutal, sarcastic senior developer reviewing a junior's code stats. Roast their coding habits, commit streaks, or languages used based on the provided stats. Keep it strictly safe for work (SFW), funny, and under 3 sentences. No cursing.`;
} else if (mode === 'hype') {
systemInstruction = `You are the ultimate enthusiastic developer hype-man. Look at the user's coding stats and hype them up! Make them feel like a 10x coding god. Keep it energetic, modern, and under 3 sentences.`;
} else {
return NextResponse.json({ error: 'Invalid mode.' }, { status: 400 });
if (!process.env.GEMINI_API_KEY) {
return NextResponse.json({ error: 'Gemini API key not configured.' }, { status: 500 });
}

const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
const prompt = `
${systemInstruction}

User Stats:
- Commits this week: ${stats.commits || 0}
- Top Languages: ${stats.languages?.join(', ') || 'None'}
- Merged PRs: ${stats.mergedPRs || 0}
- Failed Goals: ${stats.failedGoals || 0}

Give me the ${mode}!
You are an expert developer assistant. Write a concise, professional weekly standup update based on:
Commits: ${commits}, PRs: ${prsMerged}, Issues: ${issuesClosed}
Rules: Under 4 sentences, professional tone, Slack-ready format, use emojis. DO NOT hallucinate details.
`;

const result = await model.generateContent(prompt);
const responseText = result.response.text();
return NextResponse.json({ standup: result.response.text() }, { status: 200 });

return NextResponse.json({ message: responseText.trim() });
} catch (error) {
console.error('Gemini API Error:', error);
return NextResponse.json(
{ error: 'Failed to generate response. Please try again.' },
{ status: 500 }
);
return NextResponse.json({ error: 'Failed to generate update.' }, { status: 500 });
}
}
}
73 changes: 73 additions & 0 deletions src/components/WeeklyStandupWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client';
import { useState } from 'react';

export default function WeeklyStandupWidget({ commits, prsMerged, issuesClosed }: { commits: number, prsMerged: number, issuesClosed: number }) {
const [standup, setStandup] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);

const handleGenerate = async () => {
setIsLoading(true);
setError(null);
setCopied(false);

try {
const res = await fetch('/api/ai/standup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ commits, prsMerged, issuesClosed }),
});
const data = await res.json();

if (!res.ok) {
throw new Error(data.error || 'Failed to generate update');
}

setStandup(data.standup);
} catch (err: any) {
setError(err.message);
} finally {
setIsLoading(false);
}
};

const handleCopy = () => {
navigator.clipboard.writeText(standup);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<div className="bg-[#0e0e0e] border border-[#161616] rounded-xl p-6 flex flex-col gap-4 text-white">
<div className="flex justify-between items-center">
<h3 className="text-lg font-bold">Weekly Standup Generator πŸ€–</h3>
<button
onClick={handleGenerate}
disabled={isLoading}
className="bg-blue-600 hover:bg-blue-700 transition-colors px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50"
>
{isLoading ? 'Generating...' : 'Generate Update'}
</button>
</div>

{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 text-sm p-3 rounded-lg">
{error}
</div>
)}

{standup && (
<div className="mt-2 bg-[#161616] p-4 rounded-lg relative group">
<p className="text-sm text-gray-300 whitespace-pre-wrap pr-12">{standup}</p>
<button
onClick={handleCopy}
className="absolute top-3 right-3 bg-[#2a2a2a] hover:bg-[#333] text-xs px-3 py-1.5 rounded transition-all"
>
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
)}
</div>
);
}