# AI Text Editor Plugin for the Remix Supabase Saas Starter Kit

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

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

---

This plugin adds an AI Editor component Remix 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.**

## 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/remix-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:
- `app/routes/resources.ai.autocomplete.ts`
- `app/routes/resources.ai.edit.ts`

#### Edit Route

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

```ts
import type { ActionFunctionArgs } from '@remix-run/node';
import { createAIEditActionHandler } from '~/plugins/text-editor/lib/route-handler';

export const action = (params: ActionFunctionArgs) =>
  createAIEditActionHandler(params.request);
```

#### Autocomplete Route

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

```ts
import type { ActionFunctionArgs } from '@remix-run/node';
import { createAIAutocompleteActionHandler } from '~/plugins/text-editor/lib/route-handler';

export const action = (params: ActionFunctionArgs) =>
  createAIAutocompleteActionHandler(params.request);
```

### Style

Import the CSS file at `plugins/text-editor/editor.css` into your global CSS
file at `app/styles/index.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
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 { lazy } from 'react';

import DarkModeToggle from '~/components/DarkModeToggle';
import Logo from '~/core/ui/Logo';
import ClientOnly from '~/core/ui/ClientOnly';

const EditorContainer = lazy(() => {
  return import('~/components/EditorContainer');
});

function EditorPage() {
  return (
    <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 />

      <div className={'fixed top-4 right-4'}>
        <DarkModeToggle />
      </div>

      <Logo href={'/'} className={'w-32 h-32'} />

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

export default EditorPage;
```
