Skip to content

Commit 993ed9b

Browse files
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>
1 parent 876454c commit 993ed9b

File tree

10 files changed

+215
-0
lines changed

10 files changed

+215
-0
lines changed

references/ai-chat/next.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { NextConfig } from "next";
2+
3+
const nextConfig: NextConfig = {};
4+
5+
export default nextConfig;

references/ai-chat/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "references-ai-chat",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev --turbopack",
7+
"build": "next build",
8+
"start": "next start",
9+
"dev:trigger": "trigger dev"
10+
},
11+
"dependencies": {
12+
"@ai-sdk/openai": "^2.0.0",
13+
"@ai-sdk/react": "^2.0.0",
14+
"@trigger.dev/sdk": "workspace:*",
15+
"ai": "^6.0.0",
16+
"next": "15.3.3",
17+
"react": "^19.0.0",
18+
"react-dom": "^19.0.0"
19+
},
20+
"devDependencies": {
21+
"@tailwindcss/postcss": "^4",
22+
"@trigger.dev/build": "workspace:*",
23+
"@types/node": "^22",
24+
"@types/react": "^19",
25+
"@types/react-dom": "^19",
26+
"tailwindcss": "^4",
27+
"trigger.dev": "workspace:*",
28+
"typescript": "^5"
29+
}
30+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @type {import('postcss-load-config').Config} */
2+
const config = {
3+
plugins: {
4+
"@tailwindcss/postcss": {},
5+
},
6+
};
7+
8+
export default config;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "tailwindcss";
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Metadata } from "next";
2+
import "./globals.css";
3+
4+
export const metadata: Metadata = {
5+
title: "AI Chat — Trigger.dev",
6+
description: "AI SDK useChat powered by Trigger.dev durable tasks",
7+
};
8+
9+
export default function RootLayout({ children }: { children: React.ReactNode }) {
10+
return (
11+
<html lang="en">
12+
<body className="bg-gray-50 text-gray-900 antialiased">{children}</body>
13+
</html>
14+
);
15+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { auth } from "@trigger.dev/sdk";
2+
import { Chat } from "@/components/chat";
3+
4+
export default async function Home() {
5+
const accessToken = await auth.createTriggerPublicToken("ai-chat");
6+
7+
return (
8+
<main className="flex min-h-screen flex-col items-center justify-center p-4">
9+
<div className="w-full max-w-2xl">
10+
<h1 className="mb-6 text-center text-2xl font-semibold">
11+
AI Chat <span className="text-gray-400">— powered by Trigger.dev</span>
12+
</h1>
13+
<Chat accessToken={accessToken} />
14+
</div>
15+
</main>
16+
);
17+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"use client";
2+
3+
import { useChat } from "@ai-sdk/react";
4+
import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
5+
import { useState } from "react";
6+
7+
export function Chat({ accessToken }: { accessToken: string }) {
8+
const [input, setInput] = useState("");
9+
10+
const { messages, sendMessage, status, error } = useChat({
11+
transport: new TriggerChatTransport({
12+
task: "ai-chat",
13+
accessToken,
14+
baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL,
15+
}),
16+
});
17+
18+
function handleSubmit(e: React.FormEvent) {
19+
e.preventDefault();
20+
if (!input.trim() || status === "streaming") return;
21+
22+
sendMessage({ text: input });
23+
setInput("");
24+
}
25+
26+
return (
27+
<div className="flex flex-col rounded-xl border border-gray-200 bg-white shadow-sm">
28+
{/* Messages */}
29+
<div className="flex-1 space-y-4 overflow-y-auto p-4" style={{ maxHeight: "60vh" }}>
30+
{messages.length === 0 && (
31+
<p className="text-center text-sm text-gray-400">Send a message to start chatting.</p>
32+
)}
33+
34+
{messages.map((message) => (
35+
<div
36+
key={message.id}
37+
className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
38+
>
39+
<div
40+
className={`max-w-[80%] rounded-lg px-4 py-2 text-sm ${
41+
message.role === "user"
42+
? "bg-blue-600 text-white"
43+
: "bg-gray-100 text-gray-900"
44+
}`}
45+
>
46+
{message.parts.map((part, i) => {
47+
if (part.type === "text") {
48+
return <span key={i}>{part.text}</span>;
49+
}
50+
return null;
51+
})}
52+
</div>
53+
</div>
54+
))}
55+
56+
{status === "streaming" && (
57+
<div className="flex justify-start">
58+
<div className="rounded-lg bg-gray-100 px-4 py-2 text-sm text-gray-400">
59+
Thinking…
60+
</div>
61+
</div>
62+
)}
63+
</div>
64+
65+
{/* Error */}
66+
{error && (
67+
<div className="border-t border-red-100 bg-red-50 px-4 py-2 text-sm text-red-600">
68+
{error.message}
69+
</div>
70+
)}
71+
72+
{/* Input */}
73+
<form onSubmit={handleSubmit} className="flex gap-2 border-t border-gray-200 p-4">
74+
<input
75+
type="text"
76+
value={input}
77+
onChange={(e) => setInput(e.target.value)}
78+
placeholder="Type a message…"
79+
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"
80+
/>
81+
<button
82+
type="submit"
83+
disabled={!input.trim() || status === "streaming"}
84+
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
85+
>
86+
Send
87+
</button>
88+
</form>
89+
</div>
90+
);
91+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { chatTask } from "@trigger.dev/sdk/ai";
2+
import { streamText, convertToModelMessages } from "ai";
3+
import { openai } from "@ai-sdk/openai";
4+
5+
export const chat = chatTask({
6+
id: "ai-chat",
7+
run: async ({ messages }) => {
8+
return streamText({
9+
model: openai("gpt-4o-mini"),
10+
system: "You are a helpful assistant. Be concise and friendly.",
11+
messages: convertToModelMessages(messages),
12+
});
13+
},
14+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from "@trigger.dev/sdk";
2+
3+
export default defineConfig({
4+
project: process.env.TRIGGER_PROJECT_REF!,
5+
dirs: ["./src/trigger"],
6+
maxDuration: 300,
7+
});

references/ai-chat/tsconfig.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2017",
4+
"lib": ["dom", "dom.iterable", "esnext"],
5+
"allowJs": true,
6+
"skipLibCheck": true,
7+
"strict": true,
8+
"noEmit": true,
9+
"esModuleInterop": true,
10+
"module": "esnext",
11+
"moduleResolution": "bundler",
12+
"resolveJsonModule": true,
13+
"isolatedModules": true,
14+
"jsx": "preserve",
15+
"incremental": true,
16+
"plugins": [
17+
{
18+
"name": "next"
19+
}
20+
],
21+
"paths": {
22+
"@/*": ["./src/*"]
23+
}
24+
},
25+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26+
"exclude": ["node_modules"]
27+
}

0 commit comments

Comments
 (0)