Skip to content

Commit 23b511e

Browse files
committed
Add AI SDK demo
1 parent 8cd881a commit 23b511e

File tree

6 files changed

+270
-0
lines changed

6 files changed

+270
-0
lines changed

references/realtime-streams/src/app/actions.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import { tasks, auth } from "@trigger.dev/sdk";
44
import type { streamsTask } from "@/trigger/streams";
5+
import type { aiChatTask } from "@/trigger/ai-chat";
56
import { redirect } from "next/navigation";
7+
import type { UIMessage } from "ai";
68

79
export async function triggerStreamTask(
810
scenario: string,
@@ -38,3 +40,26 @@ export async function triggerStreamTask(
3840

3941
redirect(path);
4042
}
43+
44+
export async function triggerAIChatTask(messages: UIMessage[]) {
45+
// Trigger the AI chat task
46+
const handle = await tasks.trigger<typeof aiChatTask>(
47+
"ai-chat",
48+
{
49+
messages,
50+
},
51+
{},
52+
{
53+
clientConfig: {
54+
future: {
55+
unstable_v2RealtimeStreams: true,
56+
},
57+
},
58+
}
59+
);
60+
61+
console.log("Triggered AI chat run:", handle.id);
62+
63+
// Redirect to chat page
64+
redirect(`/chat/${handle.id}?accessToken=${handle.publicAccessToken}`);
65+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { AIChat } from "@/components/ai-chat";
2+
import Link from "next/link";
3+
4+
export default function ChatPage({
5+
params,
6+
searchParams,
7+
}: {
8+
params: { runId: string };
9+
searchParams: { accessToken?: string };
10+
}) {
11+
const { runId } = params;
12+
const accessToken = searchParams.accessToken;
13+
14+
if (!accessToken) {
15+
return (
16+
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
17+
<main className="flex flex-col gap-8 row-start-2 items-center">
18+
<h1 className="text-2xl font-bold text-red-600">Missing Access Token</h1>
19+
<p className="text-gray-600">This page requires an access token to view the stream.</p>
20+
<Link href="/" className="text-blue-600 hover:underline">
21+
Go back home
22+
</Link>
23+
</main>
24+
</div>
25+
);
26+
}
27+
28+
return (
29+
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
30+
<main className="flex flex-col gap-8 row-start-2 items-start w-full max-w-4xl">
31+
<div className="flex items-center justify-between w-full">
32+
<h1 className="text-2xl font-bold">AI Chat Stream: {runId}</h1>
33+
<Link
34+
href="/"
35+
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors"
36+
>
37+
← Back to Home
38+
</Link>
39+
</div>
40+
41+
<div className="w-full bg-purple-50 p-4 rounded-lg">
42+
<p className="text-sm text-purple-900 mb-2">
43+
🤖 <strong>AI SDK v5:</strong> This stream uses AI SDK&apos;s streamText with
44+
toUIMessageStream()
45+
</p>
46+
<p className="text-xs text-purple-700">
47+
Try refreshing to test stream reconnection - it should resume where it left off.
48+
</p>
49+
</div>
50+
51+
<div className="w-full border border-gray-200 rounded-lg p-6 bg-white">
52+
<AIChat accessToken={accessToken} runId={runId} />
53+
</div>
54+
</main>
55+
</div>
56+
);
57+
}

references/realtime-streams/src/app/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TriggerButton } from "@/components/trigger-button";
2+
import { AIChatButton } from "@/components/ai-chat-button";
23

34
export default function Home() {
45
return (
@@ -10,6 +11,14 @@ export default function Home() {
1011
refresh the page to test stream reconnection.
1112
</p>
1213

14+
<div className="mt-8 pt-8 border-t border-gray-300 w-full">
15+
<h2 className="text-xl font-semibold mb-4">AI Chat Stream (AI SDK v5)</h2>
16+
<p className="text-sm text-gray-600 mb-4">
17+
Test AI SDK v5&apos;s streamText with toUIMessageStream()
18+
</p>
19+
<AIChatButton />
20+
</div>
21+
1322
<div className="flex flex-col gap-4">
1423
<TriggerButton scenario="markdown">Markdown Stream</TriggerButton>
1524
<TriggerButton scenario="continuous">Continuous Stream</TriggerButton>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import { triggerAIChatTask } from "@/app/actions";
4+
import { useTransition } from "react";
5+
import type { UIMessage } from "ai";
6+
7+
export function AIChatButton() {
8+
const [isPending, startTransition] = useTransition();
9+
10+
function handleClick() {
11+
startTransition(async () => {
12+
// Create a sample conversation to trigger
13+
const messages: UIMessage[] = [
14+
{
15+
id: "1",
16+
role: "user",
17+
parts: [
18+
{
19+
type: "text",
20+
text: "Write a detailed explanation of how streaming works in modern web applications, including the benefits and common use cases.",
21+
},
22+
],
23+
},
24+
];
25+
26+
await triggerAIChatTask(messages);
27+
});
28+
}
29+
30+
return (
31+
<button
32+
onClick={handleClick}
33+
disabled={isPending}
34+
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
35+
>
36+
{isPending ? "Starting AI Chat..." : "🤖 Start AI Chat Stream"}
37+
</button>
38+
);
39+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"use client";
2+
3+
import { useRealtimeStream } from "@trigger.dev/react-hooks";
4+
import type { UIMessage, UIMessageChunk } from "ai";
5+
import { Streamdown } from "streamdown";
6+
7+
export function AIChat({ accessToken, runId }: { accessToken: string; runId: string }) {
8+
const { parts, error } = useRealtimeStream<UIMessageChunk>(runId, "chat", {
9+
accessToken,
10+
baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL,
11+
timeoutInSeconds: 600,
12+
});
13+
14+
if (error) return <div className="text-red-600 font-semibold">Error: {error.message}</div>;
15+
16+
if (!parts) return <div className="text-gray-600">Loading...</div>;
17+
18+
// Compute derived state directly from parts
19+
let accumulatedText = "";
20+
let currentId: string | null = null;
21+
let isComplete = false;
22+
23+
for (const chunk of parts) {
24+
switch (chunk.type) {
25+
case "text-start":
26+
if (!currentId) {
27+
currentId = chunk.id;
28+
}
29+
break;
30+
case "text-delta":
31+
accumulatedText += chunk.delta;
32+
break;
33+
case "text-end":
34+
isComplete = true;
35+
break;
36+
case "error":
37+
console.error("Stream error:", chunk.errorText);
38+
break;
39+
}
40+
}
41+
42+
// Determine what to render
43+
const messages: UIMessage[] = [];
44+
let currentText = "";
45+
let currentMessageId: string | null = null;
46+
let currentRole: "assistant" | null = null;
47+
48+
if (isComplete && currentId && accumulatedText) {
49+
// Streaming is complete, show as completed message
50+
messages.push({
51+
id: currentId,
52+
role: "assistant",
53+
parts: [{ type: "text", text: accumulatedText }],
54+
});
55+
} else if (currentId) {
56+
// Still streaming
57+
currentText = accumulatedText;
58+
currentMessageId = currentId;
59+
currentRole = "assistant";
60+
}
61+
62+
return (
63+
<div className="space-y-6">
64+
<div className="text-sm font-medium text-gray-700 mb-4">
65+
<span className="font-semibold">Run:</span> {runId}
66+
</div>
67+
68+
{/* Render completed messages */}
69+
{messages.map((message) => (
70+
<div key={message.id} className="p-4 rounded-lg bg-gray-50 border-l-4 border-purple-500">
71+
<div className="text-xs font-semibold text-gray-500 uppercase mb-2">{message.role}</div>
72+
<div className="prose prose-sm max-w-none text-gray-900">
73+
{message.parts.map((part, idx) =>
74+
part.type === "text" ? (
75+
<Streamdown key={idx} isAnimating={false}>
76+
{part.text}
77+
</Streamdown>
78+
) : null
79+
)}
80+
</div>
81+
</div>
82+
))}
83+
84+
{/* Render current streaming message */}
85+
{currentMessageId && currentRole && (
86+
<div className="p-4 rounded-lg bg-gray-50 border-l-4 border-purple-500">
87+
<div className="text-xs font-semibold text-gray-500 uppercase mb-2">
88+
{currentRole} <span className="text-purple-600">(streaming...)</span>
89+
</div>
90+
<div className="prose prose-sm max-w-none text-gray-900">
91+
<Streamdown isAnimating={true}>{currentText}</Streamdown>
92+
</div>
93+
</div>
94+
)}
95+
</div>
96+
);
97+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { logger, streams, task } from "@trigger.dev/sdk";
2+
import { openai } from "@ai-sdk/openai";
3+
import { convertToModelMessages, streamText, UIMessage, UIMessageChunk } from "ai";
4+
5+
export type AI_STREAMS = {
6+
chat: UIMessageChunk;
7+
};
8+
9+
export type AIChatPayload = {
10+
messages: UIMessage[];
11+
};
12+
13+
export const aiChatTask = task({
14+
id: "ai-chat",
15+
run: async (payload: AIChatPayload) => {
16+
logger.info("Starting AI chat stream", {
17+
messageCount: payload.messages.length,
18+
});
19+
20+
// Stream text from OpenAI
21+
const result = streamText({
22+
model: openai("gpt-4o"),
23+
system: "You are a helpful assistant.",
24+
messages: convertToModelMessages(payload.messages),
25+
});
26+
27+
// Get the UI message stream
28+
const uiMessageStream = result.toUIMessageStream();
29+
30+
// Append the stream to metadata
31+
const { waitUntilComplete } = await streams.append("chat", uiMessageStream);
32+
33+
// Wait for the stream to complete
34+
await waitUntilComplete();
35+
36+
logger.info("AI chat stream completed");
37+
38+
return {
39+
message: "AI chat stream completed successfully",
40+
messageCount: payload.messages.length,
41+
};
42+
},
43+
});

0 commit comments

Comments
 (0)