Preview/advisors
This commit is contained in:
parent
40c9809225
commit
238160d610
@ -58,11 +58,11 @@
|
||||
region: "eu",
|
||||
});
|
||||
</script> -->
|
||||
<script
|
||||
<!-- <script
|
||||
type="text/javascript"
|
||||
src="//script.crazyegg.com/pages/scripts/0121/5622.js"
|
||||
async="async"
|
||||
></script>
|
||||
></script> -->
|
||||
</head>
|
||||
<body>
|
||||
<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": {
|
||||
"@chargebee/chargebee-js-react-wrapper": "^0.6.3",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@smakss/react-scroll-direction": "^4.0.4",
|
||||
"@stripe/react-stripe-js": "^2.3.1",
|
||||
"@stripe/stripe-js": "^2.1.9",
|
||||
"apng-js": "^1.1.1",
|
||||
@ -1022,6 +1023,17 @@
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.3.1.tgz",
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@chargebee/chargebee-js-react-wrapper": "^0.6.3",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@smakss/react-scroll-direction": "^4.0.4",
|
||||
"@stripe/react-stripe-js": "^2.3.1",
|
||||
"@stripe/stripe-js": "^2.1.9",
|
||||
"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,
|
||||
AppleAuth,
|
||||
AIRequestsV2,
|
||||
Assistants,
|
||||
OpenAI
|
||||
SinglePayment,
|
||||
} from './resources'
|
||||
|
||||
@ -56,6 +58,15 @@ const api = {
|
||||
getZodiacs: createMethod<Zodiacs.Payload, Zodiacs.Response>(Zodiacs.createRequest),
|
||||
AIRequestsV2: createMethod<AIRequestsV2.Payload, AIRequestsV2.Response>(AIRequestsV2.createRequest),
|
||||
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),
|
||||
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 AppleAuth from "./AppleAuth";
|
||||
export * as AIRequestsV2 from "./AIRequestsV2";
|
||||
export * as Assistants from "./Assistants";
|
||||
export * as OpenAI from "./OpenAI";
|
||||
export * as SinglePayment from "./SinglePayment";
|
||||
|
||||
@ -1,34 +1,48 @@
|
||||
import { AuthToken } from './types'
|
||||
import { ErrorResponse, isErrorResponse, ApiError, buildUnknownError } from './errors'
|
||||
import { AuthToken } from "./types";
|
||||
import {
|
||||
ErrorResponse,
|
||||
isErrorResponse,
|
||||
ApiError,
|
||||
buildUnknownError,
|
||||
} from "./errors";
|
||||
|
||||
export function createMethod<P, R>(createRequest: (payload: P) => Request) {
|
||||
return async (payload: P): Promise<R> => {
|
||||
const request = createRequest(payload)
|
||||
const response = await fetch(request)
|
||||
const data: R | ErrorResponse = await response.json()
|
||||
const statusCode = response.status
|
||||
const request = createRequest(payload);
|
||||
const response = await fetch(request);
|
||||
const data: R | ErrorResponse = await response.json();
|
||||
const statusCode = response.status;
|
||||
|
||||
if (!response.ok) {
|
||||
const body = isErrorResponse<R>(data) ? data : { error: buildUnknownError(statusCode) }
|
||||
throw new ApiError({ body, statusCode })
|
||||
const body = isErrorResponse<R>(data)
|
||||
? data
|
||||
: { error: buildUnknownError(statusCode) };
|
||||
throw new ApiError({ body, statusCode });
|
||||
}
|
||||
|
||||
if (isErrorResponse<R>(data)) {
|
||||
throw new ApiError({ body: data, statusCode })
|
||||
throw new ApiError({ body: data, statusCode });
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
export function getBaseHeaders(): Headers {
|
||||
return new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
}
|
||||
|
||||
export function getAuthHeaders(token: AuthToken): Headers {
|
||||
const headers = getBaseHeaders()
|
||||
headers.append('Authorization', `Bearer ${token}`)
|
||||
return headers
|
||||
const headers = getBaseHeaders();
|
||||
headers.append("Authorization", `Bearer ${token}`);
|
||||
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 AddConsultationPage from "../pages/AdditionalPurchases/pages/AddConsultation";
|
||||
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 SuccessPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage";
|
||||
import FailPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage";
|
||||
@ -125,6 +127,15 @@ function App(): JSX.Element {
|
||||
const [searchParams] = useSearchParams();
|
||||
const jwtToken = searchParams.get("token");
|
||||
|
||||
useEffect(() => {
|
||||
// api.getAppConfig({ bundleId: "auraweb" }),
|
||||
dispatch(
|
||||
actions.siteConfig.update({
|
||||
openAiToken: "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",
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isProduction) return;
|
||||
ReactGA.send({
|
||||
@ -453,6 +464,10 @@ function App(): JSX.Element {
|
||||
path={routes.client.wallpaper()}
|
||||
element={<WallpaperPage />}
|
||||
/>
|
||||
<Route path={routes.client.advisors()} element={<Advisors />} />
|
||||
<Route path={routes.client.advisors()}>
|
||||
<Route path=":id" element={<AdvisorChatPage />} />
|
||||
</Route>
|
||||
<Route
|
||||
path={routes.client.magicBall()}
|
||||
element={<MagicBallPage />}
|
||||
@ -578,6 +593,13 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
|
||||
image: "Compatibility.svg",
|
||||
onClick: handleCompatibility,
|
||||
},
|
||||
{
|
||||
title: "Advisors",
|
||||
path: routes.client.advisors(),
|
||||
paths: [routes.client.advisors()],
|
||||
image: "moon.svg",
|
||||
onClick: () => null,
|
||||
},
|
||||
{
|
||||
title: "My Moon",
|
||||
path: routes.client.wallpaper(),
|
||||
|
||||
@ -29,6 +29,15 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const homeConfig = useSelector(selectors.selectHome);
|
||||
const showNavbarFooter = homeConfig.isShowNavbar;
|
||||
const queryTimeOutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (queryTimeOutRef.current) {
|
||||
clearTimeout(queryTimeOutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpenModal) return;
|
||||
@ -67,8 +76,7 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element {
|
||||
setIsOpenModal(false);
|
||||
};
|
||||
|
||||
|
||||
const token = useSelector(selectors.selectToken)
|
||||
const token = useSelector(selectors.selectToken);
|
||||
const createCallback = useCallback(async () => {
|
||||
const data: UserCallbacks.PayloadPost = {
|
||||
data: {
|
||||
@ -87,7 +95,7 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element {
|
||||
token,
|
||||
});
|
||||
if (!getCallback.user_callback.is_complete) {
|
||||
setTimeout(getUserCallbacksRequest, 3000);
|
||||
queryTimeOutRef.current = setTimeout(getUserCallbacksRequest, 3000);
|
||||
}
|
||||
dispatch(
|
||||
actions.userCallbacks.update({
|
||||
|
||||
@ -3,7 +3,7 @@ import Title from "../Title";
|
||||
import styles from "./styles.module.css";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
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 { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
@ -26,6 +26,15 @@ function CompatResultPage(): JSX.Element {
|
||||
const [isOpenModal, setIsOpenModal] = useState(true);
|
||||
const [isVisualLoading, setIsVisualLoading] = useState(true);
|
||||
const [isDataLoading, setIsDataLoading] = useState(true);
|
||||
const queryTimeOutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (queryTimeOutRef.current) {
|
||||
clearTimeout(queryTimeOutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleNext = () => {
|
||||
if (homeConfig.pathFromHome === EPathsFromHome.breath) {
|
||||
@ -67,7 +76,7 @@ function CompatResultPage(): JSX.Element {
|
||||
token,
|
||||
});
|
||||
if (aIRequest.ai_request.state !== "ready") {
|
||||
setTimeout(loadAIRequest, 3000);
|
||||
queryTimeOutRef.current = setTimeout(loadAIRequest, 3000);
|
||||
}
|
||||
setText(aIRequest?.ai_request?.response?.body || "Loading...");
|
||||
setIsDataLoading(false);
|
||||
|
||||
@ -1,21 +1,30 @@
|
||||
import './styles.css'
|
||||
import "./styles.css";
|
||||
|
||||
export enum LoaderColor {
|
||||
White = 'white',
|
||||
Black = 'black',
|
||||
White = "white",
|
||||
Black = "black",
|
||||
Red = "red",
|
||||
}
|
||||
|
||||
type LoaderProps = {
|
||||
color?: LoaderColor
|
||||
}
|
||||
color?: LoaderColor;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Loader({ color = LoaderColor.Black }: LoaderProps): JSX.Element {
|
||||
const colorClass = color === LoaderColor.White ? 'loader__white' : 'loader__black'
|
||||
const colorClasses = {
|
||||
[LoaderColor.White]: "loader__white",
|
||||
[LoaderColor.Black]: "loader__black",
|
||||
[LoaderColor.Red]: "loader__red",
|
||||
};
|
||||
|
||||
function Loader({ color = LoaderColor.Black, className }: LoaderProps): JSX.Element {
|
||||
return (
|
||||
<div className='loader-container'>
|
||||
<div className={`loader ${colorClass}`}><span></span></div>
|
||||
<div className={`loader-container ${className}`}>
|
||||
<div className={`loader ${colorClasses[color]}`}>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Loader
|
||||
export default Loader;
|
||||
|
||||
@ -26,6 +26,10 @@
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.loader.loader__red span::after {
|
||||
border-color: #ff2c57;
|
||||
}
|
||||
|
||||
@keyframes loader-1-1 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
|
||||
@ -29,7 +29,7 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
|
||||
|
||||
const [isShowOnboardingNavbarFooter, setIsShowOnboardingNavbarFooter] =
|
||||
useState(!onboardingConfigNavbarFooter?.isShown);
|
||||
const [selectedOnboarding, setSelectedOnboarding] = useState(3);
|
||||
const [selectedOnboarding, setSelectedOnboarding] = useState(4);
|
||||
const [rerender, setRerender] = useState(false);
|
||||
rerender
|
||||
|
||||
@ -66,6 +66,12 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
|
||||
onClick: () => setShownOnboarding(),
|
||||
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"),
|
||||
onClick: () => setSelectedOnboarding(0),
|
||||
@ -74,7 +80,7 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
|
||||
];
|
||||
|
||||
const handleClick = (item: INavbarHomeItems) => {
|
||||
onboardingsSettings[selectedOnboarding].onClick();
|
||||
onboardingsSettings[selectedOnboarding]?.onClick();
|
||||
if (item.onClick) {
|
||||
item.onClick();
|
||||
}
|
||||
@ -96,9 +102,9 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
|
||||
isShow={index === selectedOnboarding}
|
||||
>
|
||||
<TextWithFinger
|
||||
text={onboardingsSettings[index].text}
|
||||
crossClickHandler={() => onboardingsSettings[index].onClick()}
|
||||
classNameText={onboardingsSettings[index].classNameText}
|
||||
text={onboardingsSettings[index]?.text || ""}
|
||||
crossClickHandler={() => onboardingsSettings[index]?.onClick()}
|
||||
classNameText={onboardingsSettings[index]?.classNameText}
|
||||
/>
|
||||
</Onboarding>
|
||||
)}
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
.result-container__value-label {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.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})`,
|
||||
}}
|
||||
>
|
||||
<object type="image/svg+xml" data={StaryKey}>svg-animation</object>
|
||||
<object
|
||||
type="image/svg+xml"
|
||||
data={StaryKey}
|
||||
style={{ minHeight: "138px" }}
|
||||
>
|
||||
svg-animation
|
||||
</object>
|
||||
<div>
|
||||
{path && (
|
||||
<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 siteHost = "https://aura.wit.life";
|
||||
const prefix = "api/v1";
|
||||
const openAIHost = " https://api.openai.com";
|
||||
const openAiPrefix = "v1";
|
||||
|
||||
const routes = {
|
||||
client: {
|
||||
@ -116,6 +118,9 @@ const routes = {
|
||||
unlimitedReadings: () => [host, "unlimited-readings"].join("/"),
|
||||
addConsultation: () => [host, "add-consultation"].join("/"),
|
||||
|
||||
// Advisors
|
||||
advisors: () => [host, "advisors"].join("/"),
|
||||
advisorChat: (id: number) => [host, "advisors", id].join("/"),
|
||||
// Email - Pay - Email
|
||||
epeBirthdate: () => [host, "epe", "birthdate"].join("/"),
|
||||
epePayment: () => [host, "epe", "payment"].join("/"),
|
||||
@ -178,9 +183,27 @@ const routes = {
|
||||
),
|
||||
getAiRequestsV2: (id: string) =>
|
||||
[apiHost, "api/v2", "ai", "requests", `${id}.json`].join("/"),
|
||||
|
||||
dApiTestPaymentProducts: () =>
|
||||
[dApiHost, "payment", "test", "products"].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.trialChoice(),
|
||||
routes.client.palmistry(),
|
||||
routes.client.advisors(),
|
||||
];
|
||||
export const isEntrypoint = (path: string) => entrypoints.includes(path);
|
||||
export const isNotEntrypoint = (path: string) => !isEntrypoint(path);
|
||||
@ -284,10 +308,14 @@ export const withoutFooterRoutes = [
|
||||
routes.client.addReport(),
|
||||
routes.client.unlimitedReadings(),
|
||||
routes.client.addConsultation(),
|
||||
routes.client.advisors(),
|
||||
routes.client.epeSuccessPayment(),
|
||||
];
|
||||
|
||||
export const withoutFooterPartOfRoutes = [routes.client.questionnaire()];
|
||||
export const withoutFooterPartOfRoutes = [
|
||||
routes.client.questionnaire(),
|
||||
routes.client.advisors(),
|
||||
];
|
||||
|
||||
export const hasNoFooter = (path: string) => {
|
||||
const targetRoute = withoutFooterPartOfRoutes.findIndex((route) =>
|
||||
@ -304,6 +332,7 @@ export const withNavbarFooterRoutes = [
|
||||
routes.client.breath(),
|
||||
routes.client.breathResult(),
|
||||
routes.client.wallpaper(),
|
||||
routes.client.advisors(),
|
||||
];
|
||||
export const hasNavbarFooter = (path: string) =>
|
||||
withNavbarFooterRoutes.includes(path);
|
||||
@ -353,6 +382,7 @@ export const withoutHeaderRoutes = [
|
||||
routes.client.email("marketing-landing"),
|
||||
routes.client.email("marketing-trial-payment"),
|
||||
routes.client.tryApp(),
|
||||
routes.client.advisors(),
|
||||
routes.client.epeSuccessPayment(),
|
||||
];
|
||||
export const hasNoHeader = (path: string) => {
|
||||
|
||||
@ -16,6 +16,7 @@ import form, {
|
||||
import aura, { actions as auraActions } from "./aura";
|
||||
import siteConfig, {
|
||||
selectHome,
|
||||
selectOpenAiToken,
|
||||
actions as siteConfigActions,
|
||||
} from "./siteConfig";
|
||||
import onboardingConfig, {
|
||||
@ -105,6 +106,7 @@ export const selectors = {
|
||||
selectQuestionnaire,
|
||||
selectUserDeviceType,
|
||||
selectIsShowTryApp,
|
||||
selectOpenAiToken,
|
||||
...formSelectors,
|
||||
};
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ interface ISiteConfig {
|
||||
isShowNavbar: boolean;
|
||||
pathFromHome: EPathsFromHome;
|
||||
};
|
||||
openAiToken: string;
|
||||
}
|
||||
|
||||
const initialState: ISiteConfig = {
|
||||
@ -19,6 +20,7 @@ const initialState: ISiteConfig = {
|
||||
isShowNavbar: false,
|
||||
pathFromHome: EPathsFromHome.compatibility,
|
||||
},
|
||||
openAiToken: "",
|
||||
};
|
||||
|
||||
const siteConfigSlice = createSlice({
|
||||
@ -37,4 +39,8 @@ export const selectHome = createSelector(
|
||||
(state: { siteConfig: ISiteConfig }) => state.siteConfig.home,
|
||||
(siteConfig) => siteConfig
|
||||
);
|
||||
export const selectOpenAiToken = createSelector(
|
||||
(state: { siteConfig: ISiteConfig }) => state.siteConfig.openAiToken,
|
||||
(siteConfig) => siteConfig
|
||||
);
|
||||
export default siteConfigSlice.reducer;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user