L3 Development Guide

1. FE Guide

1.1 Initialize project

  • This document is meant for developers who want to develop apps that could be published into our AI marketplace platform.

  • You could freely using any framework or library that you used to, but in this document we strongly recommend using ReactJS

  • First, make sure that you are using node version 18 or above, otherwise you have to upgrade your node version. This document will not cover that section for you, you have to do it by yourself.

  • To initialize the project, you first need to install Vite with bellow command

    NPM: npm install -D vite
    
    Yarn: yarn add -D vite
    
    Pnpm: pnpm add -D vite
    
    Bun: bun add -D vite
  • When Vite has been installed successfully, you could run init command to create ReactJS app following Vite - ReactJS + TS template

    NPM: npm create vite
    
    Yarn: yarn create vite
    
    Pnpm: pnpm create vite
    
    Bun: bun create vite

=> Your terminal will show configuration lines for project:

  • Project name you want

  • Package name:

  • Framework you want to create, at here we recommend using React

  • And template you want to use, at here we rec recommend using Typescript for better type secure

  • If everything has been configured correctly, your terminal will show

  • After the project has been created, navigate to the project folder and open your code editor (at here we using vscode):

  • To run the project, you first need to install all the dependencies, because Vite only provides you a template project but doesn’t pre-install all the dependencies for you.

    NPM: npm install
    
    Yarn: yarn
    
    Pnpm: pnpm install
    
    Bun: bun install
  • And finally after all the dependencies has been installed, you could run your project via command:

    NPM: npm run dev
    
    Yarn: yarn dev
    
    Pnpm: pnpm dev
    
    Bun: bun dev
  • But not yet, we just want you to install one more package, it’s react-router-dom. This package allows your react app to have multiple of pages and query params which very essential for following sections

    • To install react-router-dom, first you need to run install command:

      NPM: npm install react-router-dom

      Yarn: yarn add react-router-dom

      Pnpm: pnpm install react-router-dom

      Bun: bun install react-router-dom

    • After the package has been installed, you have to add routers into your project on your own, the package

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App.tsx';
    import { createBrowserRouter, RouterProvider } from 'react-router-dom';
    import './index.css';
    
    
    const router = createBrowserRouter([
      {
        path: '/',
        element: <App />
      }
    ]);
    
    
    ReactDOM.createRoot(document.getElementById('root')!).render(
      <React.StrictMode>
        <RouterProvider router={router} />
      </React.StrictMode>
    );
  • If you have completed all above steps, then the initialization has been done.

1.2. Project structure

  • This section will provide you with our recommendations for the project boilerplate, of course you could use your own boilerplate that you used to. But till the end of this document we are gonna use our project structure. This will cause you a lot of time to convert between our and your project structure, your decision.

  • At here you could see it’s default boilerplate that Vite has create for you, this is totally awesome

  • But we want you to add more folder that we serve their specific role and purpose in the project, you could see in below images:

    • Root folder - src folder:

  • Components folder, which contains the smallest unit as elements to larger such as module, layouts, features

  • Helper folders, which have configs folder including constant variable for project, helpers folder including helpers function for project and hooks folder including helpers hooks for project

  • Pages folder, which contains all the necessary pages you need for your app

  • Services folder, which contains all the necessary services you need for your app. Services folder including

    • Base service folder which contains common configuration for every service

    • Instances service folder which contains all the service that reuse base service configuration

    • Types folder which contains every type that are needed in requests, responses or maybe errors.

  • Types folder, which contains all the necessary global types you need for entire app, every file in here should have .d.ts extension to make every types, interfaces global scope and don’t have to use import / export

1.3. Configurations

  • In this section, we gonna discuss about necessary configuration for project base and custom variable for starting the project

  • Project base, in this part, we gonna setup the configuration for routing, authentication, services, hooks, helper functions:

1.3.1. Initial configs

  • Config alias in tsconfig-app.json

