Next.jsでOpenAIを利用し、渡した単語のイメージカラーを提案してもらう

2023.05.29

こんにちは。データアナリティクス事業本部 サービスソリューション部の北川です。

最近は業務でOpenAIを利用する機会が多く、Next.jsでもOpenAI APIを試してみます。

試してみた

今回はある単語を渡して、それにあった色を提案してもらおうと思います。例えば、OpenAIが思う"深海"や"森林"のイメージに合うメインカラーや、アクセントカラーを提案してもらいます。

OpenAI APIキーの取得

openAIにログインし、右上の自分のアカウントから[View API keys]に移動します。移動先のAPI KEYから[+create new secret key]に任意のkey名を指定して、作成することができます。

こちらのエントリにも、APIキーの取得方法が記述されています。

プロジェクトの作成

Next.jsのプロジェクトを作成します。いくつか質問されますが、今回は全てyesを選択します。

$ npx create-next-app@latest --ts sample-app
Need to install the following packages:
  create-next-app@13.4.4
Ok to proceed? (y) y
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Use App Router (recommended)? … No / Yes
✔ Would you like to customize the default import alias? … No / Yes

ローカル環境を立ち上げます。

$ npm run dev

OpenAI APIの利用

一度に複数の質問を渡す

今回は、指定したテーマに合うメイン、サブトーン、アクセントの色をそれぞれ別々の質問として投げてみます。

質問を複数渡す場合は、promptに渡す形式を配列にします。res.json().choicesのtextプロパティににそれぞれの返答が配列で返ってきます。

const res = await fetch(URL, {
  method: "POST",
  body: JSON.stringify({
    prompt: ["質問1", "質問2", "質問3"],
    max_tokens: 200,
  }),
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${API_KEY}`,
  },
});

const json = await res.json();
console.log(json.choices);

Homeコンポーネントを以下のように変更します。今回は検証なので、API_KEYを直接書き込んでいます。

export default function Home() {
  const [text, setText] = useState<string>("");
  const [isLoading, setIsLoading] = useState(false);

  const [theme, setTheme] = useState<string[]>([]);

  const handleSubmit = async (e: { preventDefault: () => void }) => {
    e.preventDefault();
    if (text === "") return;
    setIsLoading(true);

    const API_KEY = "取得したAPIキー";
    const model = "text-davinci-003";
    const URL = "https://api.openai.com/v1/engines/" + model + "/completions";

    const colors = ["メイン", "サブトーン", "アクセント"];

    const questions = colors.map((color) => {
      return `前提条件:あなたはブログのデザインカラーを決める係です。
              ユーザーから渡される抽象的なテーマから、そのテーマに合う"${color}"の色を、backgroundColorと、textColorをそれぞれ1つずつのみ提案してください。
              色はrgba形式で、${color} : {"background": "提案する色", "color": "提案する色"}の形で提案します。

              テーマ:"${text}"`;
    });

    const res = await fetch(URL, {
      method: "POST",
      body: JSON.stringify({
        prompt: questions,
        max_tokens: 200,
      }),
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_KEY}`,
      },
    });

    const json = await res.json();

    const list = () =>
      json.choices.map((value: { text: string }) => {
        return value.text;
      });

    setTheme(list);
    setIsLoading(false);
  };

  return (
    ...
  )
}

UIの変更

Homeコンポーネントのreturn内にフォームを追加し、UIを変更します。フォームにフォーカスできないので、元々存在するNext.jsの画像を囲うdivタグに"-z-10"を追加します。

