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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Commit Coach is a hackathon-ready Next.js project that turns resolutions into we

## Stack
- Next.js App Router + TypeScript + Tailwind
- Opik for AI agent orchestration + tracing
- Opik for AI agent orchestration
- Supabase for auth + storage

Expand Down Expand Up @@ -42,6 +43,15 @@ This generates `icon-512.png` and `icon-1024.png` from `public/brand/icon.svg`.
- `/app/goal/[id]` goal details
- `/app/review` weekly review

## Agent API routes

All routes expect JSON and return structured JSON validated by Zod.

- `POST /api/agents/intake`
- `POST /api/agents/planner`
- `POST /api/agents/accountability`
- `POST /api/agents/reflection`

## Opik agent plan (starter)
- Intake agent → converts the resolution into a SMART goal.
- Planner agent → builds weekly milestones + daily tasks.
Expand Down
18 changes: 18 additions & 0 deletions app/api/agents/accountability/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { accountabilityInputSchema } from "@/lib/agents/schemas";
import { runAccountability } from "@/lib/agents/runner";
import { saveCheckIn } from "@/lib/fallbackStore";

export async function POST(request: Request) {
const body = await request.json();
const parsed = accountabilityInputSchema.safeParse(body);

if (!parsed.success) {
return NextResponse.json({ error: "Invalid input", issues: parsed.error.flatten() }, { status: 400 });
}

const result = await runAccountability(parsed.data);
saveCheckIn(result);

return NextResponse.json(result);
}
18 changes: 18 additions & 0 deletions app/api/agents/intake/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { intakeInputSchema } from "@/lib/agents/schemas";
import { runIntake } from "@/lib/agents/runner";
import { saveIntake } from "@/lib/fallbackStore";

export async function POST(request: Request) {
const body = await request.json();
const parsed = intakeInputSchema.safeParse(body);

if (!parsed.success) {
return NextResponse.json({ error: "Invalid input", issues: parsed.error.flatten() }, { status: 400 });
}

const result = await runIntake(parsed.data);
saveIntake(result);

return NextResponse.json(result);
}
18 changes: 18 additions & 0 deletions app/api/agents/planner/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { plannerInputSchema } from "@/lib/agents/schemas";
import { runPlanner } from "@/lib/agents/runner";
import { savePlan } from "@/lib/fallbackStore";

export async function POST(request: Request) {
const body = await request.json();
const parsed = plannerInputSchema.safeParse(body);

if (!parsed.success) {
return NextResponse.json({ error: "Invalid input", issues: parsed.error.flatten() }, { status: 400 });
}

const result = await runPlanner(parsed.data);
savePlan(result);

return NextResponse.json(result);
}
18 changes: 18 additions & 0 deletions app/api/agents/reflection/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { reflectionInputSchema } from "@/lib/agents/schemas";
import { runReflection } from "@/lib/agents/runner";
import { saveReflection } from "@/lib/fallbackStore";

export async function POST(request: Request) {
const body = await request.json();
const parsed = reflectionInputSchema.safeParse(body);

if (!parsed.success) {
return NextResponse.json({ error: "Invalid input", issues: parsed.error.flatten() }, { status: 400 });
}

const result = await runReflection(parsed.data);
saveReflection(result);

return NextResponse.json(result);
}
33 changes: 33 additions & 0 deletions app/app/goal/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import AccountabilityCheckIn from "@/components/AccountabilityCheckIn";
import PageHeader from "@/components/PageHeader";
import { getGoal } from "@/lib/fallbackStore";