{
  "compilerOptions": {
    "composite": true,
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,


    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    // "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",
    "paths": {
      "@/*": ["./src/*"],
      "pages/*": ["./src/pages/*"],
      "components/*": ["./src/components/*"],
      "elements/*": ["./src/components/elements/*"],
      "modules/*": ["./src/components/modules/*"],
      "features/*": ["./src/components/features/*"],
      "layouts/*": ["./src/components/layouts/*"],
      "services/*": ["./src/services/*"],
      "helpers/*": ["./src/helpers/*"],
      "hooks/*": ["./src/hooks/*"],
      "configs/*": ["./src/configs/*"]
    },


    /* Linting */
    "strict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}
  • Config alias in vite.config.ts

import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';


type AppServerConfig = { host: string; port: number };


// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
  const env = loadEnv(mode, process.cwd(), '');


  const serverConfig: AppServerConfig = JSON.parse(
    env.VITE_SERVER_CONFIG || '{}'
  );
  return {
    plugins: [react()],
    server: {
      host: serverConfig.host || '0.0.0.0',
      port: serverConfig.port || 3000
    },
    preview: {
      host: serverConfig.host || '0.0.0.0',
      port: serverConfig.port || 3000
    },
    resolve: {
      alias: [
        { find: '@', replacement: path.resolve(__dirname, 'src') },
        { find: 'pages', replacement: path.resolve(__dirname, 'src/pages') },
        {
          find: 'components',
          replacement: path.resolve(__dirname, 'src/components')
        },
        {
          find: 'elements',
          replacement: path.resolve(__dirname, 'src/components/elements')
        },
        {
          find: 'modules',
          replacement: path.resolve(__dirname, 'src/components/modules')
        },
        {
          find: 'features',
          replacement: path.resolve(__dirname, 'src/components/features')
        },
        {
          find: 'layouts',
          replacement: path.resolve(__dirname, 'src/components/layouts')
        },
        {
          find: 'services',
          replacement: path.resolve(__dirname, 'src/services')
        },
        {
          find: 'helpers',
          replacement: path.resolve(__dirname, 'src/helpers')
        },
        { find: 'hooks', replacement: path.resolve(__dirname, 'src/hooks') },
        { find: 'configs', replacement: path.resolve(__dirname, 'src/configs') }
      ]
    },
    envPrefix: ['VITE_', 'REACT_']
  };
});

1.3.2. Routing

Create /pages/routes.tsx file

import { RouteObject } from 'react-router-dom';
import Home from 'pages/home';


const routes: RouteObject[] = [
  {
    path: '/',
    Component: Home
  }
];


export default routes;

And import the object into main.tsx file:

import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import routes from 'pages/routes';


const router = createBrowserRouter(routes);


ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

=> At this step the simple version of configuration has been done, but in the real use case, you may have a lot of screens, pages and some of them may share the same authentication logic or layout. Then you could try our recommendation below:

Create /configs/routes.ts file

enum ROUTES {
  HOME = '/',
  INTERNAL_ERROR = '/error_500'
}


export default ROUTES;

Create /components/layouts/main.tsx file

import { PropsWithChildren } from 'react';


type Props = PropsWithChildren<{}>;


function MainLayout({ children }: Props) {
  // add your authentication logic here
  return <>{children}</>;
}


export type MainLayoutProps = Props;
export default MainLayout;

Create /configs/keys.ts file

export enum LAYOUT_TYPES {
  MAIN
  // add more layout keys here
}

Create /configs/routes/ts file

enum ROUTES {
  HOME = '/',
  INTERNAL_ERROR = '/error_500'
  // add more pages root path here
}

export default ROUTES;

Create /components/layouts/index.tsx file

import { PropsWithChildren } from 'react';
import { LAYOUT_TYPES } from 'configs/keys';
import MainLayout, { MainLayoutProps } from './main';


type LayoutProps = MainLayoutProps;
// add more layout prop type here using union type


type LayoutType = typeof MainLayout;
//  add more layout type here using union type


type Props = PropsWithChildren<{ layoutType: LAYOUT_TYPES } & LayoutProps>;


export const Layouts: Record<LAYOUT_TYPES, LayoutType> = {
  [LAYOUT_TYPES.MAIN]: MainLayout
  // add more layout component here
};


export const Layouts: Record<LAYOUT_TYPES, LayoutType> = {
  [LAYOUT_TYPES.MAIN]: MainLayout
};


function LayoutPicker({ layoutType, ...props }: Props) {
  const Layout = Layouts[layoutType];
  return <Layout {...(props as any)} />;
}


export default LayoutPicker;

Create /pages/error.tsx file

import { Outlet, RouteObject } from 'react-router-dom';
import Home from 'pages/home';
import LayoutPicker from 'components/layouts';
import { LAYOUT_TYPES } from 'configs/keys';
import ROUTES from 'configs/routes';


const routes: RouteObject[] = [
  {
    element: (
      <LayoutPicker layoutType={LAYOUT_TYPES.MAIN}>
        <Outlet />
      </LayoutPicker>
    ),
    children: [
      {
        path: ROUTES.HOME,
        Component: Home
      },
      // add new page in here
      {
        path: ROUTES.INTERNAL_ERROR,
        Component: ErrorPage
      },
      {
        path: '*',
        element: <ErrorPage />
      }
    ]
  }
  // create more layout, pages in here
];


export default routes;

