diff --git a/README.md b/README.md index ac04c5d..4b5262e 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/app/api/agents/accountability/route.ts b/app/api/agents/accountability/route.ts new file mode 100644 index 0000000..0ffe9e2 --- /dev/null +++ b/app/api/agents/accountability/route.ts @@ -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); +} diff --git a/app/api/agents/intake/route.ts b/app/api/agents/intake/route.ts new file mode 100644 index 0000000..786b9fe --- /dev/null +++ b/app/api/agents/intake/route.ts @@ -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); +} diff --git a/app/api/agents/planner/route.ts b/app/api/agents/planner/route.ts new file mode 100644 index 0000000..36674da --- /dev/null +++ b/app/api/agents/planner/route.ts @@ -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); +} diff --git a/app/api/agents/reflection/route.ts b/app/api/agents/reflection/route.ts new file mode 100644 index 0000000..a769575 --- /dev/null +++ b/app/api/agents/reflection/route.ts @@ -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); +} diff --git a/app/app/goal/[id]/page.tsx b/app/app/goal/[id]/page.tsx index d0bc420..aad22f4 100644 --- a/app/app/goal/[id]/page.tsx +++ b/app/app/goal/[id]/page.tsx @@ -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 = [ @@ -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 (
+
+
+

Agent check-ins

+

+ Scheduled prompts keep you aligned with the plan and help the accountability agent + adapt on the fly. +

+
+ {checkIns.map((checkIn) => ( +
+

{checkIn.label}

+

{checkIn.detail}

+
+ ))} +
+
+ +

Agent check-ins

diff --git a/app/app/onboarding/page.tsx b/app/app/onboarding/page.tsx index 6e6f431..4134fe3 100644 --- a/app/app/onboarding/page.tsx +++ b/app/app/onboarding/page.tsx @@ -1,3 +1,5 @@ +import PageHeader from "@/components/PageHeader"; +import OnboardingForm from "@/components/OnboardingForm"; import Link from "next/link"; import PageHeader from "@/components/PageHeader"; @@ -37,6 +39,7 @@ export default function OnboardingPage() { ))}

+

Resolution intake

diff --git a/app/app/page.tsx b/app/app/page.tsx index 3c4ed00..ce1f73e 100644 --- a/app/app/page.tsx +++ b/app/app/page.tsx @@ -1,5 +1,8 @@ import Link from "next/link"; import PageHeader from "@/components/PageHeader"; +import { getLatestGoal } from "@/lib/fallbackStore"; + +const defaultHighlights = [ const highlights = [ { @@ -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 (

Today’s momentum

+ {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?

@@ -72,6 +104,7 @@ export default function DashboardPage() {

Tasks to close today

    + {(plan?.dailyCommitments ?? tasks).map((task) => ( {tasks.map((task) => (
  • diff --git a/app/app/review/page.tsx b/app/app/review/page.tsx index 8e37859..85abaf1 100644 --- a/app/app/review/page.tsx +++ b/app/app/review/page.tsx @@ -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" }, @@ -13,6 +15,9 @@ const reflections = [ ]; export default function ReviewPage() { + const latestGoal = getLatestGoal(); + const goalId = latestGoal?.intake.goalId ?? "commit-30"; + return (
    @@ -35,6 +40,20 @@ export default function ReviewPage() { ))} +
    +
    +

    What the agent noticed

    +
      + {reflections.map((item) => ( +
    • + + {item} +
    • + ))} +
    +
    + +

    What the agent noticed

      diff --git a/components/AccountabilityCheckIn.tsx b/components/AccountabilityCheckIn.tsx new file mode 100644 index 0000000..2989893 --- /dev/null +++ b/components/AccountabilityCheckIn.tsx @@ -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(null); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + 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 ( +
      +

      Check-in with your agent

      +
      +