This commit is contained in:
2026-03-10 20:43:02 +01:00
parent 10b3f989c8
commit 34dc53a8e9
11 changed files with 564 additions and 0 deletions

97
src/app/api/chat/route.ts Normal file
View File

@@ -0,0 +1,97 @@
import { auth } from '@clerk/nextjs/server'
import { createOpenAI } from '@ai-sdk/openai'
import { streamText, tool, convertToModelMessages, stepCountIs, type UIMessage } from 'ai'
import { z } from 'zod'
import { eq, and } from 'drizzle-orm'
import { env } from '~/env'
import { db } from '~/server/db'
import { chatSession, chatMessage } from '~/server/dbschema/schema'
import { scheduleMeeting } from '~/app/actions/scheduleMeeting'
const openai = createOpenAI({ apiKey: env.OPENAI_API_KEY })
export async function POST(req: Request) {
const { userId } = await auth()
if (!userId) return new Response('Unauthorized', { status: 401 })
const { messages, sessionId } = (await req.json()) as {
messages: UIMessage[]
sessionId: string
}
// Verify this session belongs to the authenticated user
const session = await db
.select()
.from(chatSession)
.where(and(eq(chatSession.id, sessionId), eq(chatSession.userId, userId)))
.limit(1)
.then((r) => r[0])
if (!session) return new Response('Session not found', { status: 404 })
// Save the latest user message
const lastMessage = messages[messages.length - 1]
if (lastMessage?.role === 'user') {
const content = lastMessage.parts
.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
.map((p) => p.text)
.join('')
if (content) {
await db.insert(chatMessage).values({ sessionId, role: 'user', content })
}
}
const result = streamText({
model: openai('gpt-4o'),
system: `You are an AI recruiter assistant on Gregor Lohaus's personal portfolio website.
Your role is to help visitors learn about Gregor's background, skills, and experience, and to schedule meetings.
About Gregor:
- Fullstack developer specialising in TypeScript, React, Next.js, and modern web technologies
- Experienced with Drizzle ORM, tRPC, PostgreSQL, server components, and the T3 stack
- Also experienced with React Native, Expo, Java/Spring, gRPC, AWS, and Linux/Debian server administration
- Open to new opportunities and collaboration
Be professional, friendly, and concise. When a visitor wants to schedule a meeting, collect the necessary details (preferred date/time, duration, and optionally their name/email) and use the scheduleMeeting tool.`,
messages: await convertToModelMessages(messages),
tools: {
scheduleMeeting: tool({
description: 'Schedule a meeting with Gregor Lohaus and add it to his Google Calendar',
inputSchema: z.object({
title: z.string().describe('Meeting title'),
description: z.string().describe('Meeting description / agenda'),
dateTime: z
.string()
.describe(
'ISO 8601 datetime for the meeting start, e.g. 2025-04-01T10:00:00',
),
durationMinutes: z
.number()
.int()
.min(15)
.max(120)
.describe('Duration of the meeting in minutes'),
attendeeEmail: z
.string()
.email()
.optional()
.describe('Email of the visitor to invite (if provided)'),
attendeeName: z.string().optional().describe('Name of the visitor'),
}),
execute: async (input) => scheduleMeeting({ ...input, userId }),
}),
},
stopWhen: stepCountIs(5),
onFinish: async ({ text, finishReason }) => {
if (text && finishReason === 'stop') {
await db.insert(chatMessage).values({
sessionId,
role: 'assistant',
content: text,
})
}
},
})
return result.toUIMessageStreamResponse()
}