=> Now you have successfully completed configuration for routing in the app, if you need to add more pages or layout, just have to copy or follow the guideline comments above.

1.3.3. Authentication

Because the app could embedded to any consumer site which will surely using the login/registration part therefore we strongly recommend your app no need to handle those works, just simply let consumer site pass authentication keys to the app via query params

  • Query params:

=> Example url: https://your_domain.com/your_app_page?access_key=user-access-token&app_id=app-id-registered

  • Authentication in your pages and get authentication keys:

Get authentication keys:

Add query param keys in /configs/keys.ts file

export enum QUERY_PARAMS {
  APP_KEY = 'access_key', // string
  APP_ID = 'app_id' // string | number
  // add more query params here
}

Create /helpers/handlers.ts file

// handle a promise
export async function handleRequest<T, E = any>(
  promise: Promise<T>
): Promise<{ res: T; error: null } | { res: null; error: E }> {
  try {
    return { res: await promise, error: null };
  } catch (error) {
    return { res: null, error: error as E };
  }
}


// handle a callback which is async
export async function handleAsync<T, A>(
  args: A,
  cb: (args: A) => Promise<T>
): Promise<{ res: T | null; error: any | null }> {
  try {
    return { res: await cb(args), error: null };
  } catch (error) {
    return { res: null, error };
  }
}


// handle a function which could cause error
export function handleException<T, A>(
  args: A,
  cb: (args: A) => T
): { res: T | null; error: unknown | null } {
  try {
    return { res: cb(args), error: null };
  } catch (error) {
    return { res: null, error };
  }
}

Create /helpers/browser-storage.ts file

export default class StorageUtils {
  static setItem(key: string, value: string) {
    try {
      window?.localStorage?.setItem(key, value);
    } catch (error) {}
  }


  static getItem(key: string, defaultValue: string) {
    try {
      const result = window?.localStorage?.getItem(key);
      if (result === null || result === undefined) return defaultValue;
      return result;
    } catch (error) {}
  }
}

Install jwt-decode package:

NPM: npm install jwt-decode
Yarn: yarn add jwt-decode
Pnpm: pnpm install jwt-decode
Bun: bun install jwt-decode

Create /hooks/useAuth.ts file

import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { jwtDecode } from 'jwt-decode';
import moment from 'moment';
import StorageUtils from 'helpers/browser-storage';
import { QUERY_PARAMS } from 'configs/keys';
import { handleException } from 'helpers/handlers';


type ReturnType = {
  isAuth: boolean;
  accessKey: string | null;
  appId: string | null;
};


type AppAuth = Omit<ReturnType, 'isAuth'>;


const initialAppAuth: AppAuth = {
  accessKey: null,
  appId: null
};


export default function useAuth(): ReturnType {
  // get query params
  const [queryParams] = useSearchParams();
  const accessKeyParam = queryParams.get(QUERY_PARAMS.APP_KEY);
  const appIdParam = queryParams.get(QUERY_PARAMS.APP_ID);


  // hook states
  const [isAuth, setIsAuth] = useState<boolean>(true);
  const [userAuth, setUserAuth] = useState<AppAuth>(initialAppAuth);


  function saveUserAuth(p: { accessKey: string | null; appId: string | null }) {
    if (!p.accessKey) {
      StorageUtils.removeItem(QUERY_PARAMS.APP_KEY);
    }
    if (!p.appId) {
      StorageUtils.removeItem(QUERY_PARAMS.APP_ID);
    }
    if (p.accessKey) {
      StorageUtils.setItem(QUERY_PARAMS.APP_KEY, p.accessKey);
    }
    if (p.appId) {
      StorageUtils.setItem(QUERY_PARAMS.APP_ID, p.appId);
    }
  }


  function onAuthError() {
    setUserAuth({ ...initialAppAuth });
    setIsAuth(false);
    saveUserAuth(initialAppAuth);
  }


  function onAuthPassed(accessKey: string, appId: string) {
    setUserAuth({ accessKey, appId });
    setIsAuth(true);
    saveUserAuth({ accessKey, appId });
  }


  function handleCheckAuth(
    accessKey: string | undefined | null,
    appId: string | undefined | null
  ) {
    // if one of required params is missing then return un-auth
    if (!accessKey || !appId) {
      return onAuthError();
    }


    const { res: decode, error } = handleException(accessKey, (token) =>
      jwtDecode<tokenDecodePayload>(token)
    );


    // if token couldn't decode then return un-auth
    if (error || !decode) {
      return onAuthError();
    }


    // if expDate of token is lower than current time
    const expDate = moment.unix(Number(decode.exp)).utc();
    if (expDate.isSameOrBefore(moment.utc())) return onAuthError();


    // if everything is passed then user is auth
    return onAuthPassed(accessKey, appId);
  }


  useEffect(() => {
    const accessKeyLocalStore = StorageUtils.getItem(QUERY_PARAMS.APP_KEY, '');
    const appIdLocalStore = StorageUtils.getItem(QUERY_PARAMS.APP_ID, '');


    handleCheckAuth(
      accessKeyLocalStore || accessKeyParam,
      appIdLocalStore || appIdParam
    );
  }, [accessKeyParam, appIdParam]);


  return { isAuth, ...userAuth };
}

