Typoless APIとNext.jsを利用して、校正機能付きエディターを実装する

ほとんどのアプリケーションには文章を入力する機能が付帯していると思いますが、文章作成・編集・管理がメイン機能となっているサービスにおいては、文章の校正機能を提供したい場合があります。

今回は、「Typoless APIを活用した文章校正ツール」の開発方法をご紹介します。
以下のような校正ツールが開発できます。

![スクリーンショット 2024-04-07 17.45.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677068/d066bfdb-58c3-7ac9-2473-0b840df95c44.png)

## TypolessのAPIキーを取得

はじめに今回のアプリの根幹となる校正が行えるように、TypolessのAPIキーを取得します。

次のような手順でアカウント登録とトライアルを開始してください。

https://typoless.ashi.com

1. https://typoless.ashi.com にアクセスし、右上の新規登録をクリックし、その後「API連携プランではじめる」をクリックします。
14日間無料とありますが、14日経過後に勝手に課金されることはありませんので、「ひとまず試してみるか」くらいの軽い気持ちで登録しても問題ありません。    
2. 各種情報を入力し新規登録ボタンをクリックすると、ログイン画面に遷移します。入力したメールアドレスとパスワードでログインください。
3. プランの選択画面が出てくるので、内容を確認して「申し込む（初回14日間無料）」ボタンをクリックします。するとプラン契約の画面に遷移しますので、問題がなければ必要事項を記入の上「申し込む」をクリックしてください。上述の通り、トライアルを始めただけでは課金は発生せず、本契約をしたい場合はトライアル後に再度手続きを行う必要があります。そのため、お気軽にトライアルをご利用ください。
4. 契約に成功すると、アプリに再度戻ってきます。誘導の通りアカウント設定に移動すると、APIキーが発行されており、APIが利用可能になります。

トライアル期間は2週間で、累計20万文字まで利用することができます。

## アプリのひな形を作成

ここからはAPIを組み込むアプリの実装を行っていきます。

今回は`create next app` で作成したひな形の上で開発を行います。
create-next-app@14.1.4を利用しました。

```bash
$ npx create-next-app
✔ What is your project named? … typoless-api-demo
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … No
```

以上で`typoless-api-demo` というフォルダが作成され、その中にアプリのひな形が作成されました。

## テキストエリアを用意

今回の実装は一部以下の記事を参考にしています。

https://zenn.dev/moozaru/articles/58bd654bbc402c

appの下にあるpage.tsxにテキストエリアを用意します。

最初に、デザインをシンプルにするため、デフォルトの装飾を消します。
root直下のtailwind.config.ts内のtheme部分を削除し、app/globals.cssを以下のようにします。

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}
```

その後、app/page.tsxを以下のようにします。

```tsx
"use client";

import { useFormState, useFormStatus } from "react-dom";
import { type ActionResult, formAction } from "./form-action";
import { createFeedback } from "./process-node";

export default function Home() {
  const [result, dispatch] = useFormState(formAction, {} as ActionResult);
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-8 md:p-24 bg-slate-200">
      <form className="flex flex-col w-full gap-4" action={dispatch}>
        <textarea
          className="h-[200px] p-2 border border-black rounded"
          name="text"
          placeholder="Type your message here."
          maxLength={400}
        />
        {result.response?.errorDescription && (
          <div className="text-red-500 text-center">
            {result.response.errorDescription}
          </div>
        )}
        <SubmitButton />
        {result.success && (
          <div className="text-center">
            {createFeedback(
              result.response.originalText!,
              result.response.body!
            )}
          </div>
        )}
      </form>
    </main>
  );
}

function SubmitButton() {
  const status = useFormStatus();
  return (
    <button
      className="w-auto mx-auto py-2 px-6 bg-black text-white rounded"
      type="submit"
      disabled={status.pending}
    >
      {status.pending ? "送信中..." : "送信"}
    </button>
  );
}

```

form-actionとprocess-nodeからimportしている部分はエラーが出ると思いますが、この後準備します。

シンプルなテキストエリアと送信ボタンを記述しました。

## APIとの通信部分

form-actionを定義し、APIと通信する部分を準備します。

`app/form-action.ts`を用意して、以下のコードを貼り付けます。

```ts
"use server";

export type TypolessResponse = {
  checkType: "tye" | "asahi_dict" | "custom_dict";
  comment: string;
  correction: {
    originalText: string;
    candidates: string[];
    score: number;
    position: { start: number; end: number };
  };
};

export type ActionResult = {
  success: boolean;
  response: {
    errorDescription?: string;
    body?: TypolessResponse[];
    originalText?: string;
  };
};

// 以下にAPI仕様に記載されたURLをセット
const URL = "ここにURL";

