Next.js×AIの最新の書きっぷり
日々 AI を用いた書きっぷりはどんどん変わっていまして。
基本的にアメリカを中心に情報がアップデートされているため、日本語の情報はかなり後手後手な印象を受けます。
そんな中でいろいろと情報を調べてみたのですが、基本的には AI SDK を追いかけていれば問題ないかなーという印象です。
ということで、今回は Next.js × AI の最新の書きっぷりをざっくり説明していこうと思います。
前提
.env.local
に OPENAI_API_KEY
を設定してください。
@ai-sdk/openai
では環境変数の OPENAI_API_KEY
を自動的に読み込んでくれるようになったみたいです。
Route Handler
app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText, convertToCoreMessages, StreamData } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const data = new StreamData();
data.append({ test: "value" });
const result = await streamText({
model: openai("gpt-3.5-turbo"),
messages: convertToCoreMessages(messages),
onFinish() {
data.close();
},
});
return result.toDataStreamResponse({ data });
}
app/page.tsx
"use client";
import { useChat } from "ai/react";
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, data } = useChat();
return (
<div>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
{messages.map((m) => (
<div key={m.id}>
{m.role === "user" ? "User: " : "AI: "}
{m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
placeholder="Say something..."
onChange={handleInputChange}
/>
</form>
</div>
);
}
useChat
を使用すると自動的に /api/chat
のエンドポイントに POST
を叩くようです。
もちろんエンドポイントは動的に書き換えられるのですが、初期値があるというのもなかなか初見殺しだなと。
Server Action
app/actions.ts
"use server";
import { createStreamableValue } from "ai/rsc";
import { CoreMessage, streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export async function continueConversation(messages: CoreMessage[]) {
const result = await streamText({
model: openai("gpt-3.5-turbo"),
messages,
});
const data = { test: "hello" };
const stream = createStreamableValue(result.textStream);
return { message: stream.value, data };
}
app/page.tsx
"use client";
import { type CoreMessage } from "ai";
import { useState } from "react";
import { continueConversation } from "./actions";
import { readStreamableValue } from "ai/rsc";
export const maxDuration = 30;
export default function Chat() {
const [messages, setMessages] = useState<CoreMessage[]>([]);
const [input, setInput] = useState("");
const [data, setData] = useState<any>();
return (
<div>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
{messages.map((m, i) => (
<div key={i}>
{m.role === "user" ? "User: " : "AI: "}
{m.content as string}
</div>
))}
<form
onSubmit={async (e) => {
e.preventDefault();
const newMessages: CoreMessage[] = [
...messages,
{ content: input, role: "user" },
];
setMessages(newMessages);
setInput("");
const result = await continueConversation(newMessages);
setData(result.data);
for await (const content of readStreamableValue(result.message)) {
setMessages([
...newMessages,
{
role: "assistant",
content: content as string,
},
]);
}
}}
>
<input
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.target.value)}
/>
</form>
</div>
);
}
サーバーアクションを使用した場合は useChat
が使用できないっぽいので、結構ベタな実装感になるみたいです。
Streaming React Components
今度は打って変わって、最近微妙に流行りつつあるサーバーアクションがコンポーネントを返すサンプルです。
app/actions.tsx
"use server";
import { streamUI } from "ai/rsc";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const LoadingComponent = () => <div>getting weather...</div>;
const getWeather = async (location: string) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return "82°F️ ☀️";
};
interface WeatherProps {
location: string;
weather: string;
}
const WeatherComponent = (props: WeatherProps) => (
<div>
The weather in {props.location} is {props.weather}
</div>
);
export async function streamComponent() {
const result = await streamUI({
model: openai("gpt-4o"),
prompt: "Get the weather for San Francisco",
text: ({ content }) => <div>{content}</div>,
tools: {
getWeather: {
description: "Get the weather for a location",
parameters: z.object({
location: z.string(),
}),
generate: async function* ({ location }) {
yield <LoadingComponent />;
const weather = await getWeather(location);
return <WeatherComponent weather={weather} location={location} />;
},
},
},
});
return result.value;
}
app/page.tsx
"use client";
import { useState } from "react";
import { streamComponent } from "./actions";
export default function Page() {
const [component, setComponent] = useState<React.ReactNode>();
return (
<div>
<form
onSubmit={async (e) => {
e.preventDefault();
setComponent(await streamComponent());
}}
>
<button>Stream Component</button>
</form>
<div>{component}</div>
</div>
);
}
サーバーアクションがコンポーネントを返すケースはまだかなりマイナーな気がしますが、こういうのもできるっぽいです。
しかしジェネレーター関数を使うとは。
ほとんど公式のコピペですが、そんな感じっぽいです。
ほかにも generateObject()
や streamObject()
あたりもよく使用すると思いますか、こちらは公式ドキュメントを読めばほぼ問題なく理解できるかなと。