Skip to content
Open
Binary file added Assets/54aa05e7-d160-4b04-85f8-2b122c39e6cf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,327 changes: 1,270 additions & 57 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
"@upstash/redis": "^1.38.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"framer-motion": "^12.39.0",
"html-to-image": "^1.11.13",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7",
"lucide-react": "^1.16.0",
"next": "^14.2.35",
"next-auth": "^4.24.7",
"react": "^18",
"react-dom": "^18",
"react-markdown": "^10.1.0",
"recharts": "^2.12.7"
},
"devDependencies": {
Expand Down
37 changes: 37 additions & 0 deletions src/app/api/chatbot/history/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";

export async function GET() {
try {
const session = await getServerSession(authOptions);

if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const userId = session.user.email || session.user.name || "unknown-user";

const { data, error } = await supabaseAdmin
.from("chatbot_messages")
.select("id, role, message, created_at")
.eq("user_id", userId)
.order("created_at", { ascending: true })
.limit(30);

if (error) {
return NextResponse.json(
{ error: "Failed to load chat history" },
{ status: 500 }
);
}

return NextResponse.json({ messages: data || [] });
} catch {
return NextResponse.json(
{ error: "Something went wrong while loading chat history" },
{ status: 500 }
);
}
}
266 changes: 266 additions & 0 deletions src/app/api/chatbot/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";
import {
fetchCIForChatbot,
fetchContributionsForChatbot,
fetchIssuesForChatbot,
fetchLanguagesForChatbot,
fetchPRsForChatbot,
fetchReposForChatbot,
fetchStreakForChatbot,
fetchWeeklySummaryForChatbot,
} from "@/lib/chatbot-metrics";

export async function POST(req: Request) {
try {
const { message } = await req.json();
const session = await getServerSession(authOptions);

if (!session?.accessToken || !session.githubLogin) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (!message || typeof message !== "string") {
return NextResponse.json(
{ error: "Message is required" },
{ status: 400 }
);
}

if (message.length > 2000) {
return NextResponse.json(
{ error: "Message too long" },
{ status: 400 }
);
}

if (!process.env.GROQ_API_KEY) {
return NextResponse.json(
{ error: "AI service unavailable" },
{ status: 500 }
);
}

const userId = session.githubId ?? session.githubLogin;

const cacheContext = {
bypass: false,
userId,
};

const { data: previousChats, error: historyError } = await supabaseAdmin
.from("chatbot_messages")
.select("role,message")
.eq("user_id", userId)
.order("created_at", { ascending: false })
.limit(30);

if (historyError) {
console.error("Failed to load chat history:", historyError);
}

const chatHistory =
previousChats
?.reverse()
.map((chat) => ({
role: chat.role === "bot" ? "assistant" : "user",
content: chat.message,
})) || [];

const { error: userInsertError } = await supabaseAdmin
.from("chatbot_messages")
.insert({
user_id: userId,
role: "user",
message,
});

if (userInsertError) {
console.error("Failed to save user message:", userInsertError);
}

const contributions = await fetchContributionsForChatbot(
session.accessToken,
session.githubLogin,
365,
cacheContext
);

const repos = await fetchReposForChatbot(
session.accessToken,
session.githubLogin,
365,
cacheContext
);

const streak = await fetchStreakForChatbot(
session.accessToken,
session.githubLogin,
cacheContext
);

const prs = await fetchPRsForChatbot(
session.accessToken,
cacheContext
);

const languages = await fetchLanguagesForChatbot(
session.accessToken,
session.githubLogin
);

const issues = await fetchIssuesForChatbot(
session.accessToken
);

const ci = await fetchCIForChatbot(
session.accessToken,
session.githubLogin,
cacheContext
);

const weeklySummary = await fetchWeeklySummaryForChatbot(
session.accessToken,
session.githubLogin
);

const dashboardContext = {
user: {
githubLogin: session.githubLogin,
},
contributions,
streak,
repos,
prs,
languages,
issues,
ci,
weeklySummary,
};

const groqResponse = await fetch(
"https://api.groq.com/openai/v1/chat/completions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.GROQ_API_KEY}`,
},
body: JSON.stringify({
model: "llama-3.3-70b-versatile",
temperature: 0.4,
messages: [
{
role: "system",
content: `
You are DevTrack AI Assistant.

You help users understand their own DevTrack dashboard.

Formatting rules:
- ALWAYS respond in valid markdown.
- NEVER return plain paragraph blocks for analytics.
- ALWAYS use numbered lists or bullet lists for repositories, commits, PRs, rankings, and metrics.
- ALWAYS use headings for sections.
- ALWAYS use **bold** formatting for repository names and important metrics.
- Keep responses concise and developer-focused.
- NEVER dump raw JSON.
- Separate sections with spacing.

Repository ranking responses MUST follow this format exactly:

## Top Repositories

1. **Repository Name** → X commits
2. **Repository Name** → X commits
3. **Repository Name** → X commits

Insight responses MUST follow this format:

## Insights

- Insight 1
- Insight 2
- Insight 3

Recommendation responses MUST follow this format:

## Recommendations

- Recommendation 1
- Recommendation 2
- Recommendation 3

Data rules:
- Use dashboard data as the primary source for analytics and metrics.
- You may also use recent conversation history for contextual follow-up questions.
- If the user previously mentioned information in chat history, reference it clearly as user-provided information.
- Distinguish between dashboard facts and conversational facts.
- If some dashboard data is unavailable or failed to load, clearly mention it.
- Repository and contribution data currently represents up to 365 days of data.
- Do NOT call 365-day data "all-time" unless explicitly available.

Conversation memory rules:
- When the user refers to "it", "that", "he", "she", "they", or similar follow-up references, infer the reference using recent chat history.
- Prefer conversational continuity over generic dashboard summaries for short follow-up questions.
- If the user previously mentioned a repository, person, project, or metric, use that context in later replies.
- If the information is not in chat history or dashboard data, clearly say it is not available.

Example:
User says: "My friend Rahul has 400 commits"
Later user asks: "How many more commits does Rahul have than me?"
Correct behavior:
- Use Rahul's 400 commits from chat history as user-provided information.
- Compare it against the logged-in user's dashboard commits.
- Mention that Rahul's commit count came from the conversation, not dashboard data.

Dashboard data:
${JSON.stringify(dashboardContext)}
`,
},
...chatHistory,
{
role: "user",
content: message,
},
],
}),
}
);

const data = await groqResponse.json();

if (!groqResponse.ok) {
return NextResponse.json(
{ error: "Failed to get AI response" },
{ status: groqResponse.status }
);
}

const reply =
data?.choices?.[0]?.message?.content || "No response generated.";

const { error: botInsertError } = await supabaseAdmin
.from("chatbot_messages")
.insert({
user_id: userId,
role: "bot",
message: reply,
});

if (botInsertError) {
console.error("Failed to save bot message:", botInsertError);
}

return NextResponse.json({ reply });
} catch (error) {
console.error("Chatbot route error:", error);

return NextResponse.json(
{ error: "Something went wrong while processing chatbot request" },
{ status: 500 }
);
}
}
2 changes: 1 addition & 1 deletion src/app/api/metrics/contributions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function mergeContributionDays(
return result;
}

