# AI Text Editor Plugin for the Next.js Supabase Saas Starter Kit

> This plugin adds an AI WYSIWYG Editor to your Next.js Supabase SaaS application.

*Canonical: https://makerkit.dev/docs/next-supabase/plugins/ai-text-editor*

---

This plugin adds an AI Editor component Next.js application. You can
import this component anywhere in your application.

It is built with Lexical, a text editor framework for React, by Meta.

**This plugin is currently experimental and not yet officially released.**

## Using the Plugin

### Installation

To install the plugin, you can use git subtrees from your original repository:

```
git subtree add --prefix plugins/text-editor git@github.com:makerkit/next-supabase-saas-kit-plugins.git text-editor --squash
```

After running this command, you will have the plugin in your repository at
`plugins/text-editor`. Once pulled, you can apply any customization you need.

#### Using the CLI

If you're using the CLI, you can run the following command to install the plugin:

```
npx @makerkit/cli@latest plugins install
```

Follow the instructions to install the plugin.

#### Add the plugin as a workspace in your package.json

You can do so by adding the following to your `package.json` file:

```json
{
  "workspaces": [
    "plugins/text-editor"
  ]
}
```

Add it next to the other workspaces in your `package.json` file.

#### Add the paths alias to the TypeScript configuration

To make sure that the TypeScript compiler can find the plugin, you will need
to add the following paths alias to your `tsconfig.json` file, in addition
to the other paths aliases that you may have.

If not yet present, add the following to your `tsconfig.json` file:

```json
{
  "compilerOptions": {
    "paths": {
      "~/plugins/*": [
        "./plugins/*"
      ]
    }
  }
}
```

You only need to add this once, even if you have multiple plugins.

### Installing dependencies

To install the dependencies, you can run the following command:

```
npm i
```

NPM will install the dependencies in the `plugins/text-editor` folder as an NPM
workspace.

### Add the required API handlers

You will need to add the following API handlers to your application:
- `api/ai/autocomplete/route.ts`
- `api/ai/edit/route.ts`

#### Edit Route

To add the edit route, create a file at `src/app/api/ai/edit/route.ts` with the
following content:

```ts
import { createAIEditRouteHandler } from '~/plugins/text-editor/lib/route-handler';

export const runtime = `edge`;

export const POST = createAIEditRouteHandler;
```

#### Autocomplete Route

To add the autocomplete route, create a file at `src/app/api/ai/autocomplete/route.ts` with the
following content:

```ts
import { createAIAutocompleteRouteHandler } from '~/plugins/text-editor/lib/route-handler';

export const runtime = `edge`;

export const POST = createAIAutocompleteRouteHandler;
```

### Style

Import the CSS file at `plugins/text-editor/editor.css` into your global CSS
file at `src/app/styles/globals.css`:

```css
@import "../../plugins/text-editor/editor.css";
```

### Using the plugin

To use the plugin, you can import the component from the plugin folder.

NB: the example below uses `rxjs`, which is a dependency not included in the
kit. I do recommend using it, but you can use any other library to handle
debouncing and other logic that you may need if you want to use autosaving
for your application.

```tsx
'use client';

import { useCallback, useEffect, useMemo, useRef } from 'react';
import { toast } from 'sonner';

import {
  debounceTime,
  distinctUntilChanged,
  Subject,
  switchMap,
  tap,
} from 'rxjs';

import Editor from '~/plugins/text-editor/components/Editor';

export default function EditorContainer() {
  const subject$ = useMemo(() => new Subject(), []);
  const currentToastId = useRef<string | number>();

  const onChange = useCallback(
    (content: string) => {
      subject$.next(content);
    },
    [subject$],
  );

  useEffect(() => {
    const subscription = subject$
      .pipe(
        debounceTime(2000),
        distinctUntilChanged(),
        switchMap(() => {
          if (currentToastId.current) {
            toast.dismiss(currentToastId.current);
          }

          currentToastId.current = toast.loading('Saving...', {
            id: currentToastId.current,
          });

          return new Promise((resolve) => {
            setTimeout(resolve, 2000);
          });
        }),
        tap(() => {
          toast.success('Saved!', {
            id: currentToastId.current,
          });
        }),
      )
      .subscribe(() => {
        currentToastId.current = undefined;
      });

    return () => {
      subscription.unsubscribe();
    };
  }, [subject$]);

  return (
    <>
      <Editor
        className={'h-[80vh] w-[100vh]'}
        content={getInitialContent()}
        onChange={onChange}
      />
    </>
  );
}

function getInitialContent() {
  return `
## Introducing Makerkit's AI Editor

This plugin is powered by OpenAI's GPT-3 and Lexical's Editor, and helps you add an AI-powered editor to your SaaS, in just a few lines of code.

Install it using the CLI:

\`\`\`
npx @makerkit/cli plugins install
\`\`\`

Available for *Pro* and *Teams* customers **for free**.
  `.trim();
}
```

Now, import the component `EditorContainer` anywhere in your application:

```tsx
import { Toaster } from 'sonner';
import dynamic from 'next/dynamic';

import PageLoadingIndicator from '~/core/ui/PageLoadingIndicator';
import { withI18n } from '~/i18n/with-i18n';
import I18nProvider from '~/i18n/I18nProvider';
import getLanguageCookie from '~/i18n/get-language-cookie';

const EditorContainer = dynamic(
  () => {
    return import('~/app/editor/EditorContainer');
  },
  {
    ssr: false,
    loading: () => (
      <PageLoadingIndicator fullPage={false}>Loading...</PageLoadingIndicator>
    ),
  },
);

function EditorPage() {
  return (
    <I18nProvider lang={getLanguageCookie()}>
      <div
        className={
          'w-screen h-screen flex justify-center items-center bg-gray-50' +
          ' dark:bg-dark-900 flex flex-col space-y-4'
        }
      >
        <Toaster />

        <EditorContainer />
      </div>
    </I18nProvider>
  );
}

export default withI18n(EditorPage);
```
