feat(reference): add ai-chat Next.js reference project

Minimal example showcasing the new chatTask + TriggerChatTransport APIs:
- Backend: chatTask with streamText auto-pipe (src/trigger/chat.ts)
- Frontend: TriggerChatTransport with useChat (src/components/chat.tsx)
- Token generation via auth.createTriggerPublicToken (src/app/page.tsx)
- Tailwind v4 styling

Co-authored-by: Eric Allam <eric@trigger.dev>
This commit is contained in:
Cursor Agent
2026-02-15 13:24:32 +00:00
committed by Eric Allam
parent f2e5e4ce45
commit ce16c56ed1
10 changed files with 215 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default nextConfig;
+30
View File
@@ -0,0 +1,30 @@
{
"name": "references-ai-chat",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"dev:trigger": "trigger dev"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.0",
"@ai-sdk/react": "^2.0.0",
"@trigger.dev/sdk": "workspace:*",
"ai": "^6.0.0",
"next": "15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@trigger.dev/build": "workspace:*",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"trigger.dev": "workspace:*",
"typescript": "^5"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "AI Chat — Trigger.dev",
description: "AI SDK useChat powered by Trigger.dev durable tasks",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="bg-gray-50 text-gray-900 antialiased">{children}</body>
</html>
);
}
+17
View File
@@ -0,0 +1,17 @@
import { auth } from "@trigger.dev/sdk";
import { Chat } from "@/components/chat";
export default async function Home() {
const accessToken = await auth.createTriggerPublicToken("ai-chat");
return (
<main className="flex min-h-screen flex-col items-center justify-center p-4">
<div className="w-full max-w-2xl">
<h1 className="mb-6 text-center text-2xl font-semibold">
AI Chat <span className="text-gray-400"> powered by Trigger.dev</span>
</h1>
<Chat accessToken={accessToken} />
</div>
</main>
);
}
@@ -0,0 +1,91 @@
"use client";
import { useChat } from "@ai-sdk/react";
import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
import { useState } from "react";
export function Chat({ accessToken }: { accessToken: string }) {
const [input, setInput] = useState("");
const { messages, sendMessage, status, error } = useChat({
transport: new TriggerChatTransport({
task: "ai-chat",
accessToken,
baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL,
}),
});
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || status === "streaming") return;
sendMessage({ text: input });
setInput("");
}
return (
<div className="flex flex-col rounded-xl border border-gray-200 bg-white shadow-sm">
{/* Messages */}
<div className="flex-1 space-y-4 overflow-y-auto p-4" style={{ maxHeight: "60vh" }}>
{messages.length === 0 && (
<p className="text-center text-sm text-gray-400">Send a message to start chatting.</p>
)}
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 text-sm ${
message.role === "user"
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-900"
}`}
>
{message.parts.map((part, i) => {
if (part.type === "text") {
return <span key={i}>{part.text}</span>;
}
return null;
})}
</div>
</div>
))}
{status === "streaming" && (
<div className="flex justify-start">
<div className="rounded-lg bg-gray-100 px-4 py-2 text-sm text-gray-400">
Thinking
</div>
</div>
)}
</div>
{/* Error */}
{error && (
<div className="border-t border-red-100 bg-red-50 px-4 py-2 text-sm text-red-600">
{error.message}
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="flex gap-2 border-t border-gray-200 p-4">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message…"
className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
/>
<button
type="submit"
disabled={!input.trim() || status === "streaming"}
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
Send
</button>
</form>
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { chatTask } from "@trigger.dev/sdk/ai";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export const chat = chatTask({
id: "ai-chat",
run: async ({ messages }) => {
return streamText({
model: openai("gpt-4o-mini"),
system: "You are a helpful assistant. Be concise and friendly.",
messages: convertToModelMessages(messages),
});
},
});
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: process.env.TRIGGER_PROJECT_REF!,
dirs: ["./src/trigger"],
maxDuration: 300,
});
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}