Then modify /layouts/main.tsx file

import useAuth from 'hooks/useAuth';
import ErrorPage from 'pages/error';
import { PropsWithChildren } from 'react';


type Props = PropsWithChildren<{}>;


function MainLayout({ children }: Props) {
  const { isAuth } = useAuth();


  if (!isAuth) return <ErrorPage />;


  return <>{children}</>;
}


export type MainLayoutProps = Props;
export default MainLayout;

=> After doing this, all the page that share main layout should have the same authentication

1.3.4. Service

This section is optional, feel free to implement your own services configs and function

On this section, we will provide to you our simple service configs

First, we create base config in /services/base/index.ts file

import axios, { AxiosError, AxiosResponse } from 'axios';


const baseUri = import.meta.env.REACT_APP_BASE_URI;


const baseService = axios.create({
  withCredentials: false,
  responseType: 'json',
  baseURL: baseUri || '',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json'
  }
});


function handleInterceptRequest(config: any) {
  return config;
}
function handleSuccess(response: AxiosResponse<any>) {
  return response;
}


function handleError(error: AxiosError) {
  throw error;
}
baseService.interceptors.request.use(handleInterceptRequest);


baseService.interceptors.response.use(handleSuccess, handleError);


export default baseService;

Then, create /services/configs/index.ts, this file will contain all the endpoints or static variables

export enum USERS_ENDPOINT {
  USE = 'use'
}

Then, create /configs/product-methods.ts file, same as above file, this file will contain all the method that we gonna call to Layer 1, layer 2 APIs

export enum PRODUCT_METHOD {
  APP_3 = 'image_enhancement'
}

After that, create /services/types/requests.ts file, which define types for request in the API calls

import { PRODUCT_METHOD } from 'configs/product-methods';


type ImageResolutionPayload = {
  url: string;
  rescalingFactor: number;
};


//query to generate product
export type QueryGenProduct = {
  productId: string;
} & {
  method: PRODUCT_METHOD.APP_3;
  payload: ImageResolutionPayload;
};

Create /services/types/response.ts file, which define types for response in the API calls

export type GenProductCommonData = {
  taskId: string;
};

export type ResGenProduct<T = GenProductCommonData> = {
  data: T;
};

Finally, create /services/instances/user.ts file, which hold a group of APIs with the same endpoint or same purpose

import { AxiosRequestConfig } from 'axios';
import baseService from '../base';
import { USERS_ENDPOINT } from '../configs';
import { GenProductCommonData, ResGenProduct } from '../types/response';
import { QueryGenProduct } from '../types/request';
import { QUERY_PARAMS } from 'configs/keys';
import { getReqPath } from '@/helpers';
import StorageUtils from 'helpers/browser-storage';


const baseUrl: string = `/user/products`;
const service = baseService;


// only use this function for requests that need authentication token
function getAuthOption(options?: AxiosRequestConfig) {
  const res = { ...(options || {}) };
  const token = StorageUtils.getItem(QUERY_PARAMS.APP_KEY, '');
  if (!token) {
    throw new Error('Unauthenticated user rejected access');
  }


  res.headers = res.headers || {};
  res.headers.Authorization = `Bearer ${token}`;


  return res;
}


const userService = {
  genProduct<Res = GenProductCommonData>({
    productId,
    method,
    payload
  }: QueryGenProduct) {
    const emittedData = !!payload
      ? {
          method,
          payload
        }
      : { method, payload: {} };
    return service.post<ResGenProduct<Res>>(
      getReqPath(baseUrl, String(productId), USERS_ENDPOINT.USE),
      emittedData,
      getAuthOption()
    );
  }
};


export default userService;

=> After doing this, you have configured services successfully. Repeat all the steps if you want to create more services for further use.

Note that, in the example above we have to develop apps that serve for both user and publisher use, therefore we have to configure service for both user and publisher roles. You could use only one role for your entire app that is totally fine and has no restrictions. Just find a suitable approach that fits your business as much as possible.

  • Final item in this section, custom variable for starting the project

    • For this item, we gonna use env variables

    • Note that, we are using typescript, therefore even env variables must have type secure

    • Steps:

First, modify src/vite-env.d.ts file

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_SERVER_CONFIG?: `"host":"${string}" , "port":"${number}"`;
  readonly REACT_APP_BASE_URI?: string;
  // more env variables...
}


interface ImportMeta {
  readonly env: ImportMetaEnv;
}

Then, create .env file at the root of the project

VITE_SERVER_CONFIG={"host":"0.0.0.0", "port":"3200"}
REACT_APP_BASE_URI=https://your_api_domain.com/api/v1

# Note that, VITE_SERVER_CONFIG env is meant for both development process and production run, which decide your app host and port. Meanwhile, REACT_APP_BASE_URI will provide an API domain for your app which could be different from deployments.

=> After doing this, you have configured your app successfully. Further configurations please visit https://vitejs.dev/config/

1.4. Simple process

  • After every configuration has been set, let's try to create a simple app which allows users to type in their prompt and AI will create a corresponding gif image.

  • First, create a form which could receive a prompt in home page with a little styling:

<div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center'
      }}
    >
      <h1
        style={{
          marginBottom: '20px'
        }}
      >
        Example App
      </h1>
      {isLoading && <p>Processing...</p>}
      <form onSubmit={handleSubmit} onChange={handleHideError}>
        <label>
          <div style={{ marginBottom: '10px' }}>Prompt:</div>
          <textarea name='prompt' style={{ width: '400px' }} />
        </label>
        <label>
          <div style={{ marginBottom: '10px' }}>Negative prompt:</div>
          <textarea name='negativePrompt' style={{ width: '400px' }} />
        </label>
        {error && <p style={{ color: 'red' }}> {error}</p>}
        <div style={{ textAlign: 'center', marginTop: '10px' }}>
          <button type='submit'>Submit</button>
        </div>
      </form>
</div>

=> The UI should look like this:

  • Then, create status states

  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [error, setError] = useState<string>('');
  const [taskId, setTaskId] = useState<string>('');
  const [output, setOutput] = useState<string>('');
  • After that, create utility function for each of following process:

    • When submitted

      function handleSubmitted(taskId: string) {
        setError('');
        setTaskId(taskId);
      }
    • When error happened

      function handleError(errMsg: string) {
        setError(errMsg);
        setIsLoading(false);
      }
    • When retry input if having error

      function handleHideError() {
        if (error.length < 1) return;
        setError('');
      }
    • When received result

      function handleSuccess(output: string) {
        setError('');
        setTaskId('');
        setOutput(output);
        setIsLoading(false);
      }
  • Create function for sending prompt API call

First, get AppId

const { appId } = useAuth();

Then, create image default width, height in outer scope of component

const defaultWith = 400;
const defaultHeight = 400;

function Home() {
// Component logic
}

Then, create function to submit prompt

async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
// if appId wasn't passed or form already processing return every function call
if (!appId || isLoading) return;


setIsLoading(true);


// get form data from form element
const formData = Object.fromEntries(
    new FormData(e.currentTarget)
) as Record<'prompt' | 'negativePrompt', string>;


// making request
const { res, error } = await handleRequest(
    userService.genProduct({
    productId: appId,
    method: PRODUCT_METHOD.APP_4,
    payload: {
        width: defaultWith,
        height: defaultHeight,
        prompt: formData.prompt,
        negativePrompt: formData.negativePrompt
    }
    })
);


if (error || !res) {
    return handleError(error?.response?.data?.message || 'Generation error');
}
handleSubmitted(res.data.data.taskId);
}
  • Create interval for listening to the result

First, create a ref

const interval = useRef<NodeJS.Timeout | null>(null);

Then, function to clear interval

function handleClearInterval() {
if (!interval.current) return;


clearInterval(interval.current);
interval.current = null;
}

After that, assign the interval to interval ref, and wrap it all inside a useEffect hook

useEffect(() => {
if (taskId.length < 1) return;


handleClearInterval();


interval.current = setInterval(() => {
    handleGetResult(taskId);
}, 15000);


return () => {
    handleClearInterval();
};
}, [taskId]);
  • Create function for receiving API call

  async function handleGetResult(taskId: string) {
    if (!appId) return;
    const { res, error } = await handleRequest(
      userService.getResultProduct<{ data: string }>({
        productId: appId,
        taskId
      })
    );
    if (error || !res) {
      return handleError(error?.response?.data?.message || 'Generation error');
    }
    // if Layer 1 and layer 2 process has been completed successfully clear interval and display the result
    if (res.data.data.status === PRODUCT_GENERATE_STS.Completed) {
      handleClearInterval();
      handleSuccess(res.data.data.result?.data || '');
    }
  }
  • Create a simple UI for displaying the result

      </form>
      {output && (
        <>
          <h2>Output image:</h2>
          <img
            src={output}
            width={defaultWith}
            height={defaultHeight}
            alt='Out put gif image'
          />
        </>
      )}
    </div>