export async function formAction(
  _prev: any,
  formData: FormData
): Promise<ActionResult> {
  const text = formData.get("text") as string;
  if (!text) {
    return {
      success: false,
      response: {
        errorDescription: "文章を入力してください",
      },
    };
  }

  const res = await fetch(URL, {
    method: "POST",
    headers: {
      // 以下にAPIキーをセット
      "x-api-key": "ここにAPIキー",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      text,
      disableCustomDict: true,
      disableAsahiDict: true,
    }),
  });
  const { results } = await res.json();

  return {
    success: true,
    response: {
      body: results,
      originalText: text,
    },
  };
}
```

なお、コード内に登場するURLとAPIキーについては、みなさんご自身で確認して貼り付けてください。
URLはTypolessのWebコンソールにログイン後に表示される「API仕様書」ボタンをクリックして、確認してください。
APIキーについてはログイン後の画面から取得できます。

![スクリーンショット 2024-04-07 17.25.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677068/921eb119-3d83-0cdf-4dc6-24ac035dd99d.png)

今回のデモではAIによる解析のみを利用します。そのため`fetch`の`body`部分で`disableCustomDict: true`と`disableAsahiDict: true` を記述しています。

Typoless APIでは、朝日新聞社内で実際に運用されている10万以上のルールを基に作成した「ルール辞書」や、ユーザー独自にカスタマイズできる「カスタム辞書」の解析も同時に行うことができます。

カスタム辞書に関してはAPIからルールの取得や更新など、一連のCRUD操作を行えますので、実際に利用する際は活用してみてください。

なお`form-action` はNext.jsのserver actionsを利用して動作しています。
詳しい解説は行いませんが、APIキーはクライアント側から確認できないようになっています。

## APIの結果を加工してコンポーネントに変換する部分

最後にAPIの結果をテキストを入力したユーザーにわかりやすく加工する部分を準備します。

`app/process-node.tsx` を定義して、以下のコードを貼り付けましょう。

```tsx
import { ReactNode } from "react";
import { TypolessResponse } from "./form-action";

export function createFeedback(
  originalText: string,
  typolessResponses: TypolessResponse[]
) {
  if (typolessResponses.length === 0) {
    return "指摘はありません";
  }

  let index = 0;
  const reactNodes = typolessResponses.map((response) => {
    const keepTextNode = proccessKeepNode(
      originalText.slice(index, response.correction.position.start)
    );
    const errorNode = proccessErrorNode(response);
    index = response.correction.position.end;
    return combineNodes(keepTextNode, errorNode);
  });
  reactNodes.push(proccessKeepNode(originalText.slice(index)));

  return <>{reactNodes}</>;
}

function proccessErrorNode(response: TypolessResponse) {
  if (response.comment.includes("=>")) {
    return (
      <span
        title={response.comment}
        className="bg-blue-300 text-black"
        key={`repl-${response.correction.originalText}`}
      >
        {response.correction.originalText}
      </span>
    );
  }
  if (response.comment.includes("削除")) {
    return (
      <span
        title={response.comment}
        className="bg-red-300 text-black line-through"
        key={`del-${response.correction.originalText}`}
      >
        {response.correction.originalText}
      </span>
    );
  }
  if (response.comment.includes("挿入")) {
    return (
      <span
        title={response.comment}
        key={`ins-${response.correction.originalText}`}
      >
        {response.correction.originalText}
        <span className="bg-yellow-300 text-black">__</span>
      </span>
    );
  }
  return <></>;
}

function proccessKeepNode(text: string) {
  return (
    <span className="whitespace-pre" key={`keep-${text}`}>
      {text}
    </span>
  );
}

function combineNodes(node1: JSX.Element, node2: JSX.Element): ReactNode {
  return (
    <span key={`combined-${node1.key}-${node2.key}`}>
      {node1}
      {node2}
    </span>
  );
}
```

簡単に解説を行うと、TypolessのAIは3種類の指摘（置換・削除・挿入）をしてくれるので、それに応じた装飾を行なっています。

1. 「=>」がcommentに含まれている場合は置換の指示を表しており、文字を青背景で表示しています。
2. 挿入の場合はその文字の直後に_を表示しつつ、それを黄背景で表示しています。
3. 削除の場合は不必要と思われる文字に線を引きつつ、それを赤背景で表示しています。

今回のデモでは利用していませんが、AIの指摘にはそれぞれscore（0-1の範囲）が付与されており、これはAIの指摘に対する自信度を示しています。
自信度が一定未満の場合は指摘を表示しないようにすることなども可能です。

## 完成

この状態でビルドを実行します。

私はyarnを利用しているので`yarn dev`でビルドします。

```bash
# 初回実行時のみ
# yarn
$ yarn dev
yarn run v1.22.22
next dev
   ▲ Next.js 14.1.4
   - Local:        http://localhost:3000

 ✓ Ready in 1484ms
```

`http://localhost:3000` にアクセスすると、シンプルなテキストエリアとボタンが表示されていると思います。

![スクリーンショット 2024-04-07 17.44.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677068/04d6c367-5c47-d993-1b02-0658e0c415d2.png)

試しに誤りを含む文章を入力して校正をかけてみました。
送信ボタンの下に校正結果が表示されます。

また、エラーにカーソルを合わせると、修正方法の候補が提示されます。

![スクリーンショット 2024-04-07 17.45.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677068/d066bfdb-58c3-7ac9-2473-0b840df95c44.png)

今回はテキストエリアとその文章の校正結果が表示されるシンプルなアプリのご紹介となりましたが、記事中でも触れたように「ルール辞書」や「カスタム辞書」、AI指摘のscoreなどを活用することで、より完成度の高い校正を実現することができるでしょう。

## Typoless APIの情報はこちら

APIの仕様については以下からご確認いただけます。

https://mediardc.notion.site/Typoless-API-be3def50d18343199fef6fbe56ffa265

料金については以下からご確認いただけます。

https://typoless.asahi.com/price#api-plan-table