diff --git a/index.html b/index.html index f1dab0c..2d0d1da 100755 --- a/index.html +++ b/index.html @@ -58,11 +58,11 @@ region: "eu", }); --> - + > --> diff --git a/package-lock.json b/package-lock.json index e1139c6..7dc1d88 100755 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c66ba58..03a0ec7 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/star.png b/public/star.png new file mode 100644 index 0000000..003bd4f Binary files /dev/null and b/public/star.png differ diff --git a/public/star.svg b/public/star.svg new file mode 100644 index 0000000..eab0dd5 --- /dev/null +++ b/public/star.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/api/api.ts b/src/api/api.ts index 9ba2ec0..a854254 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -24,6 +24,8 @@ import { SubscriptionPlans, AppleAuth, AIRequestsV2, + Assistants, + OpenAI SinglePayment, } from './resources' @@ -56,6 +58,15 @@ const api = { getZodiacs: createMethod(Zodiacs.createRequest), AIRequestsV2: createMethod(AIRequestsV2.createRequest), getAIRequestsV2: createMethod(AIRequestsV2.createRequestGet), + // Advisors chats + assistants: createMethod(Assistants.createRequest), + setExternalChatIdAssistant: createMethod(Assistants.createRequestSetExternalChatId), + createThread: createMethod(OpenAI.createRequest), + createMessage: createMethod(OpenAI.createRequest), + getListMessages: createMethod(OpenAI.createRequest), + runThread: createMethod(OpenAI.createRequest), + getStatusRunThread: createMethod(OpenAI.createRequest), + getListRuns: createMethod(OpenAI.createRequest), getSinglePaymentProducts: createMethod(SinglePayment.createRequestGet), createSinglePayment: createMethod(SinglePayment.createRequestPost), } diff --git a/src/api/resources/Assistants.ts b/src/api/resources/Assistants.ts new file mode 100644 index 0000000..1dcf735 --- /dev/null +++ b/src/api/resources/Assistants.ts @@ -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, + }); +}; diff --git a/src/api/resources/OpenAI.ts b/src/api/resources/OpenAI.ts new file mode 100644 index 0000000..4697dc5 --- /dev/null +++ b/src/api/resources/OpenAI.ts @@ -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, + }); +} diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index bc355c7..6c11bf3 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -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"; diff --git a/src/api/utils.ts b/src/api/utils.ts index 6464a96..39f80a1 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -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(createRequest: (payload: P) => Request) { return async (payload: P): Promise => { - 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(data) ? data : { error: buildUnknownError(statusCode) } - throw new ApiError({ body, statusCode }) + const body = isErrorResponse(data) + ? data + : { error: buildUnknownError(statusCode) }; + throw new ApiError({ body, statusCode }); } if (isErrorResponse(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; } diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 97356c3..7713ecb 100755 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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={} /> + } /> + + } /> + } @@ -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(), diff --git a/src/components/BreathPage/index.tsx b/src/components/BreathPage/index.tsx index ca23057..d18d01f 100644 --- a/src/components/BreathPage/index.tsx +++ b/src/components/BreathPage/index.tsx @@ -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(); + + 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({ diff --git a/src/components/CompatResultPage/index.tsx b/src/components/CompatResultPage/index.tsx index abe7088..5bd1588 100644 --- a/src/components/CompatResultPage/index.tsx +++ b/src/components/CompatResultPage/index.tsx @@ -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(); + + 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); diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx index 034bbaa..bf10f33 100644 --- a/src/components/Loader/index.tsx +++ b/src/components/Loader/index.tsx @@ -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 ( -
-
+
+
+ +
- ) + ); } -export default Loader +export default Loader; diff --git a/src/components/Loader/styles.css b/src/components/Loader/styles.css index 476e0a7..7b33eb6 100644 --- a/src/components/Loader/styles.css +++ b/src/components/Loader/styles.css @@ -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); } diff --git a/src/components/NavbarFooter/index.tsx b/src/components/NavbarFooter/index.tsx index 95c65dc..974c84e 100644 --- a/src/components/NavbarFooter/index.tsx +++ b/src/components/NavbarFooter/index.tsx @@ -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} > onboardingsSettings[index].onClick()} - classNameText={onboardingsSettings[index].classNameText} + text={onboardingsSettings[index]?.text || ""} + crossClickHandler={() => onboardingsSettings[index]?.onClick()} + classNameText={onboardingsSettings[index]?.classNameText} /> )} diff --git a/src/components/UserCallbacksPage/styles.module.css b/src/components/UserCallbacksPage/styles.module.css index 73abc19..1bc8293 100644 --- a/src/components/UserCallbacksPage/styles.module.css +++ b/src/components/UserCallbacksPage/styles.module.css @@ -26,6 +26,7 @@ .result-container__value-label { color: #fff; font-weight: 500; + text-align: left; } .result-container__value-value { diff --git a/src/components/pages/AdvisorChat/components/ChatHeader/index.tsx b/src/components/pages/AdvisorChat/components/ChatHeader/index.tsx new file mode 100644 index 0000000..f9d31b7 --- /dev/null +++ b/src/components/pages/AdvisorChat/components/ChatHeader/index.tsx @@ -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 ( +
+
+
Advisors +
+
+ {name} + +
+ {name} +
+ ); +} + +export default ChatHeader; diff --git a/src/components/pages/AdvisorChat/components/ChatHeader/styles.module.css b/src/components/pages/AdvisorChat/components/ChatHeader/styles.module.css new file mode 100644 index 0000000..7097cf2 --- /dev/null +++ b/src/components/pages/AdvisorChat/components/ChatHeader/styles.module.css @@ -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); +} diff --git a/src/components/pages/AdvisorChat/components/InputMessage/index.tsx b/src/components/pages/AdvisorChat/components/InputMessage/index.tsx new file mode 100644 index 0000000..4aaed23 --- /dev/null +++ b/src/components/pages/AdvisorChat/components/InputMessage/index.tsx @@ -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) => void; + submitForm: (messageText: string) => void; +} + +function InputMessage({ + placeholder, + messageText, + textareaRows, + disabledButton, + disabledTextArea, + isLoading, + handleChangeMessageText, + submitForm, + classNameContainer = "", +}: IInputMessageProps) { + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + submitForm(messageText); + }; + + return ( +
handleSubmit(e)} + > +