My first Cloudflare Workers AI
ワークショップ
来週というか今週の金曜日、名古屋でCloudflare Workers + Honoのワークショップをやるのですが、先日Birthday Weekで発表された「Workers AI (AIアプリがGPUで動く!)」が楽しいので、それを使ったアプリを作ろうということになりました。
先日もServerlessDays Tokyo 2023というイベントでCloudflare Workers + Honoのワークショップをやったのですが(4時間!)、その時にドタバタしちゃったんで、今回はもうやることを全部ここに書こうと思います。とはいえ「Cloudflare Workersとは」とか「Honoの特徴は?」とか書き出すとキリがないので、参考文献を参考にしてください。
準備してもらうこと
まず、Cloudflareのアカウントを作って、Dashboardに入れることを確認してください。
次に、持ち込むPCで以下やっておいてください。
GitとNode.jsのインストール
GitとNode.jsが入ってるかを確認してください。入ってなかったら入れてください。分からない方は調べてもらえると!
Honoが動くことを確認
ターミナルで以下を実行してください。
npm create hono@latest my-first-app
cd my-first-app
npm install
これでローカル環境ができます。最低限、ここまでやっておいてください。もし興味のある方は開発サーバーを立ち上げたり、デプロイまでやってみるといいかもです。
開発サーバーを立ち上げる。
npm run dev
デプロイする。
npm run deploy
デプロイした場合、ほっておいても基本的に問題ありませんが、もし気になる方はダッシュボードの「Workes & Pages」というメニューから対象のWorkerを削除してください。
完成形
これが完成形です。1.5倍速のスクリーンキャストです。
レポジトリはこちら。
さて、以降作っていくので、ワークショップ参加する方で、ネタバレしたくない人は見ないでください!
スターター
本来、Honoのアプリを作る場合は"create-hono"コマンドを使います。
npm create hono@latest my-first-app
ただ、今回はより完成形に近いスターターを用意したので、そちらから始めていきましょう。
ダウンロードとインストール
以下のコマンドでダウンロードできます。
npx degit 'yusukebe/my-first-workers-ai#starter' my-first-workers-ai
ディレクトリに入って、お好きなパッケージマネージャーでインストールしてください。特に指定がなければnpm
を使います。
cd my-first-workers-ai
npm install
構成
構成はこんな感じ。
$ tree .
.
├── .gitignore
├── README.md
├── assets
│ └── script.js // 最後にインタラクティブするためのJS
├── package-lock.json
├── package.json
├── src
│ ├── globals.d.ts // .jsをテキストとして扱うための型定義
│ ├── index.tsx // メインのファイル、JSXを書きたいので`.tsx`にしてある
│ └── renderer.tsx // レンダラーの定義
├── tsconfig.json
└── wrangler.toml // Workersの設定が書いてある
はじめてのCloudlfare Workers + Hono
AIアプリを作る前に「Cloudflare Workers上でHonoを動かす」のを入門してみましょう。ここでは最低限のことを書くので、全てを知りたい方は先日のワークショップで使った大作の資料を御覧ください。
Hello World
なにはともあれ「Hello World」。といってももう書いてあります。
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
export default app
では開発サーバーでローカルで立ち上げてみましょう。WranglerというCLIを使います。もしグローバルにコマンドが入っていれば以下でいけます。
wrangler dev src/index.tsx
今回はもうすでにpackage.json
でスクリプト定義してあるので、以下を使ってください。
npm run dev
注意したいのは、今回は--remote
というオプションを付与しています。Workers AIを使うには今のところ「リモート」に繋がないといけないからです。ちなみにCloudflareでは、ローカルモードでもAI Bindingsを使えるようにする予定があります。「--remote」のみだとPagesで開発ができないので、それ欲しいですよね。
さて、開発サーバーが立ち上がったら、「B」をタイプするか、http://localhost:8787
へアクセスしましょう。これがあなたのはじめてのCloudflare Workers、そしてHonoアプリです。
デプロイ
もうこのままデプロイしてしまいましょう。はじめてCloudflareにデプロイするという方はログインしなくてはいけなかったり、アカウントを作っているけどメール認証が終わってなければしなくてはいけなかったりしますが、それは現地で対応します。
デプロイも簡単です。素のWranglerコマンドだと以下です。
wrangler deploy src/index.tsx
package.json
には--minify
オプションを追加したdeploy
コマンドを用意しているので、そちらを使ってください。
npm run deploy
すると、"my-first-workers-ai"という名前であなたのWorkerが「全世界」にデプロイされます!おめでとうございます。https://my-first-workers-ai.設定したサブドメイン.workers.dev
にアクセスしましょう。
上記の"my-first-workers-ai"という名前はwrangler.toml
のname
プロパティの値なので、変更したい方はそれを変えてください。
name = "my-first-workers-ai"
JSX
あとはJSONを返したり、クエリを受け取ったり試すのですが、ひとつ重要な概念としてJSXがあるので、そこだけ解説しておきます。
JSXとはReactなどで採用されているJavaScriptの中でマークアップを書くSyntaxです。それがHonoでは標準で備わっていて、拡張子を.ts
から.tsx
に変えるだけで使えるようになっています。ですので、以下のように書いてHTMLを返却できます。
app.get('/about', (c) => {
return c.html(
<div>
<h1>About me</h1>
<h2>Favorites</h2>
<ul>
<li>Ramen</li>
<li>Sushi</li>
<li>Watching Baseball</li>
</ul>
</div>
)
})
気をつけたいのは、このJSXはサーバーサイドでのレンダリングのため「だけ」であり、クライアントは一切関係ありません。どちらかというとmustacheとかHandlebarsとかのテンプレートエンジンのかわりと考えてください。げんにmustacheミドルウェアを廃止する変わりにJSXが導入されました。
さらに突っ込んだ話をするとHonoの中で、ReactやPreactを使うことは可能で、Reactの場合はreact-dom
のrenderToReadbleSteam()
とかrenderToString()
を使ってください。そして適切にhydrateすればクライアントも同じように面倒をみることができます。ただ、それはだいぶだるいので、Next.jsなどのフレームワークが存在するのであります。
そして、実はHonoベースのIsland Hydrateにも対応し、File-baseのルーティングができるフレームワーク「Sonik」を開発中です。
まだ「Dev」ステージでAPIは変更される可能がありますが、なかなかできが良く、今回と似たような「ChatGPT Streaming」のアプリでも大活躍しています。
気になる方はチェックしてみてください。
で、何が言いたいかというとHonoのJSXは万能ではないということです。最近HonoのJSXが「手軽」という文脈で注目されているのですが、盲目的にならないでください。他のUIライブラリも使えます。また、今回はHonoのJSXをWorkersで動かして、HTMLを出力していますが、HTMLの場合、ページが多いとPagesが選択になります(SonikはPagesが前提です)。
とはいえ、このワークショップでは「サクッと」デモを作るのに、Workers + HonoのJSXという構成をとります。上記のようなメタフレームがある一方で、「サクッと」UIを作るのに、Workers + HonoのJSXはすごく重宝します。
AIアプリを作る
では、Workers AIのアプリを作っていきましょう。
Bindingsと型
WorkersにはBindingsという仕組みがあって、例えば以下が利用できます。
- KV
- Durable Objects
- R2
- D1
- Queue
- など
今回のAIもBindingsとして扱います。ちなみにこのBindingsはCloudflare Workers/Pagesにとって非常に重要な概念で、今回のBirthday Weekで出た「Hyperdrive」もBindings経由で利用することも含め、今後も活用されていきます。
Bindingsを有効にするにはまず、wranlger.toml
を編集します。今回のスターターではもうすでに記述があります。
[ai]
binding = "AI"
そして、TypeScriptの型をsrc/index.tsx
内で定義します。
type Bindings = {
AI: any
}
any
になっているのはまだ型定義が提供されていないからそうします。
TypeScriptはとっつきにくいし、JavaScriptが好きな方もいますが、とくにHonoを使う場合はTypeScriptを推奨しています。僕も慣れるまで時間がかかったのですが、まぁ型とかジェネリクスとか分からなかったらそういうものだと思ってください。
ついでに、「AI」のリクエストに必要なmessages
とレスポンスのanswer
の型定義もしてしまいます。あとで使います。
type Answer = {
response: string
}
type Message = {
content: string
role: string
}
new Ai()
いよいよ、AIを使いましょう。今回は「Text Generation = LLM」のモデルを使ってGPTアプリを作ります。このモデルはMetaのLlamaモデルそのもので、Cloudflareで動くようにしただけのものです。ですので、使い方のより詳細を知りたければそれを調べればいいでしょうl
AIを使うのは簡単です。以下をやります。
-
@cloudflare/ai
からAi
をimportする -
AI
Bindings を引数にnew Ai()
する - モデルを指定しつつプロンプトを渡して実行する
- レスポンスをテキストで返す
- 確認する
全体のコードはこんなになりました。
import { Hono } from 'hono'
import { Ai } from '@cloudflare/ai'
type Bindings = {
AI: any
}
type Answer = {
response: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/ai', async (c) => {
const ai = new Ai(c.env.AI)
const answer: Answer = await ai.run('@cf/meta/llama-2-7b-chat-int8', {
messages: [
{
role: 'user',
content: `What is Cloudflare Workers. You respond in less than 100 words.`
}
]
})
return c.text(answer.response)
})
export default app
http://localhost:8787/ai
にアクセスすると「Cloudflareとは?」の答えがでてます!
いくつか注意事項があるので書いておきます。
- 英語以外の言語の対応は不十分
- 返却できるTokenの量に限りがあるので、単語の数を絞るように命令してる
-
ai.run()
の第2引数は他にもフォーマットがある -
role
はuser
を指定したが、system
とassistant
が使える - Bindings + ライブラリ以外でも直接APIを叩ける、そうするとWorkers以外からも使える
日本語がほぼ対応してないのが、残念ですが、今後なにか策がでるだろうし、テキスト翻訳のAIモデルを組み合わせるのも面白いです。
Streamで返す
ここで面白いのやりましょう。ChatGPTみたいに「ヌルヌル文字が返ってくる」のやりましょう。
例えばChatGPTの場合、APIからのレスポンスが元からStreamで返ってきていい感じですが、Workers AIはまだStreamには未対応です(今後対応する予定あり)。ですので、全体のレスポンスが返ってくるのを待たなくてはいけないのですが、もらったテキストを一度に表示するのではなく、文字を1文字ずつ分割して順々に表示させてみましょう。
c.streamText()
最近Honoに入ったc.streamText()
を使います。
これはとてもよくできたAPIで、ベーシックな使い方だとこうします。
app.get('/stream', (c) => {
return c.streamText(async (stream) => {
stream.writeln('Hello')
await stream.sleep(1000)
stream.writeln('Hono!')
})
})
Hello
と表示されて1秒経ってからHono!
と出ます。これを利用するわけです。
1文字表示された10ms待って次の文字を表示しましょう。エンドポイントはGET /ai
にしておきます。
app.get('/ai', async (c) => {
const ai = new Ai(c.env.AI)
const answer: Answer = await ai.run('@cf/meta/llama-2-7b-chat-int8', {
messages: [
{
role: 'user',
content: `What is Cloudflare Workers. You respond in less than 100 words.`
}
]
})
const strings = [...answer.response]
return c.streamText(async (stream) => {
for (const s of strings) {
stream.write(s)
await stream.sleep(10)
}
})
})
できましたか!?
簡単なUIを作る
さてUIを作っていきましょう。
Renderer
Honoでは最近、c.render()
という機能が入りました。この"Renderer"を使うと、HTMLのレイアウトを定義できて、それを各エンドポイントで使い回せます。今回はGET /
のみのUIになるので旨味はありませんが、やってみましょう。
もうすでにsrc/renderer.tsx
というファイルがあるので、それをindex.tsx
内でimport
します。renderer
というのがすでにRendererをセットするためのミドルウェアになっているのですね。new Hono()
でHonoのインスタンスapp
ができた直後にそれを登録します。
import { renderer } from './renderer'
// Bindings、型定義
const app = new Hono<{ Bindings: Bindings }>()
app.get('*', renderer)
これで以降c.render()
を使うとテンプレートが適応されています。つまり、以下のように書くと...
app.get('/', (c) => {
return c.render(<h1>Hello AI!</h1>)
})
出力されるHTMLはこのようになります。
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div>
<h1>Hello AI!</h1>
</div>
</body>
</html>
CSSフレームワークを入れる
そのままだと味気ないので、ちょっとだけかっこよくしましょう。といっても、難しいことはしません。したいのであれば完成してからやりましょう。今回は"new.css"というCSSフレームワークを使います。
このフレームワークの素晴らしい点は、HTMLのマークアップだけでいい感じにスタイリングしてくれるところで、わざわざclass属性を生やさなくてよいです。renderer.tsx
を以下のように変更し、new.cssをCDN経由で読むようにしましょう。ついでにヘッダータイトルも追加してあります。
import { jsxRenderer } from 'hono/jsx-renderer'
export const renderer = jsxRenderer(({ children }) => (
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css" />
<script src="/script.js"></script>
</head>
<body>
<header>
<h1>My first Workers AI</h1>
</header>
<div>{children}</div>
</body>
</html>
))
何もしないよりだいぶかっこいいです。
POSTを受け取る
「フォームからテキストを入力するとそれにAIが応じてくれる」というのをやりたいので、フォームからのリクエストを受け取り、AIへリクエストをし、返ってきた値を返却するエンドポイントを作りましょう。といっても、先程つくったGET /ai
を改修するだけです。
-
GET /ai
からPOST /ai
へ変更 - JSON形式でリクエストボディを受け取る
- その中の
messages
を使ってAIへ投げる - Streamで返す
コードはこうなります。
app.post('/ai', async (c) => {
const { messages } = await c.req.json<{ messages: Message[] }>()
const ai = new Ai(c.env.AI)
const answer: Answer = await ai.run('@cf/meta/llama-2-7b-chat-int8', {
messages
})
const strings = [...answer.response]
return c.streamText(async (stream) => {
for (const s of strings) {
stream.write(s)
await stream.sleep(10)
}
})
})
Message
というのは上で定義したこれです。
type Message = {
content: string
role: string
}
で、その配列messages
を渡しているのが鍵です。具体的にはこのような値になります。つまり今までの履歴を含めて送ってあげることで、会話を続かせているのですね。user
の値はこちらが入力したもの、assistant
の値はAIからのレスポンスをそのままオウム返ししています。
{
"messages": [
{
"role": "user",
"content": "You are a helpful assistant. You do not respond as 'User' or pretend to be 'User'. You respond as 'Assistant'. You respond in less than 100 words."
},
{
"role": "assistant",
"content": "Of course! I'm here to help. How can I assist you today?"
},
{
"role": "user",
"content": "What is Cloudflare?"
},
{
"role": "assistant",
"content": "Cloudflare is a web security company that provides a range of services to protect and optimize websites, including DNS, CDN, security, and performance tools. It helps to improve website speed, reduce downtime, and protect against cyber threats."
},
{
"role": "user",
"content": "What's Workers?"
},
{
"role": "assistant",
"content": "Workers is a serverless computing platform offered by Cloudflare. It allows developers to run JavaScript code at the edge of the internet, closer to the users, resulting in faster and more efficient web applications. Workers provides a secure and scalable environment for running serverless functions, without the need to manage servers or infrastructure."
}
]
}
では検証してみます。履歴関係なくシンプルにrole:user
だけでやりましょう。curlを使ってリクエストを出してみます。せっかくなので-N
オプションをつけてヌルヌル出してみます。
curl -N -s -XPOST -H 'Content-Type: application/json' --data '{"messages":[{"role":"user","content":"hi!"}]}' http://localhost:8787/ai
いい感じですね!
curl
だとStreamにならないこともあるのですが、その場合は気にしないでください!
フォームを作成
いよいよ、フォームをつくってユーザーが入力したテキストにAIが答えるのをやってみましょう。
まずsrc/index.tsx
を編集します。GET /
のエンドポイントでJSXを使ってHTMLを返すようにします。
app.get('/', (c) => {
return c.render(
<>
<h2>You</h2>
<form id="input-form" autocomplete="off" method="post" action="/ai">
<input
type="text"
name="query"
style={{
width: '100%'
}}
/>
<button type="submit">Send</button>
</form>
<h2>AI</h2>
<pre
id="ai-content"
style={{
'white-space': 'pre-wrap'
}}
></pre>
</>
)
})
見た目はこうなりました。
「You」のinputに入力、「Send」を押すかエンターでPOST /ai
をfetchして、結果を「AI」に表示します。これを実現するのに、「クライアントサイドのJavaScript」を利用します。これまで書いてきたJavaScript/TypeScriptはWorkersのサーバーサイドだったのですが、「クライアントサイド」です。
今回はReactやjQueryなどのライブラリを使わず素で書いてみます。このJavaScriptについては、不明点があっても今回のワークショップの範疇外であるので気にしないでください。僕もChatGPTに聞いて書きました。
document.addEventListener('DOMContentLoaded', function () {
const target = document.getElementById('ai-content')
document.getElementById('input-form').addEventListener('submit', function (event) {
event.preventDefault()
target.innerHTML = 'loading...'
const formData = new FormData(event.target)
const query = formData.get('query')
fetch('/ai', {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
messages: [
{
role: 'user',
content: query
}
]
})
}).then((response) => {
response.text().then((data) => {
target.innerHTML = data
})
})
})
})
やっていることは、以下の通り。
- DOMがロードされたらイベントリスナーを追加
-
input-form
というIDを持ったフォームのSubmitが押された時の処理を書く -
ai-content
というIDを持ったターゲットの中を「loading...」にする - フォームの
input
に入力された値「qeury
」の値を取得 -
POST /ai
に対してボディをJSONでリクエスト - レスポンスのテキストをターゲットの中身とする
そして、このscript.js
をsrc/index.tsx
で読み込ませて、配信します。
import script from '../assets/script.js'
// ...
app.get('/script.js', (c) => {
return c.body(script, 200, {
'Content-Type': 'text/javascript'
})
})
そしてRenderer内のHTMLで読み込ませます。以下をメタタグ内に追加してください。
<script src="/script.js"></script>
いかがでしょうか?これで「ユーザーの入力に対してAIが答える」のがUI付きでできましたね!
返答が途中で切れちゃうことが多いと思いますが、それは前述した通り、Workers AIのLLMでは現在、返答されるtokenの数に限りがあるからです。のちほど対策します。
完成させる
さて最後の仕上げです。どうせなら以下2つをやりましょう。
- Streamで文字をヌルヌル出す
- 履歴を保持する
実現するにはassets/script.js
を変更するだけでOKです。
メッセージの履歴と初期値
まず履歴を保持するための配列messages
を用意します。めんどくさいんでグローバルに置きます。
const messages = [
{
role: 'user',
content: `You are a helpful assistant. You do not respond as 'User' or pretend to be 'User'. You respond as 'Assistant'. You respond in less than 100 words.`
}
]
もう初期値を入れてます。ユーザーからAIへの命令が入ってます。
- あなたは優秀なアシスタントです
- あなたはUserもしくはUserに似せて反応してはいけません
- アシスタントとして返信してください
- 100単語以内で返答してください
最後の「100単語以内」というのは現在のWorkers AIのLLMの場合、返答できるtoken数に限りがあるので、適当な短さを指定しています。
Streamを表示させる
Streamを表示させるための関数はこちらです。
function fetchChunked(target) {
target.innerHTML = 'loading...'
fetch('/ai', {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ messages })
}).then((response) => {
const reader = response.body.getReader()
let decoder = new TextDecoder()
target.innerHTML = ''
reader.read().then(function processText({ done, value }) {
if (done) {
messages.push({
role: 'assistant',
content: target.innerHTML
})
return
}
target.innerHTML += decoder.decode(value)
return reader.read().then(processText)
})
})
}
キモは後半で、response.body.getReader()
で取得したリーダーをread()
をするとStream、つまり今回は1文字ずつ受け取ると中身が実行され、追加された文字を含んだ文字列で、ターゲットの中身を書き換えています。全部受け取った段階で、messages
へ次に送るためにrole
をassistant
としてmessages
へ追加しています。
addEventListener
は以下としています。初期メッセージを表示するためにfetchChunked()
を最初に呼び出しています。
document.addEventListener('DOMContentLoaded', function () {
const target = document.getElementById('ai-content')
fetchChunked(target)
document.getElementById('input-form').addEventListener('submit', function (event) {
event.preventDefault()
const formData = new FormData(event.target)
const query = formData.get('query')
messages.push({
role: 'user',
content: query
})
fetchChunked(target)
})
})
assets/script.js
の全部のコードはこちらです。コピペしちゃってください!
assets/script.js 全コード
const messages = [
{
role: 'user',
content: `You are a helpful assistant. You do not respond as 'User' or pretend to be 'User'. You respond as 'Assistant'. You respond in less than 100 words.`
}
]
document.addEventListener('DOMContentLoaded', function () {
const target = document.getElementById('ai-content')
fetchChunked(target)
document.getElementById('input-form').addEventListener('submit', function (event) {
event.preventDefault()
const formData = new FormData(event.target)
const query = formData.get('query')
messages.push({
role: 'user',
content: query
})
fetchChunked(target)
})
})
function fetchChunked(target) {
target.innerHTML = 'loading...'
fetch('/ai', {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ messages })
}).then((response) => {
const reader = response.body.getReader()
let decoder = new TextDecoder()
target.innerHTML = ''
reader.read().then(function processText({ done, value }) {
if (done) {
messages.push({
role: 'assistant',
content: target.innerHTML
})
return
}
target.innerHTML += decoder.decode(value)
return reader.read().then(processText)
})
})
}
これで完成!できたかな?
デプロイ
さあいよいよデプロイだ。冒頭と同じように以下を実行しよう。
npm run deploy
これで全世界にあなたの最初のWorkers AIアプリが公開されました。素晴らしいのは、これがエッジで動いてることです。そしてそのエッジにはGPUがのっています!
答え合わせ
僕が作ったアプリはこちらです。
しばらく置いておくと思うので、答え合わせに使ってください。
発展編
これだけでも十分面白いのですが、時間が余った方、家で続きをやれる方はこれから紹介することをやるといいでしょう。
見え目を工夫する
同じ機能でも見栄えが違うだけでまるで違うアプリケーションになったりします。例えば、ChatGPTのGatewayアプリを作っているのですが、工夫するだけでだいぶそれっぽくなります。
- 入力欄を下にする
- メッセージが出てくる順番を反対にして下から出す
- テキストが出てる時、テキストの末尾は「❚」などを点滅させる
他のモデルを使う
今回使ったLLMのモデル以外にもWorkers AIには以下のモデルを使えます。
- Speech to text
- Translation
- Sentiment Analysis
- Image classification
- Embedding
例えば、今回使ったLLMは日本語に弱いので、入力と出力、特に出力の際に日⇔英の翻訳をかます、なんてのをすごい面白いです。やってみたい。
OpenAIをやってみる
さきほど紹介したGatewayアプリのようにOpenAIをやってみるのもいいでしょう。今回やったStreamの表示や、フォームでの入力などをそのまま活かせるでしょう。
また、ChatGPTのPlugin作成も面白いです。HonoはChatGPTする際に使うOpenAPIをエレガントに作れる「Zod OpenAPI」拡張があるので、ぜひ使ってください。そのあたりのHonoがAIに向いてるという話は英語ですが以下にあります。
Honoを別のプラットフォームで動かす
Honoは他のプラットフォーム、ランタイムで動きます。ぜひ動かしてみましょう。基本的にStreamも動きます。
以下は用意されているスターターの一覧です。npm create hono@latest my-app
コマンドを実行すれば選べるでレッツトライ。
- aws-lambda
- bun
- cloudflare-pages
- cloudflare-workers
- deno
- fastly
- lagon
- lambda-edge
- netlify
- nextjs
- nodejs
- vercel
その他
その他の事柄。
消しておく
デプロイした場合Workerを「DELETE」しておくのをお忘れなく!ダッシュボードの左側「Workers & Pages」から対象のWorkerを選んで、「設定」>「プロジェクトの削除」をしてください。
まとめ
以上、お疲れ様でした!果たして終わったでしょうか?当日、名古屋では見事に全員がWorkers AIのアプリを完成させているのを願っています。
もし終わらなかったら!完成品のレポジトリをcloneして、実行しちゃえばいいじゃないでしょうか!
git clone git@github.com:yusukebe/my-first-workers-ai.git
cd my-first-workers-ai
npm install
npm run dev
npm run deploy
名古屋に直接来てもらってもいいですよ!
ではさようなら。
Discussion
ワークショップ参加させていただきました!
#cssフレームワークを入れる の
render.tsx
の内容が間違っていたのでお手隙の際に修正いただけましたら🙏ありがとうございます!
ですね、ですね。修正しておきました!