2025.05.15

dify-notion-knowledge-ai-chat-bot-react

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/