Merge branch 'preview/advisors' into 'develop'
Preview/advisors See merge request witapp/aura-webapp!63
This commit is contained in:
commit
882dbc4303
@ -58,11 +58,11 @@
|
|||||||
region: "eu",
|
region: "eu",
|
||||||
});
|
});
|
||||||
</script> -->
|
</script> -->
|
||||||
<script
|
<!-- <script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="//script.crazyegg.com/pages/scripts/0121/5622.js"
|
src="//script.crazyegg.com/pages/scripts/0121/5622.js"
|
||||||
async="async"
|
async="async"
|
||||||
></script>
|
></script> -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
18
package-lock.json
generated
18
package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chargebee/chargebee-js-react-wrapper": "^0.6.3",
|
"@chargebee/chargebee-js-react-wrapper": "^0.6.3",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
|
"@smakss/react-scroll-direction": "^4.0.4",
|
||||||
"@stripe/react-stripe-js": "^2.3.1",
|
"@stripe/react-stripe-js": "^2.3.1",
|
||||||
"@stripe/stripe-js": "^2.1.9",
|
"@stripe/stripe-js": "^2.1.9",
|
||||||
"apng-js": "^1.1.1",
|
"apng-js": "^1.1.1",
|
||||||
@ -1022,6 +1023,17 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@smakss/react-scroll-direction": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smakss/react-scroll-direction/-/react-scroll-direction-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-FtzjZTJTLFN9A0mcJk7dXgYHFlGVPXW/EJooSVbe2dHU5hAi5rFk0ODimB7pHeHoDIUin5zE1NDtU2eY6olwlA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@stripe/react-stripe-js": {
|
"node_modules/@stripe/react-stripe-js": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.3.1.tgz",
|
||||||
@ -4195,6 +4207,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.2.tgz",
|
||||||
"integrity": "sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA=="
|
"integrity": "sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA=="
|
||||||
},
|
},
|
||||||
|
"@smakss/react-scroll-direction": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smakss/react-scroll-direction/-/react-scroll-direction-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-FtzjZTJTLFN9A0mcJk7dXgYHFlGVPXW/EJooSVbe2dHU5hAi5rFk0ODimB7pHeHoDIUin5zE1NDtU2eY6olwlA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"@stripe/react-stripe-js": {
|
"@stripe/react-stripe-js": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.3.1.tgz",
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chargebee/chargebee-js-react-wrapper": "^0.6.3",
|
"@chargebee/chargebee-js-react-wrapper": "^0.6.3",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
|
"@smakss/react-scroll-direction": "^4.0.4",
|
||||||
"@stripe/react-stripe-js": "^2.3.1",
|
"@stripe/react-stripe-js": "^2.3.1",
|
||||||
"@stripe/stripe-js": "^2.1.9",
|
"@stripe/stripe-js": "^2.1.9",
|
||||||
"apng-js": "^1.1.1",
|
"apng-js": "^1.1.1",
|
||||||
|
|||||||
BIN
public/star.png
Normal file
BIN
public/star.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
8
public/star.svg
Normal file
8
public/star.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
|
||||||
|
<path d="M 89.95 34.92 c -0.135 -0.411 -0.519 -0.688 -0.95 -0.688 H 56.508 L 45.948 2.814 C 45.811 2.408 45.43 2.133 45 2.133 s -0.811 0.274 -0.948 0.681 l -10.56 31.417 H 1 c -0.432 0 -0.815 0.277 -0.95 0.688 s 0.009 0.861 0.357 1.117 l 26.246 19.314 l -10 31.21 c -0.131 0.409 0.014 0.856 0.36 1.11 c 0.348 0.257 0.817 0.261 1.168 0.012 L 45 68.795 l 26.818 18.889 c 0.173 0.122 0.375 0.183 0.576 0.183 c 0.208 0 0.416 -0.064 0.592 -0.194 c 0.347 -0.254 0.491 -0.701 0.36 -1.11 l -10 -31.21 l 26.246 -19.314 C 89.94 35.781 90.085 35.331 89.95 34.92 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,212,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -24,6 +24,8 @@ import {
|
|||||||
SubscriptionPlans,
|
SubscriptionPlans,
|
||||||
AppleAuth,
|
AppleAuth,
|
||||||
AIRequestsV2,
|
AIRequestsV2,
|
||||||
|
Assistants,
|
||||||
|
OpenAI
|
||||||
SinglePayment,
|
SinglePayment,
|
||||||
} from './resources'
|
} from './resources'
|
||||||
|
|
||||||
@ -56,6 +58,15 @@ const api = {
|
|||||||
getZodiacs: createMethod<Zodiacs.Payload, Zodiacs.Response>(Zodiacs.createRequest),
|
getZodiacs: createMethod<Zodiacs.Payload, Zodiacs.Response>(Zodiacs.createRequest),
|
||||||
AIRequestsV2: createMethod<AIRequestsV2.Payload, AIRequestsV2.Response>(AIRequestsV2.createRequest),
|
AIRequestsV2: createMethod<AIRequestsV2.Payload, AIRequestsV2.Response>(AIRequestsV2.createRequest),
|
||||||
getAIRequestsV2: createMethod<AIRequestsV2.PayloadGet, AIRequestsV2.IAiResponseGet>(AIRequestsV2.createRequestGet),
|
getAIRequestsV2: createMethod<AIRequestsV2.PayloadGet, AIRequestsV2.IAiResponseGet>(AIRequestsV2.createRequestGet),
|
||||||
|
// Advisors chats
|
||||||
|
assistants: createMethod<Assistants.Payload, Assistants.Response>(Assistants.createRequest),
|
||||||
|
setExternalChatIdAssistant: createMethod<Assistants.PayloadSetExternalChatId, Assistants.ResponseSetExternalChatId>(Assistants.createRequestSetExternalChatId),
|
||||||
|
createThread: createMethod<OpenAI.PayloadCreateThread, OpenAI.ResponseCreateThread>(OpenAI.createRequest),
|
||||||
|
createMessage: createMethod<OpenAI.PayloadCreateMessage, OpenAI.ResponseCreateMessage>(OpenAI.createRequest),
|
||||||
|
getListMessages: createMethod<OpenAI.PayloadGetListMessages, OpenAI.ResponseGetListMessages>(OpenAI.createRequest),
|
||||||
|
runThread: createMethod<OpenAI.PayloadRunThread, OpenAI.ResponseGetStatusRunThread>(OpenAI.createRequest),
|
||||||
|
getStatusRunThread: createMethod<OpenAI.PayloadRunThread, OpenAI.ResponseGetStatusRunThread>(OpenAI.createRequest),
|
||||||
|
getListRuns: createMethod<OpenAI.PayloadGetListRuns, OpenAI.ResponseGetListRuns>(OpenAI.createRequest),
|
||||||
getSinglePaymentProducts: createMethod<SinglePayment.PayloadGet, SinglePayment.ResponseGet[]>(SinglePayment.createRequestGet),
|
getSinglePaymentProducts: createMethod<SinglePayment.PayloadGet, SinglePayment.ResponseGet[]>(SinglePayment.createRequestGet),
|
||||||
createSinglePayment: createMethod<SinglePayment.PayloadPost, SinglePayment.ResponsePost | SinglePayment.ResponsePostExistPaymentData>(SinglePayment.createRequestPost),
|
createSinglePayment: createMethod<SinglePayment.PayloadPost, SinglePayment.ResponsePost | SinglePayment.ResponsePostExistPaymentData>(SinglePayment.createRequestPost),
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/api/resources/Assistants.ts
Normal file
65
src/api/resources/Assistants.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import routes from "@/routes";
|
||||||
|
import { getAuthHeaders } from "../utils";
|
||||||
|
|
||||||
|
export interface Payload {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
ai_assistants: IAssistant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAssistant {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
external_id: string;
|
||||||
|
external_chat_id: string | null;
|
||||||
|
photo: {
|
||||||
|
th: string;
|
||||||
|
th2x: string;
|
||||||
|
lg: string;
|
||||||
|
};
|
||||||
|
photo_mime_type: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
expirience: string;
|
||||||
|
rating: string;
|
||||||
|
stars: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayloadSetExternalChatId extends Payload {
|
||||||
|
chatId: string;
|
||||||
|
ai_assistant_chat: {
|
||||||
|
external_id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseSetExternalChatId {
|
||||||
|
ai_assistant_chat: {
|
||||||
|
id: number;
|
||||||
|
assistant_id: number;
|
||||||
|
external_id: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createRequest = ({ token }: Payload): Request => {
|
||||||
|
const url = new URL(routes.server.assistants());
|
||||||
|
|
||||||
|
return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRequestSetExternalChatId = ({
|
||||||
|
token,
|
||||||
|
ai_assistant_chat,
|
||||||
|
chatId,
|
||||||
|
}: PayloadSetExternalChatId) => {
|
||||||
|
const url = new URL(routes.server.setExternalChatIdAssistants(chatId));
|
||||||
|
const body = JSON.stringify({ ai_assistant_chat });
|
||||||
|
return new Request(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getAuthHeaders(token),
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
};
|
||||||
164
src/api/resources/OpenAI.ts
Normal file
164
src/api/resources/OpenAI.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { getAuthOpenAIHeaders } from "../utils";
|
||||||
|
|
||||||
|
interface IDefaultPayload {
|
||||||
|
token: string;
|
||||||
|
method: "POST" | "GET";
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMessagePayload {
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
file_ids?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMessage {
|
||||||
|
id: string;
|
||||||
|
object: "thread.message" | string;
|
||||||
|
created_at: number;
|
||||||
|
thread_id: string;
|
||||||
|
role: "user" | string;
|
||||||
|
content: IMessageContent[];
|
||||||
|
file_ids: unknown[];
|
||||||
|
assistant_id: null | unknown;
|
||||||
|
run_id: null | unknown;
|
||||||
|
metadata: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMessageContent {
|
||||||
|
type: "text" | string;
|
||||||
|
text: {
|
||||||
|
value: string;
|
||||||
|
annotations: unknown[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create thread
|
||||||
|
export interface PayloadCreateThread extends IDefaultPayload {
|
||||||
|
messages: IMessagePayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseCreateThread {
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
created_at: number;
|
||||||
|
metadata: object;
|
||||||
|
}
|
||||||
|
// Create thread end
|
||||||
|
|
||||||
|
// Create message
|
||||||
|
export interface PayloadCreateMessage extends IDefaultPayload {
|
||||||
|
role: "user";
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResponseCreateMessage = IMessage;
|
||||||
|
// Create message end
|
||||||
|
|
||||||
|
// Get list messages
|
||||||
|
export interface PayloadGetListMessages extends IDefaultPayload {
|
||||||
|
QueryParams?: {
|
||||||
|
limit?: number;
|
||||||
|
order?: "asc" | "desc";
|
||||||
|
after?: string;
|
||||||
|
before?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseGetListMessages {
|
||||||
|
object: "list" | string;
|
||||||
|
data: IMessage[];
|
||||||
|
first_id: string;
|
||||||
|
last_id: string;
|
||||||
|
has_more: boolean;
|
||||||
|
}
|
||||||
|
// Get list messages end
|
||||||
|
|
||||||
|
// Run thread
|
||||||
|
interface ITool {
|
||||||
|
type: "code_interpreter" | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUsage {
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
total_tokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayloadRunThread extends IDefaultPayload {
|
||||||
|
assistant_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseRunThread {
|
||||||
|
id: string;
|
||||||
|
object: "thread.run" | string;
|
||||||
|
created_at: number;
|
||||||
|
assistant_id: string;
|
||||||
|
thread_id: string;
|
||||||
|
status: "queued" | string;
|
||||||
|
started_at: number;
|
||||||
|
expires_at: null | unknown;
|
||||||
|
cancelled_at: null | unknown;
|
||||||
|
failed_at: null | unknown;
|
||||||
|
completed_at: number;
|
||||||
|
last_error: null | unknown;
|
||||||
|
model: "gpt-4" | string;
|
||||||
|
instructions: null | unknown;
|
||||||
|
tools: ITool[];
|
||||||
|
file_ids: string[];
|
||||||
|
metadata: object;
|
||||||
|
usage: null | IUsage;
|
||||||
|
}
|
||||||
|
// Run thread end
|
||||||
|
|
||||||
|
// Get status run thread
|
||||||
|
export type PayloadGetStatusRunThread = IDefaultPayload;
|
||||||
|
|
||||||
|
export type ResponseGetStatusRunThread = ResponseRunThread;
|
||||||
|
// Get status run thread end
|
||||||
|
|
||||||
|
// Get list runs
|
||||||
|
export type PayloadGetListRuns = IDefaultPayload;
|
||||||
|
|
||||||
|
export interface ResponseGetListRuns {
|
||||||
|
object: "list" | string;
|
||||||
|
data: ResponseRunThread[];
|
||||||
|
first_id: string;
|
||||||
|
last_id: string;
|
||||||
|
has_more: boolean;
|
||||||
|
}
|
||||||
|
// Get list runs end
|
||||||
|
|
||||||
|
export type Payload =
|
||||||
|
| PayloadCreateThread
|
||||||
|
| PayloadCreateMessage
|
||||||
|
| PayloadGetListMessages
|
||||||
|
| PayloadRunThread
|
||||||
|
| PayloadGetStatusRunThread;
|
||||||
|
|
||||||
|
export function createRequest({
|
||||||
|
token,
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
...data
|
||||||
|
}: Payload): Request {
|
||||||
|
const url = new URL(path);
|
||||||
|
const body = JSON.stringify({ ...data });
|
||||||
|
if ("QueryParams" in data && data.QueryParams) {
|
||||||
|
for (const [key, value] of Object.entries(data.QueryParams)) {
|
||||||
|
url.searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (method === "GET") {
|
||||||
|
return new Request(url, {
|
||||||
|
method: method,
|
||||||
|
headers: getAuthOpenAIHeaders(token),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Request(url, {
|
||||||
|
method: method,
|
||||||
|
headers: getAuthOpenAIHeaders(token),
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -22,4 +22,6 @@ export * as GoogleAuth from "./GoogleAuth";
|
|||||||
export * as SubscriptionPlans from "./SubscriptionPlans";
|
export * as SubscriptionPlans from "./SubscriptionPlans";
|
||||||
export * as AppleAuth from "./AppleAuth";
|
export * as AppleAuth from "./AppleAuth";
|
||||||
export * as AIRequestsV2 from "./AIRequestsV2";
|
export * as AIRequestsV2 from "./AIRequestsV2";
|
||||||
|
export * as Assistants from "./Assistants";
|
||||||
|
export * as OpenAI from "./OpenAI";
|
||||||
export * as SinglePayment from "./SinglePayment";
|
export * as SinglePayment from "./SinglePayment";
|
||||||
|
|||||||
@ -1,34 +1,48 @@
|
|||||||
import { AuthToken } from './types'
|
import { AuthToken } from "./types";
|
||||||
import { ErrorResponse, isErrorResponse, ApiError, buildUnknownError } from './errors'
|
import {
|
||||||
|
ErrorResponse,
|
||||||
|
isErrorResponse,
|
||||||
|
ApiError,
|
||||||
|
buildUnknownError,
|
||||||
|
} from "./errors";
|
||||||
|
|
||||||
export function createMethod<P, R>(createRequest: (payload: P) => Request) {
|
export function createMethod<P, R>(createRequest: (payload: P) => Request) {
|
||||||
return async (payload: P): Promise<R> => {
|
return async (payload: P): Promise<R> => {
|
||||||
const request = createRequest(payload)
|
const request = createRequest(payload);
|
||||||
const response = await fetch(request)
|
const response = await fetch(request);
|
||||||
const data: R | ErrorResponse = await response.json()
|
const data: R | ErrorResponse = await response.json();
|
||||||
const statusCode = response.status
|
const statusCode = response.status;
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const body = isErrorResponse<R>(data) ? data : { error: buildUnknownError(statusCode) }
|
const body = isErrorResponse<R>(data)
|
||||||
throw new ApiError({ body, statusCode })
|
? data
|
||||||
|
: { error: buildUnknownError(statusCode) };
|
||||||
|
throw new ApiError({ body, statusCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isErrorResponse<R>(data)) {
|
if (isErrorResponse<R>(data)) {
|
||||||
throw new ApiError({ body: data, statusCode })
|
throw new ApiError({ body: data, statusCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBaseHeaders(): Headers {
|
export function getBaseHeaders(): Headers {
|
||||||
return new Headers({
|
return new Headers({
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAuthHeaders(token: AuthToken): Headers {
|
export function getAuthHeaders(token: AuthToken): Headers {
|
||||||
const headers = getBaseHeaders()
|
const headers = getBaseHeaders();
|
||||||
headers.append('Authorization', `Bearer ${token}`)
|
headers.append("Authorization", `Bearer ${token}`);
|
||||||
return headers
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthOpenAIHeaders(token: AuthToken): Headers {
|
||||||
|
const headers = getBaseHeaders();
|
||||||
|
headers.append("Authorization", `Bearer ${token}`);
|
||||||
|
headers.append("OpenAI-Beta", "assistants=v1");
|
||||||
|
return headers;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,6 +104,8 @@ import AddReportPage from "../pages/AdditionalPurchases/pages/AddReport";
|
|||||||
import UnlimitedReadingsPage from "../pages/AdditionalPurchases/pages/UnlimitedReadings";
|
import UnlimitedReadingsPage from "../pages/AdditionalPurchases/pages/UnlimitedReadings";
|
||||||
import AddConsultationPage from "../pages/AdditionalPurchases/pages/AddConsultation";
|
import AddConsultationPage from "../pages/AdditionalPurchases/pages/AddConsultation";
|
||||||
import StepsManager from "@/components/palmistry/steps-manager/steps-manager";
|
import StepsManager from "@/components/palmistry/steps-manager/steps-manager";
|
||||||
|
import Advisors from "../pages/Advisors";
|
||||||
|
import AdvisorChatPage from "../pages/AdvisorChat";
|
||||||
import PaymentWithEmailPage from "../pages/PaymentWithEmailPage";
|
import PaymentWithEmailPage from "../pages/PaymentWithEmailPage";
|
||||||
import SuccessPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage";
|
import SuccessPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage";
|
||||||
import FailPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage";
|
import FailPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage";
|
||||||
@ -125,6 +127,15 @@ function App(): JSX.Element {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const jwtToken = searchParams.get("token");
|
const jwtToken = searchParams.get("token");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// api.getAppConfig({ bundleId: "auraweb" }),
|
||||||
|
dispatch(
|
||||||
|
actions.siteConfig.update({
|
||||||
|
openAiToken: "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",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isProduction) return;
|
if (!isProduction) return;
|
||||||
ReactGA.send({
|
ReactGA.send({
|
||||||
@ -453,6 +464,10 @@ function App(): JSX.Element {
|
|||||||
path={routes.client.wallpaper()}
|
path={routes.client.wallpaper()}
|
||||||
element={<WallpaperPage />}
|
element={<WallpaperPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route path={routes.client.advisors()} element={<Advisors />} />
|
||||||
|
<Route path={routes.client.advisors()}>
|
||||||
|
<Route path=":id" element={<AdvisorChatPage />} />
|
||||||
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
path={routes.client.magicBall()}
|
path={routes.client.magicBall()}
|
||||||
element={<MagicBallPage />}
|
element={<MagicBallPage />}
|
||||||
@ -578,6 +593,13 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
|
|||||||
image: "Compatibility.svg",
|
image: "Compatibility.svg",
|
||||||
onClick: handleCompatibility,
|
onClick: handleCompatibility,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Advisors",
|
||||||
|
path: routes.client.advisors(),
|
||||||
|
paths: [routes.client.advisors()],
|
||||||
|
image: "moon.svg",
|
||||||
|
onClick: () => null,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "My Moon",
|
title: "My Moon",
|
||||||
path: routes.client.wallpaper(),
|
path: routes.client.wallpaper(),
|
||||||
|
|||||||
@ -29,6 +29,15 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const homeConfig = useSelector(selectors.selectHome);
|
const homeConfig = useSelector(selectors.selectHome);
|
||||||
const showNavbarFooter = homeConfig.isShowNavbar;
|
const showNavbarFooter = homeConfig.isShowNavbar;
|
||||||
|
const queryTimeOutRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (queryTimeOutRef.current) {
|
||||||
|
clearTimeout(queryTimeOutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpenModal) return;
|
if (isOpenModal) return;
|
||||||
@ -67,8 +76,7 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element {
|
|||||||
setIsOpenModal(false);
|
setIsOpenModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const token = useSelector(selectors.selectToken);
|
||||||
const token = useSelector(selectors.selectToken)
|
|
||||||
const createCallback = useCallback(async () => {
|
const createCallback = useCallback(async () => {
|
||||||
const data: UserCallbacks.PayloadPost = {
|
const data: UserCallbacks.PayloadPost = {
|
||||||
data: {
|
data: {
|
||||||
@ -87,7 +95,7 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element {
|
|||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
if (!getCallback.user_callback.is_complete) {
|
if (!getCallback.user_callback.is_complete) {
|
||||||
setTimeout(getUserCallbacksRequest, 3000);
|
queryTimeOutRef.current = setTimeout(getUserCallbacksRequest, 3000);
|
||||||
}
|
}
|
||||||
dispatch(
|
dispatch(
|
||||||
actions.userCallbacks.update({
|
actions.userCallbacks.update({
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Title from "../Title";
|
|||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { actions, selectors } from "@/store";
|
import { actions, selectors } from "@/store";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { AICompats, AIRequests, useApi, useApiCall } from "@/api";
|
import { AICompats, AIRequests, useApi, useApiCall } from "@/api";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import routes from "@/routes";
|
import routes from "@/routes";
|
||||||
@ -26,6 +26,15 @@ function CompatResultPage(): JSX.Element {
|
|||||||
const [isOpenModal, setIsOpenModal] = useState(true);
|
const [isOpenModal, setIsOpenModal] = useState(true);
|
||||||
const [isVisualLoading, setIsVisualLoading] = useState(true);
|
const [isVisualLoading, setIsVisualLoading] = useState(true);
|
||||||
const [isDataLoading, setIsDataLoading] = useState(true);
|
const [isDataLoading, setIsDataLoading] = useState(true);
|
||||||
|
const queryTimeOutRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (queryTimeOutRef.current) {
|
||||||
|
clearTimeout(queryTimeOutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (homeConfig.pathFromHome === EPathsFromHome.breath) {
|
if (homeConfig.pathFromHome === EPathsFromHome.breath) {
|
||||||
@ -67,7 +76,7 @@ function CompatResultPage(): JSX.Element {
|
|||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
if (aIRequest.ai_request.state !== "ready") {
|
if (aIRequest.ai_request.state !== "ready") {
|
||||||
setTimeout(loadAIRequest, 3000);
|
queryTimeOutRef.current = setTimeout(loadAIRequest, 3000);
|
||||||
}
|
}
|
||||||
setText(aIRequest?.ai_request?.response?.body || "Loading...");
|
setText(aIRequest?.ai_request?.response?.body || "Loading...");
|
||||||
setIsDataLoading(false);
|
setIsDataLoading(false);
|
||||||
|
|||||||
@ -1,21 +1,30 @@
|
|||||||
import './styles.css'
|
import "./styles.css";
|
||||||
|
|
||||||
export enum LoaderColor {
|
export enum LoaderColor {
|
||||||
White = 'white',
|
White = "white",
|
||||||
Black = 'black',
|
Black = "black",
|
||||||
|
Red = "red",
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoaderProps = {
|
type LoaderProps = {
|
||||||
color?: LoaderColor
|
color?: LoaderColor;
|
||||||
}
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function Loader({ color = LoaderColor.Black }: LoaderProps): JSX.Element {
|
const colorClasses = {
|
||||||
const colorClass = color === LoaderColor.White ? 'loader__white' : 'loader__black'
|
[LoaderColor.White]: "loader__white",
|
||||||
|
[LoaderColor.Black]: "loader__black",
|
||||||
|
[LoaderColor.Red]: "loader__red",
|
||||||
|
};
|
||||||
|
|
||||||
|
function Loader({ color = LoaderColor.Black, className }: LoaderProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className='loader-container'>
|
<div className={`loader-container ${className}`}>
|
||||||
<div className={`loader ${colorClass}`}><span></span></div>
|
<div className={`loader ${colorClasses[color]}`}>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Loader
|
export default Loader;
|
||||||
|
|||||||
@ -26,6 +26,10 @@
|
|||||||
border-color: #fff;
|
border-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loader.loader__red span::after {
|
||||||
|
border-color: #ff2c57;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes loader-1-1 {
|
@keyframes loader-1-1 {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
|
|||||||
@ -29,7 +29,7 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
|
|||||||
|
|
||||||
const [isShowOnboardingNavbarFooter, setIsShowOnboardingNavbarFooter] =
|
const [isShowOnboardingNavbarFooter, setIsShowOnboardingNavbarFooter] =
|
||||||
useState(!onboardingConfigNavbarFooter?.isShown);
|
useState(!onboardingConfigNavbarFooter?.isShown);
|
||||||
const [selectedOnboarding, setSelectedOnboarding] = useState(3);
|
const [selectedOnboarding, setSelectedOnboarding] = useState(4);
|
||||||
const [rerender, setRerender] = useState(false);
|
const [rerender, setRerender] = useState(false);
|
||||||
rerender
|
rerender
|
||||||
|
|
||||||
@ -66,6 +66,12 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
|
|||||||
onClick: () => setShownOnboarding(),
|
onClick: () => setShownOnboarding(),
|
||||||
classNameText: `${styles["compatibility-onboarding__text"]} ${styles.onboarding}`,
|
classNameText: `${styles["compatibility-onboarding__text"]} ${styles.onboarding}`,
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// text: t("au.web_onbording.moon"),
|
||||||
|
// onClick: () => setSelectedOnboarding(0),
|
||||||
|
// classNameText: `${styles["moon-onboarding__text"]} ${styles.onboarding}`,
|
||||||
|
// },
|
||||||
|
null,
|
||||||
{
|
{
|
||||||
text: t("au.web_onbording.moon"),
|
text: t("au.web_onbording.moon"),
|
||||||
onClick: () => setSelectedOnboarding(0),
|
onClick: () => setSelectedOnboarding(0),
|
||||||
@ -74,7 +80,7 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleClick = (item: INavbarHomeItems) => {
|
const handleClick = (item: INavbarHomeItems) => {
|
||||||
onboardingsSettings[selectedOnboarding].onClick();
|
onboardingsSettings[selectedOnboarding]?.onClick();
|
||||||
if (item.onClick) {
|
if (item.onClick) {
|
||||||
item.onClick();
|
item.onClick();
|
||||||
}
|
}
|
||||||
@ -96,9 +102,9 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
|
|||||||
isShow={index === selectedOnboarding}
|
isShow={index === selectedOnboarding}
|
||||||
>
|
>
|
||||||
<TextWithFinger
|
<TextWithFinger
|
||||||
text={onboardingsSettings[index].text}
|
text={onboardingsSettings[index]?.text || ""}
|
||||||
crossClickHandler={() => onboardingsSettings[index].onClick()}
|
crossClickHandler={() => onboardingsSettings[index]?.onClick()}
|
||||||
classNameText={onboardingsSettings[index].classNameText}
|
classNameText={onboardingsSettings[index]?.classNameText}
|
||||||
/>
|
/>
|
||||||
</Onboarding>
|
</Onboarding>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
.result-container__value-label {
|
.result-container__value-label {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-container__value-value {
|
.result-container__value-value {
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface IChatHeaderProps {
|
||||||
|
name: string;
|
||||||
|
avatar: string;
|
||||||
|
classNameContainer?: string;
|
||||||
|
clickBackButton: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatHeader({
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
classNameContainer = "",
|
||||||
|
clickBackButton,
|
||||||
|
}: IChatHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.container} ${classNameContainer}`}>
|
||||||
|
<div className={styles["back-button"]} onClick={clickBackButton}>
|
||||||
|
<div className={styles["arrow"]} /> Advisors
|
||||||
|
</div>
|
||||||
|
<div className={styles.name}>
|
||||||
|
{name}
|
||||||
|
<span className={styles["online-status"]} />
|
||||||
|
</div>
|
||||||
|
<img className={styles.avatar} src={avatar} alt={name} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatHeader;
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: rgb(255 255 255 / 10%);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 2px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-status {
|
||||||
|
display: block;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
background-color: rgb(6, 183, 6);
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(0, 128, 255);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button > .arrow {
|
||||||
|
display: flex;
|
||||||
|
width: 14px;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button > .arrow::before,
|
||||||
|
.back-button > .arrow::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 12px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: rgb(0, 128, 255);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button > .arrow::before {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
top: calc(50% - 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button > .arrow::after {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
top: calc(50% + 4px);
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { ChangeEvent, FormEvent } from "react";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
import Loader, { LoaderColor } from "@/components/Loader";
|
||||||
|
|
||||||
|
interface IInputMessageProps {
|
||||||
|
placeholder?: string;
|
||||||
|
classNameContainer?: string;
|
||||||
|
messageText: string;
|
||||||
|
textareaRows: number;
|
||||||
|
disabledButton: boolean;
|
||||||
|
disabledTextArea: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
handleChangeMessageText: (e: ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
|
submitForm: (messageText: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputMessage({
|
||||||
|
placeholder,
|
||||||
|
messageText,
|
||||||
|
textareaRows,
|
||||||
|
disabledButton,
|
||||||
|
disabledTextArea,
|
||||||
|
isLoading,
|
||||||
|
handleChangeMessageText,
|
||||||
|
submitForm,
|
||||||
|
classNameContainer = "",
|
||||||
|
}: IInputMessageProps) {
|
||||||
|
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submitForm(messageText);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className={`${styles.container} ${classNameContainer}`}
|
||||||
|
onSubmit={(e) => handleSubmit(e)}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
className={styles.input}
|
||||||
|
disabled={disabledTextArea}
|
||||||
|
value={messageText}
|
||||||
|
onChange={(e) => handleChangeMessageText(e)}
|
||||||
|
rows={textareaRows}
|
||||||
|
placeholder={placeholder || "Type your message"}
|
||||||
|
/>
|
||||||
|
{!isLoading && (
|
||||||
|
<button className={styles.button} disabled={disabledButton}>
|
||||||
|
<div className={styles["arrow"]} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isLoading && <Loader color={LoaderColor.White} />}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InputMessage;
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 36px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
min-width: 0;
|
||||||
|
/* aspect-ratio: 1 / 1; */
|
||||||
|
background-color: rgb(0, 110, 255);
|
||||||
|
border: none;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button > .arrow {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button > .arrow::before,
|
||||||
|
.button > .arrow::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 6px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button > .arrow::before {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
left: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button > .arrow::after {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border: solid 1px #929292;
|
||||||
|
outline: none;
|
||||||
|
background-color: #232322;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 4px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
height: inherit;
|
||||||
|
min-height: 34px;
|
||||||
|
/* max-height: 176px; */
|
||||||
|
/* width: calc(100% - 34px); */
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import styles from "./styles.module.css"
|
||||||
|
|
||||||
|
function LoaderDots() {
|
||||||
|
return (
|
||||||
|
<div className={styles.container} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoaderDots
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
left: -9999px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #7f7f7f;
|
||||||
|
color: #7f7f7f;
|
||||||
|
box-shadow: 9999px 0 0 -5px;
|
||||||
|
animation: container 1.5s infinite linear;
|
||||||
|
animation-delay: 0.25s;
|
||||||
|
margin: 2px 24px;
|
||||||
|
}
|
||||||
|
.container::before,
|
||||||
|
.container::after {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #7f7f7f;
|
||||||
|
color: #7f7f7f;
|
||||||
|
}
|
||||||
|
.container::before {
|
||||||
|
box-shadow: 9984px 0 0 -5px;
|
||||||
|
animation: container-before 1.5s infinite linear;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
.container::after {
|
||||||
|
box-shadow: 10014px 0 0 -5px;
|
||||||
|
animation: container-after 1.5s infinite linear;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes container-before {
|
||||||
|
0% {
|
||||||
|
box-shadow: 9984px 0 0 -5px;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
box-shadow: 9984px 0 0 2px;
|
||||||
|
}
|
||||||
|
60%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 9984px 0 0 -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes container {
|
||||||
|
0% {
|
||||||
|
box-shadow: 9999px 0 0 -5px;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
box-shadow: 9999px 0 0 2px;
|
||||||
|
}
|
||||||
|
60%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 9999px 0 0 -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes container-after {
|
||||||
|
0% {
|
||||||
|
box-shadow: 10014px 0 0 -5px;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
box-shadow: 10014px 0 0 2px;
|
||||||
|
}
|
||||||
|
60%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 10014px 0 0 -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface IMessageProps {
|
||||||
|
text: string | React.ReactNode;
|
||||||
|
isSelf: boolean;
|
||||||
|
advisorName?: string;
|
||||||
|
avatar?: string;
|
||||||
|
backgroundTextColor?: string;
|
||||||
|
textColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
function Message({
|
||||||
|
text,
|
||||||
|
avatar,
|
||||||
|
isSelf,
|
||||||
|
advisorName = "Advisor",
|
||||||
|
backgroundTextColor = "#0080ff",
|
||||||
|
textColor = "#fff",
|
||||||
|
}: IMessageProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
style={{ flexDirection: isSelf ? "row-reverse" : "row" }}
|
||||||
|
>
|
||||||
|
{!isSelf && avatar?.length && (
|
||||||
|
<img
|
||||||
|
className={styles.avatar}
|
||||||
|
src={avatar}
|
||||||
|
alt={`${advisorName} avatar`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={styles.text}
|
||||||
|
style={{ color: textColor, backgroundColor: backgroundTextColor }}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export default React.memo(Message);
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 70%;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
362
src/components/pages/AdvisorChat/index.tsx
Normal file
362
src/components/pages/AdvisorChat/index.tsx
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { Assistants, useApi, useApiCall } from "@/api";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { selectors } from "@/store";
|
||||||
|
import { IAssistant } from "@/api/resources/Assistants";
|
||||||
|
import Loader, { LoaderColor } from "@/components/Loader";
|
||||||
|
import ChatHeader from "./components/ChatHeader";
|
||||||
|
import InputMessage from "./components/InputMessage";
|
||||||
|
import routes from "@/routes";
|
||||||
|
import { IMessage, ResponseRunThread } from "@/api/resources/OpenAI";
|
||||||
|
import Message from "./components/Message";
|
||||||
|
import LoaderDots from "./components/LoaderDots";
|
||||||
|
import useDetectScroll from "@smakss/react-scroll-direction";
|
||||||
|
|
||||||
|
function AdvisorChatPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const api = useApi();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { scrollDir, scrollPosition } = useDetectScroll();
|
||||||
|
const token = useSelector(selectors.selectToken);
|
||||||
|
const openAiToken = useSelector(selectors.selectOpenAiToken);
|
||||||
|
const [assistant, setAssistant] = useState<IAssistant>();
|
||||||
|
const [messageText, setMessageText] = useState("");
|
||||||
|
const [textareaRows, setTextareaRows] = useState(1);
|
||||||
|
const [messages, setMessages] = useState<IMessage[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isLoadingSelfMessage, setIsLoadingSelfMessage] = useState(false);
|
||||||
|
const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false);
|
||||||
|
const [isLoadingLatestMessages, setIsLoadingLatestMessages] = useState(false);
|
||||||
|
const timeOutStatusRunRef = useRef<NodeJS.Timeout>();
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [hasMoreLatestMessages, setHasMoreLatestMessages] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
scrollDir === "up" &&
|
||||||
|
scrollPosition.top < 50 &&
|
||||||
|
!isLoadingLatestMessages
|
||||||
|
) {
|
||||||
|
loadLatestMessages();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [scrollDir, scrollPosition]);
|
||||||
|
|
||||||
|
const loadLatestMessages = async () => {
|
||||||
|
const lastIdMessage = messages[messages.length - 1]?.id;
|
||||||
|
if (!hasMoreLatestMessages || !lastIdMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoadingLatestMessages(true);
|
||||||
|
const listMessages = await api.getListMessages({
|
||||||
|
token: openAiToken,
|
||||||
|
method: "GET",
|
||||||
|
path: routes.openAi.getListMessages(assistant?.external_chat_id || ""),
|
||||||
|
QueryParams: {
|
||||||
|
after: lastIdMessage,
|
||||||
|
limit: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setHasMoreLatestMessages(listMessages.has_more);
|
||||||
|
setMessages((prev) => [...prev, ...listMessages.data]);
|
||||||
|
setIsLoadingLatestMessages(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeOutStatusRunRef.current) {
|
||||||
|
clearTimeout(timeOutStatusRunRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getCurrentAssistant = (
|
||||||
|
aiAssistants: Assistants.IAssistant[],
|
||||||
|
idAssistant: string | number
|
||||||
|
) => {
|
||||||
|
const currentAssistant = aiAssistants.find(
|
||||||
|
(a) => a.id === Number(idAssistant)
|
||||||
|
);
|
||||||
|
return currentAssistant;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMessages = async (threadId: string) => {
|
||||||
|
const listMessages = await api.getListMessages({
|
||||||
|
token: openAiToken,
|
||||||
|
method: "GET",
|
||||||
|
path: routes.openAi.getListMessages(threadId),
|
||||||
|
});
|
||||||
|
setMessages(listMessages.data);
|
||||||
|
scrollToBottom();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setExternalChatIdAssistant = async (threadId: string) => {
|
||||||
|
await api.setExternalChatIdAssistant({
|
||||||
|
token,
|
||||||
|
chatId: String(id),
|
||||||
|
ai_assistant_chat: {
|
||||||
|
external_id: threadId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCurrentAssistant = async () => {
|
||||||
|
const { ai_assistants } = await api.assistants({
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
const currentAssistant = getCurrentAssistant(ai_assistants, id || "");
|
||||||
|
setAssistant(currentAssistant);
|
||||||
|
return {
|
||||||
|
ai_assistants,
|
||||||
|
currentAssistant,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
const { ai_assistants, currentAssistant } = await updateCurrentAssistant();
|
||||||
|
let listRuns: ResponseRunThread[] = [];
|
||||||
|
if (currentAssistant?.external_chat_id?.length) {
|
||||||
|
await updateMessages(currentAssistant.external_chat_id);
|
||||||
|
const result = await api.getListRuns({
|
||||||
|
token: openAiToken,
|
||||||
|
method: "GET",
|
||||||
|
path: routes.openAi.getListRuns(currentAssistant.external_chat_id),
|
||||||
|
});
|
||||||
|
listRuns = result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
const runInProgress = listRuns.find((r) => r.status === "in_progress");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (runInProgress) {
|
||||||
|
setIsLoadingAdvisorMessage(true);
|
||||||
|
|
||||||
|
await checkStatusAndGetLastMessage(
|
||||||
|
runInProgress.thread_id,
|
||||||
|
runInProgress.id
|
||||||
|
);
|
||||||
|
setIsLoadingAdvisorMessage(false);
|
||||||
|
}
|
||||||
|
return { ai_assistants };
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [api, id, openAiToken, token]);
|
||||||
|
|
||||||
|
useApiCall<Assistants.Response>(loadData);
|
||||||
|
|
||||||
|
const createThread = async (messageText: string) => {
|
||||||
|
const thread = await api.createThread({
|
||||||
|
token: openAiToken,
|
||||||
|
method: "POST",
|
||||||
|
path: routes.openAi.createThread(),
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: messageText,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await setExternalChatIdAssistant(thread.id);
|
||||||
|
await updateCurrentAssistant();
|
||||||
|
return thread;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusThreadRun = async (
|
||||||
|
threadId: string,
|
||||||
|
runId: string
|
||||||
|
): Promise<ResponseRunThread> => {
|
||||||
|
await new Promise(
|
||||||
|
(resolve) => (timeOutStatusRunRef.current = setTimeout(resolve, 1500))
|
||||||
|
);
|
||||||
|
const run = await api.getStatusRunThread({
|
||||||
|
token: openAiToken,
|
||||||
|
method: "GET",
|
||||||
|
path: routes.openAi.getStatusRunThread(threadId, runId),
|
||||||
|
assistant_id: `${assistant?.external_id}`,
|
||||||
|
});
|
||||||
|
if (run.status !== "completed") {
|
||||||
|
return await getStatusThreadRun(threadId, runId);
|
||||||
|
}
|
||||||
|
return run;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMessage = async (messageText: string, threadId: string) => {
|
||||||
|
const message = await api.createMessage({
|
||||||
|
token: openAiToken,
|
||||||
|
method: "POST",
|
||||||
|
path: routes.openAi.createMessage(threadId),
|
||||||
|
role: "user",
|
||||||
|
content: messageText,
|
||||||
|
});
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runThread = async (threadId: string, assistantId: string) => {
|
||||||
|
const run = await api.runThread({
|
||||||
|
token: openAiToken,
|
||||||
|
method: "POST",
|
||||||
|
path: routes.openAi.runThread(threadId),
|
||||||
|
assistant_id: assistantId,
|
||||||
|
});
|
||||||
|
return run;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkStatusAndGetLastMessage = async (
|
||||||
|
threadId: string,
|
||||||
|
runId: string
|
||||||
|
) => {
|
||||||
|
const { status } = await getStatusThreadRun(threadId, runId);
|
||||||
|
|
||||||
|
if (status === "completed") {
|
||||||
|
await getLastMessage(threadId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLastMessage = async (threadId: string) => {
|
||||||
|
const lastMessage = await api.getListMessages({
|
||||||
|
token: openAiToken,
|
||||||
|
method: "GET",
|
||||||
|
path: routes.openAi.getListMessages(threadId),
|
||||||
|
QueryParams: {
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setMessages((prev) => [lastMessage.data[0], ...prev]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = async (messageText: string) => {
|
||||||
|
setMessageText("");
|
||||||
|
setIsLoadingSelfMessage(true);
|
||||||
|
let threadId = "";
|
||||||
|
let assistantId = "";
|
||||||
|
|
||||||
|
if (assistant?.external_chat_id?.length) {
|
||||||
|
const message = await createMessage(
|
||||||
|
messageText,
|
||||||
|
assistant.external_chat_id
|
||||||
|
);
|
||||||
|
setMessages((prev) => [message, ...prev]);
|
||||||
|
setIsLoadingSelfMessage(false);
|
||||||
|
setIsLoadingAdvisorMessage(true);
|
||||||
|
threadId = assistant.external_chat_id;
|
||||||
|
assistantId = assistant.external_id || "";
|
||||||
|
} else {
|
||||||
|
const thread = await createThread(messageText);
|
||||||
|
threadId = thread.id;
|
||||||
|
assistantId = assistant?.external_id || "";
|
||||||
|
|
||||||
|
await getLastMessage(threadId);
|
||||||
|
setIsLoadingSelfMessage(false);
|
||||||
|
setIsLoadingAdvisorMessage(true);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = await runThread(threadId, assistantId);
|
||||||
|
|
||||||
|
await checkStatusAndGetLastMessage(threadId, run.id);
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
setIsLoadingAdvisorMessage(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeMessageText = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const { scrollHeight, clientHeight, value } = e.target;
|
||||||
|
if (
|
||||||
|
scrollHeight > clientHeight &&
|
||||||
|
textareaRows < 5 &&
|
||||||
|
value.length > messageText.length
|
||||||
|
) {
|
||||||
|
setTextareaRows((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
scrollHeight === clientHeight &&
|
||||||
|
textareaRows > 1 &&
|
||||||
|
value.length < messageText.length
|
||||||
|
) {
|
||||||
|
setTextareaRows((prev) => prev - 1);
|
||||||
|
}
|
||||||
|
setMessageText(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIsSelfMessage = (role: string) => role === "user";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`${styles.page} page`}>
|
||||||
|
{isLoading && (
|
||||||
|
<Loader color={LoaderColor.Red} className={styles.loader} />
|
||||||
|
)}
|
||||||
|
{!isLoading && (
|
||||||
|
<ChatHeader
|
||||||
|
name={assistant?.name || ""}
|
||||||
|
avatar={assistant?.photo?.th2x || ""}
|
||||||
|
classNameContainer={styles["header-container"]}
|
||||||
|
clickBackButton={() => navigate(-1)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!!messages.length && (
|
||||||
|
<div className={styles["messages-container"]}>
|
||||||
|
{isLoadingAdvisorMessage && (
|
||||||
|
<Message
|
||||||
|
avatar={assistant?.photo?.th2x || ""}
|
||||||
|
text={<LoaderDots />}
|
||||||
|
isSelf={false}
|
||||||
|
backgroundTextColor={"#c9c9c9"}
|
||||||
|
textColor={"#000"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{messages.map((message) =>
|
||||||
|
message.content.map((content) => (
|
||||||
|
<Message
|
||||||
|
avatar={assistant?.photo?.th2x || ""}
|
||||||
|
text={content.text.value}
|
||||||
|
advisorName={assistant?.name || ""}
|
||||||
|
backgroundTextColor={
|
||||||
|
getIsSelfMessage(message.role) ? "#0080ff" : "#c9c9c9"
|
||||||
|
}
|
||||||
|
textColor={getIsSelfMessage(message.role) ? "#fff" : "#000"}
|
||||||
|
isSelf={getIsSelfMessage(message.role)}
|
||||||
|
key={message.id}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div className={styles["loader-container"]}>
|
||||||
|
{isLoadingLatestMessages && <Loader color={LoaderColor.Red} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
{!isLoading && (
|
||||||
|
<InputMessage
|
||||||
|
placeholder="Text message"
|
||||||
|
messageText={messageText}
|
||||||
|
textareaRows={textareaRows}
|
||||||
|
disabledTextArea={
|
||||||
|
isLoadingAdvisorMessage || isLoadingSelfMessage || isLoading
|
||||||
|
}
|
||||||
|
disabledButton={!messageText.length}
|
||||||
|
classNameContainer={styles["input-container"]}
|
||||||
|
handleChangeMessageText={handleChangeMessageText}
|
||||||
|
isLoading={isLoadingSelfMessage}
|
||||||
|
submitForm={sendMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdvisorChatPage;
|
||||||
52
src/components/pages/AdvisorChat/styles.module.css
Normal file
52
src/components/pages/AdvisorChat/styles.module.css
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
height: fit-content;
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
background-color: #232322;
|
||||||
|
color: #fff;
|
||||||
|
padding: 64px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
max-width: 560px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
max-width: 560px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { IAssistant } from "@/api/resources/Assistants";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
import MainButton from "@/components/MainButton";
|
||||||
|
import Title from "@/components/Title";
|
||||||
|
|
||||||
|
interface IAssistantCardProps extends IAssistant {
|
||||||
|
onClickCard: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AssistantCard({
|
||||||
|
name,
|
||||||
|
expirience,
|
||||||
|
stars,
|
||||||
|
photo: { th2x },
|
||||||
|
rating,
|
||||||
|
onClickCard,
|
||||||
|
}: IAssistantCardProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container} onClick={onClickCard}>
|
||||||
|
<div
|
||||||
|
className={styles.header}
|
||||||
|
style={{ backgroundImage: `url(${th2x})` }}
|
||||||
|
>
|
||||||
|
<Title variant="h3" className={styles.name}>
|
||||||
|
{name}
|
||||||
|
<span className={styles["online-status"]} />
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<span className={styles.expirience}>{expirience}</span>
|
||||||
|
<div className={styles["rating-container"]}>
|
||||||
|
<div className={styles.stars}>
|
||||||
|
{Array(stars)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => (
|
||||||
|
<img key={index} src="/star.svg" alt="star" />
|
||||||
|
))}
|
||||||
|
<span className={styles.rating}> | {rating}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MainButton className={styles.button}>CHAT | FREE</MainButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AssistantCard;
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: fit-content;
|
||||||
|
border: solid #b3b3b3 1px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: #000;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 167px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
box-shadow: inset 0 -24px 22px #000;
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-status {
|
||||||
|
display: block;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
background-color: rgb(6, 183, 6);
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-container {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars > img {
|
||||||
|
width: 12px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
color: gray;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
border-radius: 32px;
|
||||||
|
background-color: rgb(37, 107, 239);
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expirience {
|
||||||
|
white-space: nowrap;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
60
src/components/pages/Advisors/index.tsx
Normal file
60
src/components/pages/Advisors/index.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import Title from "@/components/Title";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Assistants, useApi, useApiCall } from "@/api";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { selectors } from "@/store";
|
||||||
|
import { IAssistant } from "@/api/resources/Assistants";
|
||||||
|
import AssistantCard from "./components/AssistantCard";
|
||||||
|
import Loader, { LoaderColor } from "@/components/Loader";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import routes from "@/routes";
|
||||||
|
|
||||||
|
function Advisors() {
|
||||||
|
const api = useApi();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const token = useSelector(selectors.selectToken);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [assistants, setAssistants] = useState<IAssistant[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
const { ai_assistants } = await api.assistants({
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
setAssistants(ai_assistants);
|
||||||
|
setIsLoading(false);
|
||||||
|
return { ai_assistants };
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [api, dispatch, token]);
|
||||||
|
|
||||||
|
useApiCall<Assistants.Response>(loadData);
|
||||||
|
|
||||||
|
const handleAdvisorClick = (assistant: IAssistant) => {
|
||||||
|
navigate(routes.client.advisorChat(assistant.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.page} page`}>
|
||||||
|
<Title variant="h1" className={styles.title}>
|
||||||
|
Advisors
|
||||||
|
</Title>
|
||||||
|
{!!assistants?.length && !isLoading && (
|
||||||
|
<div className={styles["advisors-container"]}>
|
||||||
|
{assistants.map((assistant, index) => (
|
||||||
|
<AssistantCard
|
||||||
|
key={index}
|
||||||
|
{...assistant}
|
||||||
|
onClickCard={() => handleAdvisorClick(assistant)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<Loader color={LoaderColor.Red} className={styles.loader} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Advisors;
|
||||||
28
src/components/pages/Advisors/styles.module.css
Normal file
28
src/components/pages/Advisors/styles.module.css
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
height: fit-content;
|
||||||
|
min-height: 100dvh;
|
||||||
|
background-color: #232322;
|
||||||
|
padding-top: 32px;
|
||||||
|
padding-bottom: 116px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.advisors-container {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(145px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
@ -39,7 +39,13 @@ function QuestionnaireIntermediatePage() {
|
|||||||
backgroundImage: `url(${backgroundImage})`,
|
backgroundImage: `url(${backgroundImage})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<object type="image/svg+xml" data={StaryKey}>svg-animation</object>
|
<object
|
||||||
|
type="image/svg+xml"
|
||||||
|
data={StaryKey}
|
||||||
|
style={{ minHeight: "138px" }}
|
||||||
|
>
|
||||||
|
svg-animation
|
||||||
|
</object>
|
||||||
<div>
|
<div>
|
||||||
{path && (
|
{path && (
|
||||||
<Title variant="h1" className={styles.title}>
|
<Title variant="h1" className={styles.title}>
|
||||||
|
|||||||
@ -7,6 +7,8 @@ export const apiHost = "https://api-web.aura.wit.life";
|
|||||||
const dApiHost = isProduction ? "https://d.api.witapps.us" : "https://dev.api.witapps.us"
|
const dApiHost = isProduction ? "https://d.api.witapps.us" : "https://dev.api.witapps.us"
|
||||||
const siteHost = "https://aura.wit.life";
|
const siteHost = "https://aura.wit.life";
|
||||||
const prefix = "api/v1";
|
const prefix = "api/v1";
|
||||||
|
const openAIHost = " https://api.openai.com";
|
||||||
|
const openAiPrefix = "v1";
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
client: {
|
client: {
|
||||||
@ -116,6 +118,9 @@ const routes = {
|
|||||||
unlimitedReadings: () => [host, "unlimited-readings"].join("/"),
|
unlimitedReadings: () => [host, "unlimited-readings"].join("/"),
|
||||||
addConsultation: () => [host, "add-consultation"].join("/"),
|
addConsultation: () => [host, "add-consultation"].join("/"),
|
||||||
|
|
||||||
|
// Advisors
|
||||||
|
advisors: () => [host, "advisors"].join("/"),
|
||||||
|
advisorChat: (id: number) => [host, "advisors", id].join("/"),
|
||||||
// Email - Pay - Email
|
// Email - Pay - Email
|
||||||
epeBirthdate: () => [host, "epe", "birthdate"].join("/"),
|
epeBirthdate: () => [host, "epe", "birthdate"].join("/"),
|
||||||
epePayment: () => [host, "epe", "payment"].join("/"),
|
epePayment: () => [host, "epe", "payment"].join("/"),
|
||||||
@ -178,9 +183,27 @@ const routes = {
|
|||||||
),
|
),
|
||||||
getAiRequestsV2: (id: string) =>
|
getAiRequestsV2: (id: string) =>
|
||||||
[apiHost, "api/v2", "ai", "requests", `${id}.json`].join("/"),
|
[apiHost, "api/v2", "ai", "requests", `${id}.json`].join("/"),
|
||||||
|
|
||||||
dApiTestPaymentProducts: () =>
|
dApiTestPaymentProducts: () =>
|
||||||
[dApiHost, "payment", "test", "products"].join("/"),
|
[dApiHost, "payment", "test", "products"].join("/"),
|
||||||
dApiPaymentCheckout: () => [dApiHost, "payment", "checkout"].join("/"),
|
dApiPaymentCheckout: () => [dApiHost, "payment", "checkout"].join("/"),
|
||||||
|
|
||||||
|
assistants: () => [apiHost, prefix, "ai", "assistants.json"].join("/"),
|
||||||
|
setExternalChatIdAssistants: (chatId: string) =>
|
||||||
|
[apiHost, prefix, "ai", "assistants", chatId, "chats.json"].join("/"),
|
||||||
|
},
|
||||||
|
openAi: {
|
||||||
|
createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"),
|
||||||
|
createMessage: (threadId: string) =>
|
||||||
|
[openAIHost, openAiPrefix, "threads", threadId, "messages"].join("/"),
|
||||||
|
getListMessages: (threadId: string) =>
|
||||||
|
[openAIHost, openAiPrefix, "threads", threadId, "messages"].join("/"),
|
||||||
|
runThread: (threadId: string) =>
|
||||||
|
[openAIHost, openAiPrefix, "threads", threadId, "runs"].join("/"),
|
||||||
|
getStatusRunThread: (threadId: string, runId: string) =>
|
||||||
|
[openAIHost, openAiPrefix, "threads", threadId, "runs", runId].join("/"),
|
||||||
|
getListRuns: (threadId: string) =>
|
||||||
|
[openAIHost, openAiPrefix, "threads", threadId, "runs"].join("/"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -199,6 +222,7 @@ export const entrypoints = [
|
|||||||
routes.client.magicBall(),
|
routes.client.magicBall(),
|
||||||
routes.client.trialChoice(),
|
routes.client.trialChoice(),
|
||||||
routes.client.palmistry(),
|
routes.client.palmistry(),
|
||||||
|
routes.client.advisors(),
|
||||||
];
|
];
|
||||||
export const isEntrypoint = (path: string) => entrypoints.includes(path);
|
export const isEntrypoint = (path: string) => entrypoints.includes(path);
|
||||||
export const isNotEntrypoint = (path: string) => !isEntrypoint(path);
|
export const isNotEntrypoint = (path: string) => !isEntrypoint(path);
|
||||||
@ -284,10 +308,14 @@ export const withoutFooterRoutes = [
|
|||||||
routes.client.addReport(),
|
routes.client.addReport(),
|
||||||
routes.client.unlimitedReadings(),
|
routes.client.unlimitedReadings(),
|
||||||
routes.client.addConsultation(),
|
routes.client.addConsultation(),
|
||||||
|
routes.client.advisors(),
|
||||||
routes.client.epeSuccessPayment(),
|
routes.client.epeSuccessPayment(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const withoutFooterPartOfRoutes = [routes.client.questionnaire()];
|
export const withoutFooterPartOfRoutes = [
|
||||||
|
routes.client.questionnaire(),
|
||||||
|
routes.client.advisors(),
|
||||||
|
];
|
||||||
|
|
||||||
export const hasNoFooter = (path: string) => {
|
export const hasNoFooter = (path: string) => {
|
||||||
const targetRoute = withoutFooterPartOfRoutes.findIndex((route) =>
|
const targetRoute = withoutFooterPartOfRoutes.findIndex((route) =>
|
||||||
@ -304,6 +332,7 @@ export const withNavbarFooterRoutes = [
|
|||||||
routes.client.breath(),
|
routes.client.breath(),
|
||||||
routes.client.breathResult(),
|
routes.client.breathResult(),
|
||||||
routes.client.wallpaper(),
|
routes.client.wallpaper(),
|
||||||
|
routes.client.advisors(),
|
||||||
];
|
];
|
||||||
export const hasNavbarFooter = (path: string) =>
|
export const hasNavbarFooter = (path: string) =>
|
||||||
withNavbarFooterRoutes.includes(path);
|
withNavbarFooterRoutes.includes(path);
|
||||||
@ -353,6 +382,7 @@ export const withoutHeaderRoutes = [
|
|||||||
routes.client.email("marketing-landing"),
|
routes.client.email("marketing-landing"),
|
||||||
routes.client.email("marketing-trial-payment"),
|
routes.client.email("marketing-trial-payment"),
|
||||||
routes.client.tryApp(),
|
routes.client.tryApp(),
|
||||||
|
routes.client.advisors(),
|
||||||
routes.client.epeSuccessPayment(),
|
routes.client.epeSuccessPayment(),
|
||||||
];
|
];
|
||||||
export const hasNoHeader = (path: string) => {
|
export const hasNoHeader = (path: string) => {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import form, {
|
|||||||
import aura, { actions as auraActions } from "./aura";
|
import aura, { actions as auraActions } from "./aura";
|
||||||
import siteConfig, {
|
import siteConfig, {
|
||||||
selectHome,
|
selectHome,
|
||||||
|
selectOpenAiToken,
|
||||||
actions as siteConfigActions,
|
actions as siteConfigActions,
|
||||||
} from "./siteConfig";
|
} from "./siteConfig";
|
||||||
import onboardingConfig, {
|
import onboardingConfig, {
|
||||||
@ -105,6 +106,7 @@ export const selectors = {
|
|||||||
selectQuestionnaire,
|
selectQuestionnaire,
|
||||||
selectUserDeviceType,
|
selectUserDeviceType,
|
||||||
selectIsShowTryApp,
|
selectIsShowTryApp,
|
||||||
|
selectOpenAiToken,
|
||||||
...formSelectors,
|
...formSelectors,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ interface ISiteConfig {
|
|||||||
isShowNavbar: boolean;
|
isShowNavbar: boolean;
|
||||||
pathFromHome: EPathsFromHome;
|
pathFromHome: EPathsFromHome;
|
||||||
};
|
};
|
||||||
|
openAiToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ISiteConfig = {
|
const initialState: ISiteConfig = {
|
||||||
@ -19,6 +20,7 @@ const initialState: ISiteConfig = {
|
|||||||
isShowNavbar: false,
|
isShowNavbar: false,
|
||||||
pathFromHome: EPathsFromHome.compatibility,
|
pathFromHome: EPathsFromHome.compatibility,
|
||||||
},
|
},
|
||||||
|
openAiToken: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const siteConfigSlice = createSlice({
|
const siteConfigSlice = createSlice({
|
||||||
@ -37,4 +39,8 @@ export const selectHome = createSelector(
|
|||||||
(state: { siteConfig: ISiteConfig }) => state.siteConfig.home,
|
(state: { siteConfig: ISiteConfig }) => state.siteConfig.home,
|
||||||
(siteConfig) => siteConfig
|
(siteConfig) => siteConfig
|
||||||
);
|
);
|
||||||
|
export const selectOpenAiToken = createSelector(
|
||||||
|
(state: { siteConfig: ISiteConfig }) => state.siteConfig.openAiToken,
|
||||||
|
(siteConfig) => siteConfig
|
||||||
|
);
|
||||||
export default siteConfigSlice.reducer;
|
export default siteConfigSlice.reducer;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user