diff --git a/references/ai-chat/next.config.ts b/references/ai-chat/next.config.ts new file mode 100644 index 000000000..cb651cdc0 --- /dev/null +++ b/references/ai-chat/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/references/ai-chat/package.json b/references/ai-chat/package.json new file mode 100644 index 000000000..228a09015 --- /dev/null +++ b/references/ai-chat/package.json @@ -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" + } +} diff --git a/references/ai-chat/postcss.config.mjs b/references/ai-chat/postcss.config.mjs new file mode 100644 index 000000000..79bcf135d --- /dev/null +++ b/references/ai-chat/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/references/ai-chat/src/app/globals.css b/references/ai-chat/src/app/globals.css new file mode 100644 index 000000000..f1d8c73cd --- /dev/null +++ b/references/ai-chat/src/app/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/references/ai-chat/src/app/layout.tsx b/references/ai-chat/src/app/layout.tsx new file mode 100644 index 000000000..f50702858 --- /dev/null +++ b/references/ai-chat/src/app/layout.tsx @@ -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 ( + + {children} + + ); +} diff --git a/references/ai-chat/src/app/page.tsx b/references/ai-chat/src/app/page.tsx new file mode 100644 index 000000000..16f01282c --- /dev/null +++ b/references/ai-chat/src/app/page.tsx @@ -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 ( +
+
+

+ AI Chat — powered by Trigger.dev +

+ +
+
+ ); +} diff --git a/references/ai-chat/src/components/chat.tsx b/references/ai-chat/src/components/chat.tsx new file mode 100644 index 000000000..34c68d8ba --- /dev/null +++ b/references/ai-chat/src/components/chat.tsx @@ -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 ( +
+ {/* Messages */} +
+ {messages.length === 0 && ( +

Send a message to start chatting.

+ )} + + {messages.map((message) => ( +
+
+ {message.parts.map((part, i) => { + if (part.type === "text") { + return {part.text}; + } + return null; + })} +
+
+ ))} + + {status === "streaming" && ( +
+
+ Thinking… +
+
+ )} +
+ + {/* Error */} + {error && ( +
+ {error.message} +
+ )} + + {/* Input */} +
+ 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" + /> + +
+
+ ); +} diff --git a/references/ai-chat/src/trigger/chat.ts b/references/ai-chat/src/trigger/chat.ts new file mode 100644 index 000000000..27a400239 --- /dev/null +++ b/references/ai-chat/src/trigger/chat.ts @@ -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), + }); + }, +}); diff --git a/references/ai-chat/trigger.config.ts b/references/ai-chat/trigger.config.ts new file mode 100644 index 000000000..4412bfc93 --- /dev/null +++ b/references/ai-chat/trigger.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "@trigger.dev/sdk"; + +export default defineConfig({ + project: process.env.TRIGGER_PROJECT_REF!, + dirs: ["./src/trigger"], + maxDuration: 300, +}); diff --git a/references/ai-chat/tsconfig.json b/references/ai-chat/tsconfig.json new file mode 100644 index 000000000..c1334095f --- /dev/null +++ b/references/ai-chat/tsconfig.json @@ -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"] +}