1.5 Recommend flow

  • In this section, we will give more recommendations about how you should embed your L3 app in L2 L1 apps rather than implement your app logic internally. That should be you and your team to decide which one is the best approach due to your team's current status.

  • Firstly, as we did before in the above section, L3 apps could be embedded in any of L2 or L1 apps therefore L3 apps should rely on authentication modules and take authentication keys via search params. The best way to implement this is to create a global layout or global store to save those keys into your app global provider, which allow any components of your app to receive the data and maybe do some awesome feature with it

  • After that, because L3 app might use for embedded purpose therefore you should worry about responsive, permission restriction, cross-origin policies issue

=> Recommend for the embedded

<iframe
     style={{ border: "1px solid" }}
     width={appResolution.width}
     height={appResolution.height}
     src={appURL}
     sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-downloads allow-pointer-lock allow-storage-access-by-user-activation"
     allow="accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; clipboard-write;"
/>
  • In Conclusion, your next L3 app should follow this flow

1.6 Build and production run

  • For the production use, first you have to config server config env variable to your desired host and port, and other env variables that your app business logic need including base URI

VITE_SERVER_CONFIG={"host":"0.0.0.0", "port":"3200"}
REACT_APP_BASE_URI=https://your_api_domain.com/api/v1

After configured the env variables, you have to install all the dependencies, by the following command:

NPM: npm install

Yarn: yarn

Pnpm: pnpm install

Bun: bun install

When all the needed dependencies are installed, you need to build (package) your project into production version by the following command:

NPM: npm run build

Yarn: yarn build

Pnpm: pnpm build

Bun: bun run build

When the production build is ready, run your app by the following command:

NPM: npm run preview

Yarn: yarn preview

Pnpm: pnpm preview

Bun: bun preview

2. API Guide

2.1 Use product

This API enables users to pass payload from the API /call that the publisher defined.

  • Endpoint

  • Header

  • Path

  • Body

  • Response

Case 1: method is a task that needs to be processed asynchronously. These tasks are considered to take a considerable amount of time to process and cannot respond immediately in this api's response.

Then, this Api will respond with a `taskId` and `status: "Pending"`. We will be able to use this `taskId` to check the result of this request using the Refresh result api. Refer to this example below with a Gif Generator app:

Example Request:

curl -X 'POST' \
  '<host>/api/v1/user/products/53/use' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "method": "text2gif",
  "payload": {
    "prompt": "a cute girl is smiling",
    "height": 512,
    "width": 512
  }
}
'

Example response:

{
  "code": "0",
  "message": "success",
  "data": {
    "taskId": "f9f0f918-3a27-485a-a92f-11a63a2b3630",
    "status": "Pending",
  }
}

Method `text2gif` will create a gif file from the data received in the payload. This job will definitely take a long time to process. If the system waits for it to finish processing and return it to the user, it will create a bottleneck that clogs the system. Therefore, the system will return a `taskId` as the task identifier for this request as `status: "Pending"` to indicate that this task has been recorded and is in process.

Explain further why we save app usage history in 2 ways. There will be 2 api to get app usage results with normal methods (Refresh result, Get use app histories) and 2 api to get app usage results with chat methods (Refresh result, Get use app histories):

  • With standard methods, the returned results will be updated to the request record previously saved in the database.

  • With chat methods, the returned results will be saved as a new record in the database. This will help display chat message results easily.

Case 2: Method is a job that can be response immediately (for example: querying data from the database, or updating data into the database,...).

It is simply the opposite of case 1. Then all you want from this api will be in its response.

See the example below with the Role Play Chat Bot app

Example Request:

curl -X 'POST' \
  '<host>/api/v1/user/products/324/use' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "method": "listCharacter",
  "payload": {
    "page": 1,
    "size": 10
  }
}
'

Example response:

{
  "code": "0",
  "message": "success",
  "data": {
    "dataType": "META_DATA",
    "data": {
      "characters": [
        {
          "id": "b3971500-f6b5-41b5-a80b-2d9bc9149323",
          "name": "Travel Guide",
          "baseBot": "gpt-3.5-turbo-0125",
          "image": "https://example.com/ecbdbffc-6051-4676-9828-1a15b0d4b187-1049e9d7-1d7a-4890-ad1d-0a94b1917220-1.jpg"
        },
        {
          "id": "335be667-69eb-4bd9-81a2-8f2e110b03d7",
          "name": "Bookworm Betty",
          "baseBot": "llama3-70b-8192",
          "image": "https://example.com/ecbdbffc-6051-4676-9828-1a15b0d4b187-1049e9d7-1d7a-4890-ad1d-0a94b1917220-1.jpg"
        }
      ],
      "total": 2,
      "page": 1,
      "size": 10,
      "pages": 1
    }
  }
}

