2025.05.15

Dify × NotionでAIチャットボットを構築する
- Dify
- AI
- Notion
- React
はじめに
AIアプリを作ることができるプラットフォームDifyはナレッジ(Knowledge) 機能を使って、LLMの知識データベースを拡張することができます。そしてDifyは簡単にNotionと同期する機能が備わっているため、Notionにある記事や、データベースをそのままDifyのナレッジとして利用することができます。一切コードを書かずにDify × NotionのAIアプリが公開できますが、本記事ではReact.jsで作ったアプリ上でチャットbotとして動かすところまで行なっています。
なお、Notionから同期
の方法だと現状、自動更新ができないので、それを解決するための続編も書いております。
https://topaz-inc.dev/articles/dify-extarnal-knowledge-api-retrieval-notion-api/
Dify で Notionと同期
DIfyはClooud版とDockerを使ってローカルで動かす方法がありますが、
今回はCloud 版を利用していますので、以下の URL にアクセスし、アカウントを作成します。
ナレッジを作成するときに、Notionから同期
を選択し、接続します。

接続を許可していき、Notion内のページを選択します。

成功したらデータソースでNotionが接続済みになっている状態になります。

Notion側
Notion側は自動で先ほど選択したページがDifyと接続されているので、特に何もしなくてもよかったのですが、確認と、別のページも追加したい場合は、ページごとに接続が必要です。

Difyでチャットボットアプリ作成
新規でチャットボットアプリを作成します。

作成したナレッジを選択し、問題なさそうであれば公開します。

ここまででノーコードでAIアプリが公開できましたが、外部アプリでこのアプリ機能を組み込んで利用する場合は、APIシークレットキーが必要となりますので、コピーします。

Reactアプリに組み込み
先ほどのAPIシークレットキーをセットして、https://api.dify.ai/v1/chat-messages
にPOSTリクエストする簡単なフォームを作成しました。
import * as React from "react"
import { useState } from "react"
const DIFY_API_KEY = "app-xxxxxx"
const DIFY_API_URL = "https://api.dify.ai/v1/chat-messages"
interface Message {
role: "user" | "assistant"
content: string
}
const Chat = () => {
const [isOpen, setIsOpen] = useState(false)
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState("")
const [isLoading, setIsLoading] = useState(false)
const sendMessageToDify = async (message: string): Promise<string> => {
try {
const response = await fetch(DIFY_API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${DIFY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
inputs: {},
query: message,
response_mode: "blocking",
conversation_id: "",
user: "user",
}),
})
if (!response.ok) {
throw new Error(`Dify API error: ${response.statusText}`)
}
const data = await response.json()
return data.answer
} catch (error) {
console.error("Error sending message to Dify:", error)
throw error
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!input.trim() || isLoading) return
const userMessage = input.trim()
setInput("")
setMessages(prev => [...prev, { role: "user", content: userMessage }])
setIsLoading(true)
try {
const response = await sendMessageToDify(userMessage)
setMessages(prev => [...prev, { role: "assistant", content: response }])
} catch (error) {
console.error("Error:", error)
setMessages(prev => [
...prev,
{
role: "assistant",
content: "申し訳ありません。エラーが発生しました。",
},
])
} finally {
setIsLoading(false)
}
}
return (
<div className="fixed bottom-5 right-5 z-50">
{!isOpen ? (
<button
onClick={() => setIsOpen(true)}
className="bg-rose-500 text-white rounded-full p-4 shadow-lg hover:bg-rose-600 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
</button>
) : (
<div className="bg-white rounded-lg shadow-xl w-[350px] sm:w-[400px]">
<div className="flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-semibold">チャット</h3>
<button
onClick={() => setIsOpen(false)}
className="text-gray-500 hover:text-gray-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div className="h-[400px] overflow-y-auto p-4">
{messages.map((message, index) => (
<div
key={index}
className={`mb-4 ${
message.role === "user" ? "text-right" : "text-left"
}`}
>
<div
className={`inline-block rounded-lg px-4 py-2 ${
message.role === "user"
? "bg-rose-500 text-white"
: "bg-gray-100 text-gray-900"
}`}
>
{message.content}
</div>
</div>
))}
{isLoading && (
<div className="text-left">
<div className="inline-block rounded-lg bg-gray-100 px-4 py-2 text-gray-900">
考え中...
</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={e => setInput(e.target.value)}
placeholder="メッセージを入力してください..."
className="flex-1 rounded-lg border border-gray-300 px-4 py-2 focus:border-rose-500 focus:outline-none"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading}
className="rounded-lg bg-rose-500 px-4 py-2 text-white hover:bg-rose-600 disabled:bg-gray-400"
>
送信
</button>
</div>
</form>
</div>
)}
</div>
)
}
export default Chat
これだけで簡単にNotionの記事をナレッジとしたAIチャットbotを自前のアプリに組み込めました。
データ更新の課題
ただ実用していく中ではNotionのページが更新されたら、Dify側でも自動で同期してほしいのですが、現状の「Notionから同期」の手法では自動同期ができないようで、Dify側で手動で行う必要があるようです。

現状の解決法
自動同期はいずれDifyのアップデートで実現するかもしれませんが、現状の方法としてはDifyの外部ナレッジAPI × Notion API × 外部アプリAPIを使うことで再現できるので、続編としてこちらの記事も書いております。
https://topaz-inc.dev/articles/dify-extarnal-knowledge-api-retrieval-notion-api/