return (
  <main className="flex min-h-screen flex-col items-center justify-between p-24">
    <div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
      <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
        Get started by editing&nbsp;
        <code className="font-mono font-bold">src/app/page.tsx</code>
      </p>
      <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
        <a
          className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
          href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          By{" "}
          <Image
            src="/vercel.svg"
            alt="Vercel Logo"
            className="dark:invert"
            width={100}
            height={24}
            priority
          />
        </a>
      </div>
    </div>

    {/* "-z-10"を追加 */}
    <div className="-z-10 relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px]">
      <Image
        className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
        src="/next.svg"
        alt="Next.js Logo"
        width={180}
        height={37}
        priority
      />
    </div>

    <form onSubmit={handleSubmit}>
      <input
        className="text-black p-1 text-lg w-60"
        type="text"
        placeholder="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button
        className={`mx-3 p-1.5 rounded-md font-medium bg-blue-500`}
        type="submit"
      >
        送信
      </button>
    </form>
    <p>{isLoading ? "ローディング中" : null}</p>
    <div>
      {theme.length > 0
        ? theme.map((value, index) => {
            return (
              <p
                key={index}
                className={`p-2 my-2 rounded-sm`}
              >
                {value}
              </p>
            );
          })
        : null}
    </div>

    <div className="mb-32 grid text-center lg:mb-0 lg:grid-cols-4 lg:text-left">
      <a
        href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
        className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
        target="_blank"
        rel="noopener noreferrer"
      >
        <h2 className={`mb-3 text-2xl font-semibold`}>
          Docs{" "}
          <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
            -&gt;
          </span>
        </h2>
        <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
          Find in-depth information about Next.js features and API.
        </p>
      </a>

      <a
        href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
        className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800 hover:dark:bg-opacity-30"
        target="_blank"
        rel="noopener noreferrer"
      >
        <h2 className={`mb-3 text-2xl font-semibold`}>
          Learn{" "}
          <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
            -&gt;
          </span>
        </h2>
        <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
          Learn about Next.js in an interactive course with&nbsp;quizzes!
        </p>
      </a>

      <a
        href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
        className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
        target="_blank"
        rel="noopener noreferrer"
      >
        <h2 className={`mb-3 text-2xl font-semibold`}>
          Templates{" "}
          <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
            -&gt;
          </span>
        </h2>
        <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
          Explore the Next.js 13 playground.
        </p>
      </a>

      <a
        href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
        className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
        target="_blank"
        rel="noopener noreferrer"
      >
        <h2 className={`mb-3 text-2xl font-semibold`}>
          Deploy{" "}
          <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
            -&gt;
          </span>
        </h2>
        <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
          Instantly deploy your Next.js site to a shareable URL with Vercel.
        </p>
      </a>
    </div>
  </main>
);

動作確認

単語を渡して予期した通りに、色が返ってくるか確認します。

森林、レモンを渡してみました。

なんとなくですが、形式通りに取得できているようです。

提案された色を反映

取得した色を実際に表示させてみます。 今回は検証なので多少無理やりですが、json形式で取得できる確率が高くなるよう以下のように色を取得します。

const list = () =>
  json.choices.map((value: { text: string }) => {
    return value.text;
});

上記の部分を以下のように変更します。

const list = json.choices.map((value: { text: string }) => {
  const str = `{"background":`;
  const i = value.text.indexOf(str);
  return value.text.substring(i);
});

取得した文字列をjsonに変換する関数を定義します。OpenAPIからのresponseが望んだ形式で返ってくるかわからないので、こちらもtry構文で乗り切ります。

const parseJsonString = (value: string) => {
  try {
    const str = JSON.parse(value);
    return str;
  } catch (e) {
    console.log(e);
  }
  return {};
};

作成した関数を利用して、スタイルを当ててみます。

  {theme.length > 0
    ? theme.map((value, index) => {
        return (
          <p
            key={index}
            className={`p-2 my-2 rounded-sm`}
            style={parseJsonString(value)}
          >
            {value}
          </p>
        );
      })
    : null}

再度、試してみます。

  • 森林

  • レモン

  • 深海

もちろん同じテーマで投げても色は毎回変わります。

  • ドラゴン

まとめ

今回はOpenAIにある単語を渡して、それに合う色を提案してもらいました。もちろん毎回成功するわけではないです。体感的には、30回に1回失敗するかくらいで意外と望んだ形式でresponseが返ってきていました。OpenAIでやれることはまだまだ多いので、今後も継続して触っていきたいと思います。