Role Play Chat Bot app allows users to chat with AI bots, each bot will be able to answer one or more areas well that you need answered. The list of bots has been created by the publisher and stored in the database, so when we need to display the list of bots, we just need to query the database and return the list of available bots. This work is quite light and can be answered immediately in the response of this api. That's why it's called immediate response work. Method `listCharacter` is an example of such work.

In short, we will have 2 types of data that we can receive from the response of this api.

  1. Asynchronous: The response will always return `taskId` and `status: "Pending"`. The `Refresh result` api can be used to get the final result

  2. Synchronous (immediate response): The response will return the data defined by the publisher. There is no taskId and no need to use the `Refresh result` api to get the final result.

All Jobs (method + payload) will be defined and recorded by the publisher in the `Request DTO`, `Response DTO` section in the usage tab. They will also define which methods are synchronous and which methods are asynchronous (immediate response).

In cases where an error occurs:

  • With error validating field `method`, `payload` (invalid, missing): Response returns http error 400 (Bad Request)

Example request:

curl -X 'POST' \
  '<host>/api/v1/user/products/324/use' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "method": "listCharacter"
}
'

Example response:

{
  "statusCode": 400,
  "error": "BadRequestException",
  "message": {
    "payload": "payload should not be empty, payload must be an object"
  }
}
  • If it does not fall within the above two errors, the request will be forwarded to the AI ​​server for processing. Then, if the input does not satisfy the AI ​​app's validation, the system will still return the error http 400 (Bad Request) with `code` as the error code and `message` as an error returned by the AI ​​app.

For example below with the Gif Generator App, the App has only one method, `text2gif`. Passing the method `gif_gen` is not supported, so AI Server returns the error `"message": "unsupported method gif_gen"` and `"code": "GIF_403"`

Example Request:

curl -X 'POST' \
  '<host>/api/v1/user/products/53/use' \
  -H 'accept: application/json' \
  -H 'x-product-api-key: <api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
  "method": "gif_gen",
  "payload": {
    "prompt": "a cute girl is smiling",
    "height": 512,
    "width": 512,
    "negativePrompt": ""
    }
}'

Example response:

{
  "code": "GIF_403",
  "statusCode": 400,
  "error": "BadRequestException",
  "message": "unsupported method gif_gen"
}

Regarding the callbackUrl field.

  • For example, you are developing a Layer 2 app, you have previously purchased a Layer 1 app, and your Layer 2 app needs to use it.

  • You will still need the Use Product, Refresh result or Refresh result for chat app api to be able to use the layer 1 app.

  • However, this requires you to proactively retrieve the results (by using the Refresh result or Refresh result for chat app).

  • To be able to receive results passively, use callbackUrl. Marketplace will proactively call it with the data that is the result of the Layer 1 app.

With the callback api, there are some requirements users need to follow:

  • Http method: POST

  • The request header will contain `x-marketplace-token`,please review before proceeding to ensure that the request is coming from Marketplace

  • If received, please respond with a status of 200

Note:

  • If it fails or exceeds the timeout (10s), the system will try to call the api callback every 3s up to 3 times.

  • The payload of the callback api will include the following fields:

2.2 Refresh result

This api is used to receive the return results of the asynchronous method received from the response of the `Use product` api. Method has `extra_type` which is not `chat_app`

  • Endpoint

  • Header

  • Path

  • Response

Continuing the example with Gif Generator above, below is an example result of method `text2gif` for the cases Pending, Completed, Failed

Example Request:

curl -X 'GET' \
  '<host>/api/v1/user/products/53/refresh/800e5ab2-a267-4283-95a0-ddd7d095cc63' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer <access_token>'

Example response for Pending:

{
  "code": "0",
  "message": "success",
  "data": {
    "status": "Pending"
  }
}

Example response for Success:

{
  "code": "0",
  "message": "success",
  "data": {
    "status": "Completed",
    "result": {
      "data": "https://example.com/GIFGenerator/5e7e1a54-386a-42d1-a25e-2b30a915e6b1.gif"
    }
  }
}

Example response for Failed:

{
  "code": "0",
  "message": "success",
  "data": {
    "result": {
       "error": [{"message": "Save file error"}]
    },
    "status": "Failed"
  }
}

2.3 Get use app histories

