Preview/advisors

This commit is contained in:
Денис Катаев 2024-03-27 18:49:58 +00:00 committed by Victor Ershov
parent 40c9809225
commit 238160d610
35 changed files with 1431 additions and 41 deletions

View File

@ -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
View File

@ -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",

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

8
public/star.svg Normal file
View 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

View File

@ -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),
}

View 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
View 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,
});
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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(),

View File

@ -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({

View File

@ -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);

View File

@ -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;

View File

@ -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); }

View File

@ -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>
)}

View File

@ -26,6 +26,7 @@
.result-container__value-label {
color: #fff;
font-weight: 500;
text-align: left;
}
.result-container__value-value {

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
import styles from "./styles.module.css"
function LoaderDots() {
return (
<div className={styles.container} />
)
}
export default LoaderDots

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;
}

View 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;

View 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%);
}

View File

@ -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;

View File

@ -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%;
}

View 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;

View 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;
}

View File

@ -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}>

View File

@ -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) => {

View File

@ -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,
};

View File

@ -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;