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.
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
Now you have successfully implemented react-router-dom into your project, any references or technics, tricks you could found at here in this document: https://reactrouter.com/en/main/start/tutorial
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:
=> 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
enumROUTES { HOME ='/', INTERNAL_ERROR ='/error_500'}exportdefaultROUTES;
Create /components/layouts/main.tsx file
import { PropsWithChildren } from'react';typeProps=PropsWithChildren<{}>;functionMainLayout({ children }:Props) {// add your authentication logic herereturn <>{children}</>;}exporttypeMainLayoutProps=Props;exportdefault MainLayout;
Create /configs/keys.ts file
exportenumLAYOUT_TYPES { MAIN// add more layout keys here}
Create /configs/routes/ts file
enumROUTES { HOME ='/', INTERNAL_ERROR ='/error_500'// add more pages root path here}exportdefaultROUTES;
Create /components/layouts/index.tsx file
import { PropsWithChildren } from'react';import { LAYOUT_TYPES } from'configs/keys';import MainLayout, { MainLayoutProps } from'./main';typeLayoutProps=MainLayoutProps;// add more layout prop type here using union typetypeLayoutType=typeof MainLayout;// add more layout type here using union typetypeProps=PropsWithChildren<{ layoutType:LAYOUT_TYPES } &LayoutProps>;exportconstLayouts:Record<LAYOUT_TYPES,LayoutType> = { [LAYOUT_TYPES.MAIN]: MainLayout// add more layout component here};exportconstLayouts:Record<LAYOUT_TYPES,LayoutType> = { [LAYOUT_TYPES.MAIN]: MainLayout};functionLayoutPicker({ layoutType,...props }:Props) {constLayout= Layouts[layoutType];return <Layout {...(props asany)} />;}exportdefault 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';constroutes:RouteObject[] = [ {element: ( <LayoutPickerlayoutType={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];exportdefault 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:
Params
Type
Description
Required
access_key
string
Access token
Y
app_id
string
App id
Y
=> 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:
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';constbaseUrl:string=`/user/products`;constservice= baseService;// only use this function for requests that need authentication tokenfunctiongetAuthOption(options?:AxiosRequestConfig) {constres= { ...(options || {}) };consttoken=StorageUtils.getItem(QUERY_PARAMS.APP_KEY,'');if (!token) {thrownewError('Unauthenticated user rejected access'); }res.headers =res.headers || {};res.headers.Authorization =`Bearer ${token}`;return res;}constuserService= {genProduct<Res=GenProductCommonData>({ productId, method, payload }:QueryGenProduct) {constemittedData=!!payload? { method, payload }: { method, payload: {} };returnservice.post<ResGenProduct<Res>>(getReqPath(baseUrl,String(productId),USERS_ENDPOINT.USE), emittedData,getAuthOption() ); }};exportdefault 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
# 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:
asyncfunctionhandleSubmit(e:FormEvent<HTMLFormElement>) {e.preventDefault();// if appId wasn't passed or form already processing return every function callif (!appId || isLoading) return;setIsLoading(true);// get form data from form elementconstformData=Object.fromEntries(newFormData(e.currentTarget)) asRecord<'prompt'|'negativePrompt',string>;// making requestconst { res,error } =awaithandleRequest(userService.genProduct({ productId: appId, method:PRODUCT_METHOD.APP_4, payload: { width: defaultWith, height: defaultHeight, prompt:formData.prompt, negativePrompt:formData.negativePrompt } }));if (error ||!res) {returnhandleError(error?.response?.data?.message ||'Generation error');}handleSubmitted(res.data.data.taskId);}
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
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
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
Endpoint
/api/v1/user/products/{productId}/use
Method
POST
Header
Name
Type
Required
Authorization
Bearer token
Y
Path
Name
Type
Description
Required
productId
Integer
Product ID
Y
Body
Name
Type
Description
Required
method
String
Method name
Y
payload
Object
Payload of method
Y
Response
Name
Type
Description
Required
code
String
Error code. If code = "0", that mean successful
Y
message
String
Error message. If successful, the message is "success"
Y
data
Object
Y
> taskId
UUID
If the method is asynchronous, this field will be returned
N
> status
String
If the method is asynchronous, this field will be returned and will always be "Pending".
N
> … (any fields)
If the method is an immediate response, the fields will be returned in the format the publisher has defined for this method in the `usage tab`
N
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:
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
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.