This API is used to get the usage history of asynchronous methods (methods that respond immediately will not be included here).

  • Endpoint

  • Header

  • Path

  • Param

  • Response

Example Request:

curl -X 'GET' \
  '<host>/api/v1/user/products/53/usage-histories?page=1&limit=10' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer <access_token>'

Example Response

{
  "code": "0",
  "message": "success",
  "data": [
    {
      "id": 1172,
      "productId": 53,
      "taskId": "800e5ab2-a267-4283-95a0-ddd7d095cc63",
      "status": "Completed",
      "dataType": "S3_OBJECT",
      "result": {
        "data": "https://example.com/GIFGenerator/5e7e1a54-386a-42d1-a25e-2b30a915e6b1.gif"
      },
      "input": {
        "seed": -1,
        "width": 512,
        "height": 512,
        "prompt": "a cat is running",
        "negativePrompt": "no"
      },
      "createdAt": "2024-06-25T09:49:46.654Z"
    },
    {
      "id": 1173,
      "productId": 53,
      "taskId": "a70560a7-94e5-483f-a49a-024c4409cd8b",
      "status": "Completed",
      "dataType": "S3_OBJECT",
      "result": {
        "data": "https://example.com/GIFGenerator/1ce47aa8-73e6-418c-8b01-f7fcc2c9533d.gif"
      },
      "input": {
        "seed": -1,
        "width": 512,
        "height": 512,
        "prompt": "a girl is smiling and there are trees",
        "negativePrompt": "non"
      },
      "createdAt": "2024-06-25T10:03:56.782Z"
    }
  ],
  "meta": {
    "itemsPerPage": 10,
    "totalItems": 2,
    "currentPage": 1,
    "totalPages": 1
  }
}

2.4 Refresh result for chat app

Like the refresh result api in section 2, this api is also used to receive the return results of the asynchronous method received from the response of the `Use product` api. But for Method there is `extra_type` which is `chat_app`

  • Endpoint

  • Header

  • Path

  • Param

  • Response

Example Request:

curl -X 'GET' \
'<host>/api/v1/user/products/324/chat-app/refresh/fd6db5ed-428d-4967-9aa0-cf9350330b31?from=system' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer <access_token>'

Example Response:

{
  "code": "0",
  "message": "success",
  "data": {
    "id": 389,
    "taskId": "fd6db5ed-428d-4967-9aa0-cf9350330b31",
    "sessionId": "f16f1631-c540-4fec-bfda-9ea3a20a2cf9",
    "from": "system",
    "data": {
      "data": {
        "image": "",
        "answer": "Hello again! I'm Test, and I'm here to chat about travel and quality control. If you're up for it, let's chat about your bucket list destinations. What's the first place that comes to mind when you dream about your next adventure?"
      },
      "dataType": "META_DATA"
    },
    "dataType": "META_DATA",
    "createdAt": "2024-07-08T09:36:31.110Z"
  }
}

2.5 Get use app histories for chat app

This API is used to get the usage history of asynchronous methods (methods that respond immediately will not be included here) with chat applications (eg chat bots).

  • Endpoint

  • Header

  • Path

  • Param

  • Response

Example Request:

curl -X 'GET' \
'<host>/api/v1/user/products/324/chat-app/histories?page=1&limit=10' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer <access_token>'

Example Response:

{
  "code": "0",
  "message": "success",
  "data": [
    {
      "id": 389,
      "taskId": "fd6db5ed-428d-4967-9aa0-cf9350330b31",
      "sessionId": "f16f1631-c540-4fec-bfda-9ea3a20a2cf9",
      "from": "system",
      "data": {
        "data": {
          "image": "",
          "answer": "Hello again! I'm Test, and I'm here to chat about travel and quality control. If you're up for it, let's chat about your bucket list destinations. What's the first place that comes to mind when you dream about your next adventure?"
        },
        "dataType": "META_DATA"
      },
      "dataType": "META_DATA",
      "createdAt": "2024-07-08T09:36:31.110Z"
    },
    {
      "id": 388,
      "taskId": "fd6db5ed-428d-4967-9aa0-cf9350330b31",
      "sessionId": "f16f1631-c540-4fec-bfda-9ea3a20a2cf9",
      "from": "user",
      "data": {
        "message": "hi",
        "sessionId": "f16f1631-c540-4fec-bfda-9ea3a20a2cf9",
        "characterId": "907f1d2f-bf96-4b8f-a77d-cdffe758749a"
      },
      "dataType": null,
      "createdAt": "2024-07-08T09:33:31.026Z"
    }
  ],
  "meta": {
    "itemsPerPage": 10,
    "totalItems": 2,
    "currentPage": 1,
    "totalPages": 1
  }
}

Last updated