const defaultMilestones = [
import PageHeader from "@/components/PageHeader";

const milestones = [
Expand Down Expand Up @@ -26,12 +31,22 @@ const checkIns = [
}
];

export default function GoalDetailPage({ params }: { params: { id: string } }) {
const record = getGoal(params.id);
const milestones = record?.plan
? record.plan.weeklyMilestones.map((item, index) => ({
label: `Week ${index + 1}`,
detail: item
}))
: defaultMilestones;

export default function GoalDetailPage() {
return (
<div className="min-h-screen bg-slate-50">
<div className="mx-auto max-w-6xl space-y-10 px-6 py-12">
<PageHeader
eyebrow="Goal"
title={record?.intake.goal ?? "30-day fitness sprint"}
title="30-day fitness sprint"
description="The planner agent mapped a 4-week path with progress ring tracking and daily check-ins."
ctaLabel="Log today"
Expand All @@ -51,6 +66,24 @@ export default function GoalDetailPage() {
</div>
</div>

<div className="space-y-6">
<div className="card space-y-4">
<h3 className="text-lg font-semibold text-commit-slate">Agent check-ins</h3>
<p className="text-sm text-slate-600">
Scheduled prompts keep you aligned with the plan and help the accountability agent
adapt on the fly.
</p>
<div className="space-y-3">
{checkIns.map((checkIn) => (
<div key={checkIn.label} className="rounded-2xl bg-commit-blue/5 p-4">
<p className="text-sm font-semibold text-commit-blue">{checkIn.label}</p>
<p className="text-sm text-slate-600">{checkIn.detail}</p>
</div>
))}
</div>
</div>

<AccountabilityCheckIn goalId={params.id} />
<div className="card space-y-4">
<h3 className="text-lg font-semibold text-commit-slate">Agent check-ins</h3>
<p className="text-sm text-slate-600">
Expand Down
3 changes: 3 additions & 0 deletions app/app/onboarding/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import PageHeader from "@/components/PageHeader";
import OnboardingForm from "@/components/OnboardingForm";
import Link from "next/link";
import PageHeader from "@/components/PageHeader";

Expand Down Expand Up @@ -37,6 +39,7 @@ export default function OnboardingPage() {
))}
</div>

<OnboardingForm />
<div className="card space-y-4">
<h3 className="text-lg font-semibold text-commit-slate">Resolution intake</h3>
<p className="text-sm text-slate-600">
Expand Down
33 changes: 33 additions & 0 deletions app/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Link from "next/link";
import PageHeader from "@/components/PageHeader";
import { getLatestGoal } from "@/lib/fallbackStore";

const defaultHighlights = [

const highlights = [
{
Expand All @@ -26,11 +29,37 @@ const tasks = [
];

export default function DashboardPage() {
const latestGoal = getLatestGoal();
const plan = latestGoal?.plan;
const highlights = latestGoal
? [
{
label: "Active goal",
value: latestGoal.intake.goal,
detail: latestGoal.intake.successMetric
},
{
label: "Next check-in",
value: "Tonight",
detail: "Accountability agent"
},
{
label: "Weekly focus",
value: plan?.focus ?? "Planner agent",
detail: plan?.weeklyMilestones?.[0] ?? "Weekly milestones in progress"
}
]
: defaultHighlights;

return (
<div className="min-h-screen bg-slate-50">
<div className="mx-auto max-w-6xl space-y-10 px-6 py-12">
<PageHeader
eyebrow="Dashboard"
title={latestGoal ? "Welcome back" : "Welcome back, Victory"}
description="Your AI agents are tracking momentum, celebrating wins, and adjusting your plan in real time."
ctaLabel={latestGoal ? "View goal" : "Start check-in"}
ctaHref={latestGoal ? `/app/goal/${latestGoal.intake.goalId}` : "/app/goal/commit-30"}
title="Welcome back, Victory"
description="Your AI agents are tracking momentum, celebrating wins, and adjusting your plan in real time."
ctaLabel="Start check-in"
Expand All @@ -53,6 +82,9 @@ export default function DashboardPage() {
<div className="card space-y-4">
<h2 className="text-xl font-semibold text-commit-slate">Today’s momentum</h2>
<p className="text-slate-600">
{latestGoal
? "Your planner agent is ready to adjust tasks if your schedule shifts."
: "The accountability agent noticed you skipped Tuesday. Want to reschedule a shorter session this evening?"}
The accountability agent noticed you skipped Tuesday. Want to reschedule a shorter
session this evening?
</p>
Expand All @@ -72,6 +104,7 @@ export default function DashboardPage() {
<div className="card space-y-4">
<h3 className="text-lg font-semibold text-commit-slate">Tasks to close today</h3>
<ul className="space-y-3 text-sm text-slate-600">
{(plan?.dailyCommitments ?? tasks).map((task) => (
{tasks.map((task) => (
<li key={task} className="flex items-start gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-commit-amber" />
Expand Down
19 changes: 19 additions & 0 deletions app/app/review/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import PageHeader from "@/components/PageHeader";
import ReflectionPanel from "@/components/ReflectionPanel";
import { getLatestGoal } from "@/lib/fallbackStore";

const metrics = [
{ label: "Check-ins", value: "9 / 10" },
Expand All @@ -13,6 +15,9 @@ const reflections = [
];

export default function ReviewPage() {
const latestGoal = getLatestGoal();
const goalId = latestGoal?.intake.goalId ?? "commit-30";

return (
<div className="min-h-screen bg-white">
<div className="mx-auto max-w-6xl space-y-10 px-6 py-12">
Expand All @@ -35,6 +40,20 @@ export default function ReviewPage() {
))}
</section>

<section className="grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
<div className="card space-y-4">
<h2 className="text-xl font-semibold text-commit-slate">What the agent noticed</h2>
<ul className="space-y-3 text-slate-600">
{reflections.map((item) => (
<li key={item} className="flex gap-3">
<span className="mt-1 h-2 w-2 rounded-full bg-commit-amber" />
<span>{item}</span>
</li>
))}
</ul>
</div>

<ReflectionPanel goalId={goalId} />
<section className="card space-y-4">
<h2 className="text-xl font-semibold text-commit-slate">What the agent noticed</h2>
<ul className="space-y-3 text-slate-600">
Expand Down
93 changes: 93 additions & 0 deletions components/AccountabilityCheckIn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use client";

import { useState } from "react";

interface AccountabilityResponse {
status: string;
recommendation: string;
nextAction: string;
}

export default function AccountabilityCheckIn({ goalId }: { goalId: string }) {
const [note, setNote] = useState("");
const [mood, setMood] = useState("steady");
const [completed, setCompleted] = useState(1);
const [result, setResult] = useState<AccountabilityResponse | null>(null);
const [loading, setLoading] = useState(false);

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoading(true);

const response = await fetch("/api/agents/accountability", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
goalId,
checkInNote: note,
mood,
completedTasks: completed
})
});

const data = (await response.json()) as AccountabilityResponse;
setResult(data);
setLoading(false);
};

return (
<div className="card space-y-4">
<h3 className="text-lg font-semibold text-commit-slate">Check-in with your agent</h3>
<form className="space-y-3" onSubmit={handleSubmit}>
<textarea
className="w-full rounded-xl border border-slate-200 px-4 py-3 text-sm"
rows={3}
placeholder="Share today’s progress or blockers"
value={note}
onChange={(event) => setNote(event.target.value)}
required
/>
<div className="grid gap-3 md:grid-cols-2">
<label className="text-sm text-slate-600">
Mood
<select
className="mt-2 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm"
value={mood}
onChange={(event) => setMood(event.target.value)}
>
<option value="low">Low</option>
<option value="steady">Steady</option>
<option value="high">High</option>
</select>
</label>
<label className="text-sm text-slate-600">
Completed tasks
<input
type="number"
min={0}
className="mt-2 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm"
value={completed}
onChange={(event) => setCompleted(Number(event.target.value))}
/>
</label>
</div>
<button
type="submit"
disabled={loading}
className="rounded-full bg-commit-blue px-4 py-2 text-sm font-semibold text-white"
>
{loading ? "Sending check-in..." : "Send check-in"}
</button>
</form>

{result ? (
<div className="rounded-2xl border border-slate-200 p-4 text-sm text-slate-600">
<p className="text-xs font-semibold uppercase tracking-widest text-commit-amber">Status</p>
<p className="mt-2 text-base font-semibold text-commit-slate">{result.status}</p>
<p className="mt-2">Recommendation: {result.recommendation}</p>
<p className="mt-2">Next action: {result.nextAction}</p>
</div>
) : null}
</div>
);
}
Loading