2025-06-01 21:27:22 +00:00
|
|
|
|
import { Box, Container, Typography, TextField, Button, List, ListItem, IconButton, Modal } from "@mui/material";
|
2025-05-25 13:21:19 +00:00
|
|
|
|
import { InfoPopover } from '@ui_kit/InfoPopover';
|
|
|
|
|
import GenderAndAgeSelector from "./GenderAndAgeSelector";
|
2025-06-01 14:05:41 +00:00
|
|
|
|
import { useEffect, useState } from "react";
|
2025-05-25 13:21:19 +00:00
|
|
|
|
import CustomTextField from "@ui_kit/CustomTextField";
|
|
|
|
|
import { useTheme } from "@mui/material";
|
2025-06-01 21:27:22 +00:00
|
|
|
|
import { AuditoryItem, auditoryAdd, auditoryDelete, auditoryGet } from "@/api/auditory";
|
2025-06-01 14:05:41 +00:00
|
|
|
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
|
|
|
|
import { AuditoryList } from "./AuditoryList";
|
2025-06-01 21:36:55 +00:00
|
|
|
|
import { useSnackbar } from "notistack";
|
2025-06-08 17:52:55 +00:00
|
|
|
|
import { PayModal } from "./PayModal";
|
|
|
|
|
import { useUserStore } from "@/stores/user";
|
|
|
|
|
import { cartApi } from "@/api/cart";
|
2025-05-25 13:21:19 +00:00
|
|
|
|
|
2025-06-08 17:52:55 +00:00
|
|
|
|
const tariff = "6844b8858258f5cc35791ef7";
|
2025-05-25 13:21:19 +00:00
|
|
|
|
export default function PersonalizationAI() {
|
|
|
|
|
const theme = useTheme();
|
2025-06-01 21:27:22 +00:00
|
|
|
|
const [auditory, setAuditory] = useState<AuditoryItem[]>([]);
|
|
|
|
|
const [deleteModal, setDeleteModal] = useState<number>(0);
|
2025-06-01 22:48:35 +00:00
|
|
|
|
const [link, setLink] = useState<string>('');
|
|
|
|
|
const [utmParams, setUtmParams] = useState<string>('');
|
2025-06-01 21:27:22 +00:00
|
|
|
|
const quiz = useCurrentQuiz();
|
2025-06-01 21:36:55 +00:00
|
|
|
|
const { enqueueSnackbar } = useSnackbar();
|
2025-06-08 17:52:55 +00:00
|
|
|
|
const privilegesOfUser = useUserStore((state) => state.userAccount?.privileges);
|
|
|
|
|
|
|
|
|
|
const [isStartCreate, setStartCreate] = useState(false);
|
|
|
|
|
const [gender, setGender] = useState<string>('');
|
|
|
|
|
const [age, setAge] = useState<string>('');
|
|
|
|
|
const [ageError, setAgeError] = useState(false);
|
|
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
setGender('');
|
|
|
|
|
setAge('');
|
|
|
|
|
setAgeError(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const checkPrivileges = async () => {
|
|
|
|
|
// TODO: Здесь будет проверка прав пользователя
|
|
|
|
|
console.log("______________privilegesOfUser");
|
|
|
|
|
console.log(privilegesOfUser);
|
|
|
|
|
const [_, addError] = await cartApi.add(tariff);
|
|
|
|
|
|
|
|
|
|
if (addError) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Если нам хватает денежек - покупаем тариф
|
|
|
|
|
const [data, payError] = await cartApi.pay();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createNewLink = async () => {
|
|
|
|
|
if (!quiz?.backendId) {
|
|
|
|
|
enqueueSnackbar('Ошибка: не выбран квиз', { variant: 'error' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const [result, error] = await auditoryAdd({
|
|
|
|
|
quizId: quiz.backendId,
|
|
|
|
|
body: {
|
|
|
|
|
sex: gender === "male",
|
|
|
|
|
age
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
enqueueSnackbar('Не удалось добавить ссылку', { variant: 'error' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result) {
|
|
|
|
|
handleAdd({
|
|
|
|
|
id: result.ID,
|
|
|
|
|
quiz_id: quiz.backendId,
|
|
|
|
|
sex: gender === "male",
|
|
|
|
|
age,
|
|
|
|
|
deleted: false,
|
|
|
|
|
});
|
|
|
|
|
enqueueSnackbar('Ссылка успешно добавлена', { variant: 'success' });
|
|
|
|
|
resetForm();
|
|
|
|
|
setIsModalOpen(false);
|
|
|
|
|
setStartCreate(false);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
enqueueSnackbar('Произошла ошибка при добавлении', { variant: 'error' });
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-06-01 21:27:22 +00:00
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
(async () => {
|
|
|
|
|
if (quiz?.backendId) {
|
|
|
|
|
const [result, error] = await auditoryGet({ quizId: quiz.backendId });
|
|
|
|
|
console.log("result-___---_------__---__-__---_------__---__-__---_------__---__-__---_------__---__-____--__")
|
|
|
|
|
console.log(result)
|
|
|
|
|
if (result) {
|
|
|
|
|
setAuditory(result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
}, [quiz]);
|
|
|
|
|
|
2025-06-01 21:36:55 +00:00
|
|
|
|
const handleDelete = async () => {
|
|
|
|
|
// 1. Закрываем модалку
|
|
|
|
|
setDeleteModal(0);
|
2025-06-06 13:25:06 +00:00
|
|
|
|
|
2025-06-01 21:36:55 +00:00
|
|
|
|
// 2. Находим индекс объекта в стейте
|
|
|
|
|
const indexToDelete = auditory.findIndex(item => item.id === deleteModal);
|
|
|
|
|
if (indexToDelete === -1) return;
|
2025-06-06 13:25:06 +00:00
|
|
|
|
|
2025-06-01 21:36:55 +00:00
|
|
|
|
// 3. Сохраняем удаляемый объект
|
|
|
|
|
const deletedItem = auditory[indexToDelete];
|
2025-06-06 13:25:06 +00:00
|
|
|
|
|
2025-06-01 21:36:55 +00:00
|
|
|
|
// 4. Меняем стейт, вырезая объект
|
|
|
|
|
setAuditory(prev => prev.filter(item => item.id !== deleteModal));
|
2025-06-06 13:25:06 +00:00
|
|
|
|
|
2025-06-01 21:36:55 +00:00
|
|
|
|
try {
|
|
|
|
|
// 5. Вызываем функцию удаления
|
2025-06-06 13:25:06 +00:00
|
|
|
|
const [result, error] = await auditoryDelete({
|
|
|
|
|
quizId: quiz?.backendId,
|
|
|
|
|
auditoryId: deleteModal
|
2025-06-01 21:36:55 +00:00
|
|
|
|
});
|
2025-06-06 13:25:06 +00:00
|
|
|
|
|
2025-06-01 21:36:55 +00:00
|
|
|
|
if (error) {
|
|
|
|
|
// 6. Если удалить не удалось - показываем снекбар и возвращаем ссылку
|
|
|
|
|
enqueueSnackbar('Не удалось удалить ссылку', { variant: 'error' });
|
|
|
|
|
setAuditory(prev => {
|
|
|
|
|
const newArray = [...prev];
|
|
|
|
|
newArray.splice(indexToDelete, 0, deletedItem);
|
|
|
|
|
return newArray;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Обработка ошибки сети или других ошибок
|
|
|
|
|
enqueueSnackbar('Произошла ошибка при удалении', { variant: 'error' });
|
|
|
|
|
setAuditory(prev => {
|
|
|
|
|
const newArray = [...prev];
|
|
|
|
|
newArray.splice(indexToDelete, 0, deletedItem);
|
|
|
|
|
return newArray;
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-01 21:27:22 +00:00
|
|
|
|
}
|
2025-05-25 13:21:19 +00:00
|
|
|
|
|
2025-06-06 13:25:06 +00:00
|
|
|
|
const handleAdd = (item: AuditoryItem) => {
|
2025-06-08 17:52:55 +00:00
|
|
|
|
setAuditory(old => ([...old, item]));
|
|
|
|
|
// Очищаем форму после успешного добавления
|
|
|
|
|
setGender('');
|
|
|
|
|
setAge('');
|
2025-06-01 22:02:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-01 22:48:35 +00:00
|
|
|
|
const handleLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
const newLink = e.target.value;
|
|
|
|
|
setLink(newLink);
|
|
|
|
|
|
|
|
|
|
// Регулярное выражение для поиска параметров URL
|
|
|
|
|
const paramRegex = /[?&]([^=&]+)=([^&]*)/g;
|
|
|
|
|
const params: Record<string, string> = {};
|
|
|
|
|
let match;
|
|
|
|
|
|
|
|
|
|
// Ищем все параметры в строке
|
|
|
|
|
while ((match = paramRegex.exec(newLink)) !== null) {
|
|
|
|
|
const key = decodeURIComponent(match[1]);
|
|
|
|
|
const value = decodeURIComponent(match[2]);
|
|
|
|
|
params[key] = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Преобразуем объект параметров в строку URL
|
|
|
|
|
const paramString = Object.entries(params)
|
|
|
|
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
|
|
|
.join('&');
|
|
|
|
|
|
2025-06-02 15:53:46 +00:00
|
|
|
|
setUtmParams(paramString ? `&${paramString}` : "");
|
2025-06-01 22:48:35 +00:00
|
|
|
|
};
|
|
|
|
|
|
2025-06-08 17:52:55 +00:00
|
|
|
|
const startCreate = () => {
|
|
|
|
|
setStartCreate(true);
|
|
|
|
|
if (checkPrivileges()) {
|
|
|
|
|
setIsModalOpen(true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-25 13:21:19 +00:00
|
|
|
|
return (
|
2025-06-01 21:27:22 +00:00
|
|
|
|
<>
|
2025-06-06 13:25:06 +00:00
|
|
|
|
<Container id="PersonalizationAI" maxWidth={false} sx={{ minHeight: "100%", p: "20px", height: " calc(100vh - 80px)", overflow: "auto", pt: "55px" }}>
|
2025-06-01 21:27:22 +00:00
|
|
|
|
<Typography variant="h5" color={theme.palette.grey3.main} fontWeight={700} sx={{ fontSize: 24, letterSpacing: "-0.2px" }}>
|
|
|
|
|
Персонализация вопросов с помощью AI
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography sx={{
|
|
|
|
|
color: theme.palette.grey3.main, fontSize: "18px", maxWidth: 796, m: 0,
|
|
|
|
|
mt: "19px",
|
|
|
|
|
letterSpacing: "0.009px",
|
|
|
|
|
wordSpacing: "0.1px",
|
|
|
|
|
lineHeight: "21.4px"
|
|
|
|
|
}}>
|
2025-06-06 13:25:06 +00:00
|
|
|
|
Данный раздел позволяет вам создавать персонализированный опрос под каждую целевую аудиторию отдельно, наш AI перефразирует ваши вопросы согласно настройкам. Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже. Так же вы можете обогатить свою ссылку UTM метками </Typography>
|
2025-06-01 21:27:22 +00:00
|
|
|
|
{/* Первый белый блок */}
|
|
|
|
|
<Box sx={{
|
|
|
|
|
bgcolor: "#fff",
|
|
|
|
|
borderRadius: "12px",
|
|
|
|
|
mt: "40px",
|
|
|
|
|
p: "20px 20px 30px",
|
|
|
|
|
boxShadow: "none",
|
|
|
|
|
maxWidth: "796px"
|
|
|
|
|
}}>
|
2025-06-08 17:52:55 +00:00
|
|
|
|
<GenderAndAgeSelector
|
|
|
|
|
gender={gender}
|
|
|
|
|
age={age}
|
|
|
|
|
ageError={ageError}
|
|
|
|
|
onGenderChange={setGender}
|
|
|
|
|
onAgeChange={setAge}
|
|
|
|
|
onAgeErrorChange={setAgeError}
|
|
|
|
|
onStartCreate={startCreate}
|
|
|
|
|
/>
|
2025-05-25 13:21:19 +00:00
|
|
|
|
|
2025-06-01 21:27:22 +00:00
|
|
|
|
{/* Ссылка */}
|
|
|
|
|
<Box sx={{ mt: "34px" }}>
|
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
|
|
|
<Typography sx={{ color: theme.palette.grey3.main, fontSize: "18px", fontWeight: 500 }}>Ссылка</Typography>
|
|
|
|
|
<InfoPopover />
|
|
|
|
|
</Box>
|
|
|
|
|
<Typography
|
|
|
|
|
sx={{
|
|
|
|
|
fontSize: "14px",
|
|
|
|
|
lineHeight: "100%",
|
|
|
|
|
letterSpacing: "0 %",
|
|
|
|
|
color: theme.palette.grey2.main,
|
|
|
|
|
mt: "16px"
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Вставьте ссылку со всеми utm-метками
|
|
|
|
|
</Typography>
|
2025-05-25 13:21:19 +00:00
|
|
|
|
<CustomTextField
|
|
|
|
|
placeholder="linkexample.com"
|
2025-06-01 22:48:35 +00:00
|
|
|
|
maxLength={500}
|
|
|
|
|
value={link}
|
|
|
|
|
onChange={handleLinkChange}
|
2025-05-25 13:21:19 +00:00
|
|
|
|
sxForm={{
|
2025-06-02 18:22:55 +00:00
|
|
|
|
maxWidth: "615px",
|
|
|
|
|
width: "100%",
|
2025-05-25 13:21:19 +00:00
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
|
2025-06-01 22:48:35 +00:00
|
|
|
|
<AuditoryList utmParams={utmParams} onDelete={setDeleteModal} auditory={auditory} />
|
2025-05-25 13:21:19 +00:00
|
|
|
|
|
2025-06-01 21:27:22 +00:00
|
|
|
|
</Container>
|
|
|
|
|
<Modal
|
|
|
|
|
open={Boolean(deleteModal)}
|
|
|
|
|
onClose={() => setDeleteModal(0)}
|
|
|
|
|
aria-labelledby="modal-modal-title"
|
|
|
|
|
aria-describedby="modal-modal-description"
|
|
|
|
|
>
|
|
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
position: "absolute" as "absolute",
|
|
|
|
|
top: "50%",
|
|
|
|
|
left: "50%",
|
|
|
|
|
transform: "translate(-50%, -50%)",
|
|
|
|
|
maxWidth: "620px",
|
|
|
|
|
width: "100%",
|
|
|
|
|
bgcolor: "background.paper",
|
|
|
|
|
borderRadius: "12px",
|
|
|
|
|
|
|
|
|
|
boxShadow: 24,
|
|
|
|
|
p: "20px",
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
alignItems: "center"
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-06-06 13:25:06 +00:00
|
|
|
|
<Typography sx={{ width: "100%", textAlign: "center", mb: "25px" }}>Уверены, что хотите удалить ссылку?</Typography>
|
|
|
|
|
<Button sx={{ mb: "20px" }} onClick={handleDelete}>Удалить</Button>
|
2025-06-01 21:27:22 +00:00
|
|
|
|
<Button variant="contained" onClick={() => setDeleteModal(0)} >Отмена</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
</Modal>
|
2025-06-08 17:52:55 +00:00
|
|
|
|
<PayModal
|
|
|
|
|
open={isModalOpen}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setIsModalOpen(false);
|
|
|
|
|
setStartCreate(false);
|
|
|
|
|
}}
|
|
|
|
|
onCreate={createNewLink}
|
|
|
|
|
/>
|
2025-06-01 21:27:22 +00:00
|
|
|
|
</>
|
2025-05-25 13:21:19 +00:00
|
|
|
|
);
|
|
|
|
|
}
|