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
+
+
+ {result ? (
+
+
Status
+
{result.status}
+
Recommendation: {result.recommendation}
+
Next action: {result.nextAction}
+
+ ) : null}
+
+ );
+}
diff --git a/components/OnboardingForm.tsx b/components/OnboardingForm.tsx
new file mode 100644
index 0000000..ec0021f
--- /dev/null
+++ b/components/OnboardingForm.tsx
@@ -0,0 +1,135 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+
+interface IntakeResponse {
+ goalId: string;
+ goal: string;
+ successMetric: string;
+ weeklyCadence: string;
+ initialMilestone: string;
+}
+
+interface PlannerResponse {
+ goalId: string;
+ focus: string;
+ weeklyMilestones: string[];
+ dailyCommitments: string[];
+}
+
+export default function OnboardingForm() {
+ const router = useRouter();
+ const [resolution, setResolution] = useState("");
+ const [motivation, setMotivation] = useState("");
+ const [timeframeWeeks, setTimeframeWeeks] = useState(4);
+ const [constraints, setConstraints] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [result, setResult] = useState(null);
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ setLoading(true);
+
+ const intakePayload = {
+ resolution,
+ motivation,
+ timeframeWeeks: Number(timeframeWeeks),
+ constraints: constraints
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean)
+ };
+
+ const intakeRes = await fetch("/api/agents/intake", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(intakePayload)
+ });
+
+ const intakeData = (await intakeRes.json()) as IntakeResponse;
+ setResult(intakeData);
+
+ await fetch("/api/agents/planner", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ goalId: intakeData.goalId,
+ goal: intakeData.goal,
+ timeframeWeeks: Number(timeframeWeeks),
+ successMetric: intakeData.successMetric,
+ constraints: intakePayload.constraints
+ })
+ });
+
+ setLoading(false);
+ router.push("/app");
+ };
+
+ return (
+
+
Resolution intake
+
+
+ Resolution
+ setResolution(event.target.value)}
+ required
+ />
+
+
+ Motivation
+ setMotivation(event.target.value)}
+ required
+ />
+
+
+ Timeframe (weeks)
+ setTimeframeWeeks(Number(event.target.value))}
+ required
+ />
+
+
+ Constraints (comma-separated)
+ setConstraints(event.target.value)}
+ />
+
+
+ {loading ? "Creating your plan..." : "Generate plan"}
+
+
+
+ {result ? (
+
+
+ Intake summary
+
+
{result.goal}
+
Success metric: {result.successMetric}
+
Weekly cadence: {result.weeklyCadence}
+
+ ) : null}
+
+ );
+}
diff --git a/components/ReflectionPanel.tsx b/components/ReflectionPanel.tsx
new file mode 100644
index 0000000..18439c7
--- /dev/null
+++ b/components/ReflectionPanel.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { useState } from "react";
+
+interface ReflectionResponse {
+ summary: string;
+ wins: string[];
+ adjustments: string[];
+}
+
+export default function ReflectionPanel({ goalId }: { goalId: string }) {
+ const [highlight, setHighlight] = useState("");
+ const [blocker, setBlocker] = useState("");
+ 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/reflection", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ goalId,
+ weekHighlights: highlight
+ .split("\n")
+ .map((item) => item.trim())
+ .filter(Boolean),
+ blockers: blocker
+ .split("\n")
+ .map((item) => item.trim())
+ .filter(Boolean)
+ })
+ });
+
+ const data = (await response.json()) as ReflectionResponse;
+ setResult(data);
+ setLoading(false);
+ };
+
+ return (
+
+
Weekly reflection
+
+
+ Week highlights (one per line)
+ setHighlight(event.target.value)}
+ placeholder="Completed all workouts\nHit 5k steps streak"
+ required
+ />
+
+
+ Blockers (optional)
+ setBlocker(event.target.value)}
+ placeholder="Late meetings on Thursday"
+ />
+
+
+ {loading ? "Generating summary..." : "Run reflection"}
+
+
+
+ {result ? (
+
+
Summary
+
{result.summary}
+
+
+ Wins
+
+
+ {result.wins.map((item) => (
+ • {item}
+ ))}
+
+
+
+
+ Adjustments
+
+
+ {result.adjustments.map((item) => (
+ • {item}
+ ))}
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/lib/agents/parser.ts b/lib/agents/parser.ts
new file mode 100644
index 0000000..50ca294
--- /dev/null
+++ b/lib/agents/parser.ts
@@ -0,0 +1,7 @@
+export function safeJsonParse(raw: string, fallback: T): T {
+ try {
+ return JSON.parse(raw) as T;
+ } catch {
+ return fallback;
+ }
+}
diff --git a/lib/agents/prompts.ts b/lib/agents/prompts.ts
new file mode 100644
index 0000000..ea3f0e4
--- /dev/null
+++ b/lib/agents/prompts.ts
@@ -0,0 +1,10 @@
+export const prompts = {
+ intake:
+ "You are the Intake Agent for Commit Coach. Convert the resolution into a clear SMART goal and set the first milestone.",
+ planner:
+ "You are the Planner Agent. Create weekly milestones and daily commitments that align with the SMART goal.",
+ accountability:
+ "You are the Accountability Agent. Analyze the latest check-in and propose a supportive next action.",
+ reflection:
+ "You are the Reflection Agent. Summarize the week, highlight wins, and suggest adjustments."
+};
diff --git a/lib/agents/runner.ts b/lib/agents/runner.ts
new file mode 100644
index 0000000..5b0e858
--- /dev/null
+++ b/lib/agents/runner.ts
@@ -0,0 +1,87 @@
+import { prompts } from "@/lib/agents/prompts";
+import { safeJsonParse } from "@/lib/agents/parser";
+import {
+ accountabilityOutputSchema,
+ intakeOutputSchema,
+ plannerOutputSchema,
+ reflectionOutputSchema,
+ type AccountabilityInput,
+ type IntakeInput,
+ type PlannerInput,
+ type ReflectionInput
+} from "@/lib/agents/schemas";
+import { emitOpikTrace } from "@/lib/opikClient";
+
+const goalSeed = () => `goal_${Math.random().toString(36).slice(2, 8)}`;
+
+export async function runIntake(input: IntakeInput) {
+ const draft = {
+ goalId: goalSeed(),
+ goal: `Build ${input.resolution.toLowerCase()} in ${input.timeframeWeeks} weeks`,
+ successMetric: `Complete ${input.timeframeWeeks * 3} focused sessions`,
+ weeklyCadence: `${Math.min(3, input.timeframeWeeks)} sessions per week`,
+ initialMilestone: "Book the first three sessions in your calendar"
+ };
+ const parsedDraft = safeJsonParse(JSON.stringify(draft), draft);
+ const output = intakeOutputSchema.parse(parsedDraft);
+
+ await emitOpikTrace("intake", { prompt: prompts.intake, input, output });
+ return output;
+}
+
+export async function runPlanner(input: PlannerInput) {
+ const weeklyMilestones = Array.from({ length: Math.min(input.timeframeWeeks, 4) }, (_, i) =>
+ `Week ${i + 1}: ${input.successMetric}`
+ );
+
+ const draft = {
+ goalId: input.goalId,
+ focus: "Consistency and low-friction scheduling",
+ weeklyMilestones,
+ dailyCommitments: [
+ "Morning check-in with your accountability agent",
+ "Complete the highest-impact task of the day",
+ "Evening reflection in under 2 minutes"
+ ]
+ };
+ const parsedDraft = safeJsonParse(JSON.stringify(draft), draft);
+ const output = plannerOutputSchema.parse(parsedDraft);
+
+ await emitOpikTrace("planner", { prompt: prompts.planner, input, output });
+ return output;
+}
+
+export async function runAccountability(input: AccountabilityInput) {
+ const status = input.completedTasks >= 2 ? "On track" : "Needs a reset";
+ const draft = {
+ goalId: input.goalId,
+ status,
+ recommendation:
+ input.mood === "low"
+ ? "Swap to a lighter session and celebrate a small win."
+ : "Keep the plan and protect the next check-in window.",
+ nextAction: "Send a reminder 2 hours before your next commitment."
+ };
+ const parsedDraft = safeJsonParse(JSON.stringify(draft), draft);
+ const output = accountabilityOutputSchema.parse(parsedDraft);
+
+ await emitOpikTrace("accountability", { prompt: prompts.accountability, input, output });
+ return output;
+}
+
+export async function runReflection(input: ReflectionInput) {
+ const draft = {
+ goalId: input.goalId,
+ summary: "You kept momentum through mid-week and recovered quickly after a dip.",
+ wins: input.weekHighlights,
+ adjustments: [
+ "Schedule the hardest task earlier in the day",
+ "Add a midweek accountability ping"
+ ]
+ };
+ const parsedDraft = safeJsonParse(JSON.stringify(draft), draft);
+ const output = reflectionOutputSchema.parse(parsedDraft);
+
+ await emitOpikTrace("reflection", { prompt: prompts.reflection, input, output });
+ return output;
+}
diff --git a/lib/agents/schemas.ts b/lib/agents/schemas.ts
new file mode 100644
index 0000000..87831be
--- /dev/null
+++ b/lib/agents/schemas.ts
@@ -0,0 +1,67 @@
+import { z } from "zod";
+
+export const intakeInputSchema = z.object({
+ resolution: z.string().min(5),
+ timeframeWeeks: z.number().int().min(1).max(52),
+ motivation: z.string().min(3),
+ constraints: z.array(z.string()).default([])
+});
+
+export const intakeOutputSchema = z.object({
+ goalId: z.string(),
+ goal: z.string(),
+ successMetric: z.string(),
+ weeklyCadence: z.string(),
+ initialMilestone: z.string()
+});
+
+export const plannerInputSchema = z.object({
+ goalId: z.string(),
+ goal: z.string(),
+ timeframeWeeks: z.number().int().min(1).max(52),
+ successMetric: z.string(),
+ constraints: z.array(z.string()).default([])
+});
+
+export const plannerOutputSchema = z.object({
+ goalId: z.string(),
+ focus: z.string(),
+ weeklyMilestones: z.array(z.string()).min(1),
+ dailyCommitments: z.array(z.string()).min(1)
+});
+
+export const accountabilityInputSchema = z.object({
+ goalId: z.string(),
+ checkInNote: z.string().min(3),
+ mood: z.enum(["low", "steady", "high"]),
+ completedTasks: z.number().int().min(0)
+});
+
+export const accountabilityOutputSchema = z.object({
+ goalId: z.string(),
+ status: z.string(),
+ recommendation: z.string(),
+ nextAction: z.string()
+});
+
+export const reflectionInputSchema = z.object({
+ goalId: z.string(),
+ weekHighlights: z.array(z.string()).min(1),
+ blockers: z.array(z.string()).default([])
+});
+
+export const reflectionOutputSchema = z.object({
+ goalId: z.string(),
+ summary: z.string(),
+ wins: z.array(z.string()).min(1),
+ adjustments: z.array(z.string()).min(1)
+});
+
+export type IntakeInput = z.infer;
+export type IntakeOutput = z.infer;
+export type PlannerInput = z.infer;
+export type PlannerOutput = z.infer;
+export type AccountabilityInput = z.infer;
+export type AccountabilityOutput = z.infer;
+export type ReflectionInput = z.infer;
+export type ReflectionOutput = z.infer;
diff --git a/lib/fallbackStore.ts b/lib/fallbackStore.ts
new file mode 100644
index 0000000..de550c4
--- /dev/null
+++ b/lib/fallbackStore.ts
@@ -0,0 +1,64 @@
+import type {
+ AccountabilityOutput,
+ IntakeOutput,
+ PlannerOutput,
+ ReflectionOutput
+} from "@/lib/agents/schemas";
+
+interface GoalRecord {
+ intake: IntakeOutput;
+ plan: PlannerOutput | null;
+ checkIns: AccountabilityOutput[];
+ reflections: ReflectionOutput[];
+}
+
+const goalStore = new Map();
+let latestGoalId: string | null = null;
+
+export function saveIntake(result: IntakeOutput) {
+ goalStore.set(result.goalId, {
+ intake: result,
+ plan: null,
+ checkIns: [],
+ reflections: []
+ });
+ latestGoalId = result.goalId;
+}
+
+export function savePlan(result: PlannerOutput) {
+ const record = goalStore.get(result.goalId);
+ if (!record) {
+ return;
+ }
+ record.plan = result;
+ latestGoalId = result.goalId;
+}
+
+export function saveCheckIn(result: AccountabilityOutput) {
+ const record = goalStore.get(result.goalId);
+ if (!record) {
+ return;
+ }
+ record.checkIns.unshift(result);
+ latestGoalId = result.goalId;
+}
+
+export function saveReflection(result: ReflectionOutput) {
+ const record = goalStore.get(result.goalId);
+ if (!record) {
+ return;
+ }
+ record.reflections.unshift(result);
+ latestGoalId = result.goalId;
+}
+
+export function getLatestGoal() {
+ if (!latestGoalId) {
+ return null;
+ }
+ return goalStore.get(latestGoalId) ?? null;
+}
+
+export function getGoal(goalId: string) {
+ return goalStore.get(goalId) ?? null;
+}
diff --git a/lib/opikClient.ts b/lib/opikClient.ts
index e7bb9bc..1b13846 100644
--- a/lib/opikClient.ts
+++ b/lib/opikClient.ts
@@ -1,3 +1,45 @@
+const opikApiKey = process.env.OPIK_API_KEY ?? "";
+const opikProjectId = process.env.OPIK_PROJECT_ID ?? "commit-coach";
+
+let clientPromise: Promise | null = null;
+
+async function getOpikClient() {
+ if (!opikApiKey) {
+ return null;
+ }
+ if (!clientPromise) {
+ clientPromise = import("opik").then((module) => {
+ const OpikClient = module.Opik ?? module.default ?? module;
+ return typeof OpikClient === "function"
+ ? new OpikClient({ apiKey: opikApiKey, projectId: opikProjectId })
+ : OpikClient;
+ });
+ }
+ return clientPromise;
+}
+
+export async function emitOpikTrace(name: string, payload: Record) {
+ const client = await getOpikClient();
+ const trace = {
+ name,
+ projectId: opikProjectId,
+ payload,
+ timestamp: new Date().toISOString()
+ };
+
+ if (client?.trace) {
+ return client.trace(trace);
+ }
+ if (client?.createTrace) {
+ return client.createTrace(trace);
+ }
+ if (client?.log) {
+ return client.log(trace);
+ }
+
+ console.info("[opik:trace]", trace);
+ return trace;
+}
export const opikConfig = {
apiKey: process.env.OPIK_API_KEY ?? "",
projectId: process.env.OPIK_PROJECT_ID ?? "commit-coach"
diff --git a/package.json b/package.json
index 425e49b..e1fb120 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,8 @@
"opik": "^0.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "sharp": "^0.33.2",
+ "zod": "^3.23.8"
"sharp": "^0.33.2"
},
"devDependencies": {