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:
=> 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
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:
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.
Asynchronous: The response will always return `taskId` and `status: "Pending"`. The `Refresh result` api can be used to get the final result
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)
{
"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"`
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
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`
{
"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).
{
"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
}
}
=> Your terminal will show as below, use ctrl + click into the link will open your local dev app in your default browser. Congratulation, your project has been created successfully
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
Name
Type
Description
Required
taskId
UUID
Task ID
Y
response
Object
Is the result returned from the corresponding method when using the `Use product` api. The format is defined by the publisher in the response DTO of each method in the `usage tab`
Y
errorCode
Object
Y
> status
String
An error code
Y
> reason
String
Specifically describe the error that occurred
Y
extraType
String
Extra data type. Always is "chat_app"
N
Method
GET
Name
Type
Required
Authorization
Bearer token
Y
Name
Type
Description
Required
productId
Integer
Product ID
Y
taskId
UUID
Task ID from the response of api `Use product`
Y
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
> status
String
One of the following values: "Pending", "Completed", "Failed"
Y
> result
Object
Is the result returned from the corresponding method when using the `Use product` api. The format is defined by the publisher in the response DTO of each method in the `usage tab`
N
Method
GET
Name
Type
Required
Authorization
Bearer token
Y
Name
Type
Description
Required
productId
Integer
Product ID
Y
Name
Type
Description
Required
page
Integer
Page number to retrieve.If you provide invalid value the default page number will applied
Default Value: 1
N
limit
Integer
Number of records per page
Default Value: 20
Max Value: 100
Note: If provided value is greater than max value, max value will be applied.
N
status
String
Is one of the following values: "Pending", "Completed", "Failed"
N
sortBy
String
Is one of the following values: "id", "createdAt"
N
sortDirection
String
Is one of the following values: "ASC", "DESC"
N
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
Array<Object>
Y
> id
Integer
Unique id of record
Y
> taskId
UUID
Task ID
Y
> result
Object
If status is "Completed", this field will be the data recorded in the `response DTO` of the method the publisher defines in the usage tab. If the status is "Failed", the result will be information about the error
N
>status
String
Is one of the following values: "Pending", "Completed", "Failed"
Y
> dataType
String
Is one of the following values: "META_DATA", "S3_OBJECT", "HYBRID"
N
> createdAt
Date time (ISO)
At point in time which request is created
Y
> input
Object
The input data is the method's payload
Y
meta
Object
Y
> itemCount
Integer
The amount of items on this specific page
Y
> totalItems
Integer
The total amount of items
Y
> itemsPerPage
Integer
The amount of items that were requested per page
Y
> totalPages
Integer
The total amount of pages in this paginator
Y
> currentPage
Integer
The current page this paginator "points" to
Y
Method
GET
Name
Type
Required
Authorization
Bearer token
Y
Name
Type
Description
Required
productId
Integer
Product ID
Y
taskId
UUID
Task ID from response of the api `Use product`
Y
Name
Type
Description
Required
from
String
Is one of the following values: "user", "system"
Y
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
> id
Integer
Unique id của bản ghi
Y
> taskId
UUID
Task ID
Y
> sessionId
String
Is the session ID of the app that manages chat by session
N
> from
String
Is one of the following values: "user", "system"
Y
> data
Object
If from is "user", this field is the data in the payload in the `Use product` api, otherwise if from is "system", this field is the data in the response according to the format the publisher defines in the `usage tab`
Y
> dataType
String
Is one of the following values: "META_DATA", "S3_OBJECT", "HYBRID"
N
> createdAt
Date time (ISO)
Time which request is created
Y
Method
GET
Name
Type
Required
Authorization
Bearer token
Y
Name
Type
Description
Required
productId
Integer
Product ID
Y
Name
Type
Description
Required
page
Integer
Page number to retrieve.If you provide invalid value the default page number will applied
Default Value: 1
N
limit
Integer
Number of records per page
Default Value: 20
Max Value: 100
Note: If provided value is greater than max value, max value will be applied.
N
sessionId
String
Filter by sessionId
N
sortBy
String
Is one of the following values: "id", "createdAt"
N
sortDirection
String
Is one of the following values: "ASC", "DESC"
N
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
Array<Object>
Y
> id
Integer
Unique id của bản ghi
Y
> taskId
UUID
Task ID
Y
> sessionId
String
Is the session ID of the app that manages chat by session
N
> from
String
Is one of the following values: "user", "system"
Y
> data
Object
If from is "user", this field is the data in the payload in the `Use product` api, otherwise if from is "system", this field is the data in the response according to the format the publisher defines in the `usage tab`
Y
> dataType
String
Is one of the following values: "META_DATA", "S3_OBJECT", "HYBRID"