Overview
Add your Invent AI assistant to any Remix application. Perfect for full-stack React applications with nested routing and progressive enhancement.
Installation
Create Assistant Component
// 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/button.js"
async
defer
/>
</>
);
}
Add to Root
// 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 >
);
}
Configure Environment
Add to .env
: INVENT_ASSISTANT_ID = ast_YOUR_ASSISTANT_ID
INVENT_SECRET_KEY = your_secret_key_here
User Authentication
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.
Server-Side Hash Generation
Use Remix loaders for secure hash generation:
// 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:
// 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:
// 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
// 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:
// 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:
// 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
// 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
Server Loaders Use loaders for secure hash generation
Progressive Enhancement Works without JavaScript enabled
Nested Routes Integrate with nested routing
Type Safety Full TypeScript support
Troubleshooting
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
Solutions:
Ensure data is available on both server and client
Check that loader data is serializable
Verify useEffect cleanup
Test with JavaScript disabled
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
Best Practices
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