async function fetchContributionsForAccount(
export async function fetchContributionsForAccount(
token: string,
githubLogin: string,
days: number,
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/metrics/repos/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function mergeRepoCommits(
.sort((x, y) => y.commits - x.commits);
}

async function fetchReposForAccount(
export async function fetchReposForAccount(
token: string,
githubLogin: string,
days: number,
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/metrics/streak/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function toDateStr(d: Date): string {
return d.toISOString().slice(0, 10);
}

async function fetchActiveDates(
export async function fetchActiveDates(
githubLogin: string,
token: string,
cacheContext: { bypass: boolean; userId: string }
Expand Down Expand Up @@ -78,7 +78,7 @@ async function fetchActiveDates(
return new Set(dates);
}

function calculateStreakFromDates(
export function calculateStreakFromDates(
activeDates: Set<string>,
freezeDates: Set<string>
): {
Expand Down
3 changes: 3 additions & 0 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import PersonalRecords from "@/components/PersonalRecords";
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import DashboardChatbot from "@/components/DashboardChatbot";

export default async function DashboardPage() {
const session = await getServerSession(authOptions);
Expand Down Expand Up @@ -95,6 +96,8 @@ export default async function DashboardPage() {
<LanguageBreakdown />
<GoalTracker />
</div>
<DashboardChatbot />
</div>

);
}
Loading
Loading