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:
@@ -0,0 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "@trigger.dev/sdk";
|
||||
|
||||
export default defineConfig({
|
||||
project: process.env.TRIGGER_PROJECT_REF!,
|
||||
dirs: ["./src/trigger"],
|
||||
maxDuration: 300,
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user