> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useinvent.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Remix

> Add your Invent assistant to Remix apps

## Overview

Add your Invent AI assistant to any Remix application. Perfect for full-stack React applications with nested routing and progressive enhancement.

## Installation

<Steps>
  <Step title="Create Assistant Component">
    ```tsx theme={"system"}
    // app/components/InventAssistant.tsx
    interface InventAssistantProps {
      assistantId: string;
      themeAppearance?: 'auto' | 'light' | 'dark';
      themeButtonBackgroundColor?: string;
      themeButtonColor?: string;
      userId?: string;
      userName?: string;
      userHash?: string;
      userAvatar?: string;
    }

    export default function InventAssistant({
      assistantId,
      themeAppearance = 'auto',
      themeButtonBackgroundColor,
      themeButtonColor,
      userId,
      userName,
      userHash,
      userAvatar,
    }: InventAssistantProps) {
      return (
        <>
          <invent-assistant
            assistant-id={assistantId}
            theme-appearance={themeAppearance}
            {...(themeButtonBackgroundColor && {
              'theme-button-background-color': themeButtonBackgroundColor,
            })}
            {...(themeButtonColor && {
              'theme-button-color': themeButtonColor,
            })}
            {...(userId && { 'user-id': userId })}
            {...(userName && { 'user-name': userName })}
            {...(userHash && { 'user-hash': userHash })}
            {...(userAvatar && { 'user-avatar': userAvatar })}
          />
          <script
            type="text/javascript"
            src="https://www.useinvent.com/embed.js"
            async
            defer
          />
        </>
      );
    }
    ```
  </Step>

  <Step title="Add to Root">
    ```tsx theme={"system"}
    // app/root.tsx
    import type { LinksFunction, LoaderFunctionArgs } from '@remix-run/node';
    import {
      Links,
      LiveReload,
      Meta,
      Outlet,
      Scripts,
      ScrollRestoration,
      useLoaderData,
    } from '@remix-run/react';
    import InventAssistant from './components/InventAssistant';

    export async function loader({ request }: LoaderFunctionArgs) {
      // Get user data from session
      const userData = await getUserData(request);

      return {
        assistantId: process.env.INVENT_ASSISTANT_ID,
        userData,
      };
    }

    export default function App() {
      const { assistantId, userData } = useLoaderData<typeof loader>();

      return (
        <html lang="en">
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <Meta />
            <Links />
          </head>
          <body>
            <Outlet />
            <ScrollRestoration />
            <Scripts />
            <LiveReload />
            <InventAssistant
              assistantId={assistantId}
              {...userData}
            />
          </body>
        </html>
      );
    }
    ```
  </Step>

  <Step title="Configure Environment">
    Add to `.env`:

    ```bash theme={"system"}
    INVENT_ASSISTANT_ID=ast_YOUR_ASSISTANT_ID
    INVENT_SECRET_KEY=your_secret_key_here
    ```
  </Step>
</Steps>

## User Authentication

<Warning>
  **Security Requirement:** When using any `user-*` attributes (`user-id`, `user-name`, `user-avatar`), you **must** also provide `user-hash`. Both `user-id` and `user-hash` must be provided together, or neither should be provided. The `user-hash` must be generated on your backend using HMAC-SHA256 with your assistant's secret key. Never expose the secret key to the client.
</Warning>

### Server-Side Hash Generation

Use Remix loaders for secure hash generation:

```typescript theme={"system"}
// app/root.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import crypto from 'crypto';
import { getUser } from '~/services/auth.server';

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await getUser(request);

  let userData = null;
  if (user) {
    const secretKey = process.env.INVENT_SECRET_KEY!;
    const userId = user.id;

    const userHash = crypto
      .createHmac('sha256', secretKey)
      .update(userId)
      .digest('hex');

    userData = {
      userId,
      userName: user.name,
      userAvatar: user.avatar,
      userHash,
    };
  }

  return json({
    assistantId: process.env.INVENT_ASSISTANT_ID!,
    userData,
  });
}
```

### Using Resource Routes

Create a resource route for dynamic user data:

```typescript theme={"system"}
// app/routes/api.user-hash.ts
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import crypto from 'crypto';
import { requireUser } from '~/services/auth.server';

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await requireUser(request);

  const secretKey = process.env.INVENT_SECRET_KEY!;
  const userId = user.id;

  const userHash = crypto
    .createHmac('sha256', secretKey)
    .update(userId)
    .digest('hex');

  return json({
    userId,
    userName: user.name,
    userAvatar: user.avatar,
    userHash,
  });
}
```

Then fetch in a component:

```tsx theme={"system"}
// app/components/InventAssistantWrapper.tsx
import { useFetcher } from '@remix-run/react';
import { useEffect } from 'react';
import InventAssistant from './InventAssistant';

export default function InventAssistantWrapper({
  assistantId,
}: {
  assistantId: string;
}) {
  const fetcher = useFetcher();

  useEffect(() => {
    if (fetcher.state === 'idle' && !fetcher.data) {
      fetcher.load('/api/user-hash');
    }
  }, [fetcher]);

  return (
    <InventAssistant
      assistantId={assistantId}
      {...(fetcher.data || {})}
    />
  );
}
```

## Integration with Remix Auth

```typescript theme={"system"}
// app/root.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import crypto from 'crypto';
import { authenticator } from '~/services/auth.server';

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await authenticator.isAuthenticated(request);

  let userData = null;
  if (user) {
    const secretKey = process.env.INVENT_SECRET_KEY!;
    const userHash = crypto
      .createHmac('sha256', secretKey)
      .update(user.id)
      .digest('hex');

    userData = {
      userId: user.id,
      userName: user.name,
      userAvatar: user.avatar,
      userHash,
    };
  }

  return json({
    assistantId: process.env.INVENT_ASSISTANT_ID!,
    userData,
    ENV: {
      // Only expose public variables
    },
  });
}
```

## Nested Routes with Context

Pass route-specific context:

```tsx theme={"system"}
// app/routes/blog.$slug.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { useEffect } from 'react';

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.slug);
  return json({ post });
}

export default function BlogPost() {
  const { post } = useLoaderData<typeof loader>();

  useEffect(() => {
    if (typeof window !== 'undefined') {
      window.inventContext = {
        pageType: 'blog-post',
        title: post.title,
        author: post.author,
        category: post.category,
      };
    }
  }, [post]);

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}
```

## TypeScript Support

Add type declarations:

```typescript theme={"system"}
// app/types/invent-assistant.d.ts
declare global {
  interface Window {
    inventContext?: Record<string, any>;
  }

  namespace JSX {
    interface IntrinsicElements {
      'invent-assistant': {
        'assistant-id': string;
        'theme-appearance'?: 'auto' | 'light' | 'dark';
        'theme-button-background-color'?: string;
        'theme-button-color'?: string;
        'user-id'?: string;
        'user-name'?: string;
        'user-hash'?: string;
        'user-avatar'?: string;
      };
    }
  }
}

export {};
```

## Using with Sessions

```typescript theme={"system"}
// app/services/session.server.ts
import { createCookieSessionStorage } from '@remix-run/node';
import crypto from 'crypto';

const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '__session',
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    secrets: [process.env.SESSION_SECRET!],
    sameSite: 'lax',
  },
});

export async function getUserData(request: Request) {
  const session = await sessionStorage.getSession(
    request.headers.get('Cookie')
  );

  const userId = session.get('userId');
  if (!userId) return null;

  const userHash = crypto
    .createHmac('sha256', process.env.INVENT_SECRET_KEY!)
    .update(userId)
    .digest('hex');

  return {
    userId,
    userName: session.get('userName'),
    userAvatar: session.get('userAvatar'),
    userHash,
  };
}
```

## Tips for Remix

<CardGroup cols={2}>
  <Card title="Server Loaders" icon="server">
    Use loaders for secure hash generation
  </Card>

  <Card title="Progressive Enhancement" icon="arrow-up-right">
    Works without JavaScript enabled
  </Card>

  <Card title="Nested Routes" icon="sitemap">
    Integrate with nested routing
  </Card>

  <Card title="Type Safety" icon="shield-check">
    Full TypeScript support
  </Card>
</CardGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Script loading multiple times">
    **Solutions:**

    * Add check if script already exists
    * Use proper cleanup in useEffect
    * Ensure component only renders once
    * Check for duplicate components in tree
  </Accordion>

  <Accordion title="Hydration mismatches">
    **Solutions:**

    * Ensure data is available on both server and client
    * Check that loader data is serializable
    * Verify useEffect cleanup
    * Test with JavaScript disabled
  </Accordion>

  <Accordion title="Environment variables not working">
    **Solutions:**

    * Restart dev server after adding variables
    * Don't prefix server-only variables
    * Never expose secret key to client
    * Check loader returns correct data
  </Accordion>
</AccordionGroup>

## Best Practices

<Note>
  * Always use loaders for server-side operations
  * Keep secret keys server-side only
  * Leverage Remix's built-in session management
  * Use resource routes for dynamic data
  * Test with progressive enhancement in mind
</Note>
