diff --git a/lib/api/hooks.ts b/lib/api/hooks.ts index 2ccbbe6..13a5f03 100644 --- a/lib/api/hooks.ts +++ b/lib/api/hooks.ts @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { getQuizData, getFirstQuizData } from "./quizRelase"; +import { getAndParceData } from "./quizRelase"; import { useEffect, useState } from "react"; import { initDataManager, statusOfQuiz } from "@/utils/hooks/useQuestionFlowControl"; import { addQuestions, setQuizData, useQuizStore } from "@/stores/useQuizStore"; @@ -11,7 +11,7 @@ import { addQuestions, setQuizData, useQuizStore } from "@/stores/useQuizStore"; */ export function useQuizData(quizId: string, preview: boolean) { - const { quizStep } = useQuizStore(); + const { quizStep, questions } = useQuizStore(); const [page, setPage] = useState(0); const [needFullLoad, setNeedFullLoad] = useState(false); @@ -25,7 +25,7 @@ export function useQuizData(quizId: string, preview: boolean) { async ([, id, currentPage, fullLoad]) => { // Первый запрос - получаем статус if (currentPage === 0 && !fullLoad) { - const firstData = await getQuizData({ + const firstData = await getAndParceData({ quizId: id, limit: 1, page: currentPage, @@ -39,7 +39,7 @@ export function useQuizData(quizId: string, preview: boolean) { setQuizData(firstData); // Определяем нужно ли загружать все данные - if (["line", "branch"].includes(firstData.status)) { + if (["line", "branch"].includes(firstData.settings.status)) { setNeedFullLoad(true); // Триггерит новый запрос через изменение ключа return firstData; } @@ -48,25 +48,32 @@ export function useQuizData(quizId: string, preview: boolean) { // Полная загрузка для line/branch if (fullLoad) { - const allQuestions = await getQuestionsData({ + const data = await getAndParceData({ quizId: id, limit: 100, page: 0, needConfig: false, }); - addQuestions(allQuestions); - return allQuestions; + addQuestions(data.questions.slice(1)); + return data; } - // Для AI режима - последовательная загрузка - const allQuestions = await getQuestionsData({ - quizId: id, - page: currentPage, - limit: 1, - needConfig: false, - }); - return allQuestions; + if (currentPage > questions.length) { + try { + // Для AI режима - последовательная загрузка + const data = await getAndParceData({ + quizId: id, + page: currentPage, + limit: 1, + needConfig: false, + }); + addQuestions(data.questions); + return data; + } catch (_) { + setPage(questions.length); + } + } }, { revalidateOnFocus: false, diff --git a/lib/api/useQuizGetNext.ts b/lib/api/useQuizGetNext.ts deleted file mode 100644 index 6fa96ff..0000000 --- a/lib/api/useQuizGetNext.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useState } from "react"; -import { getQuizDataAI } from "./quizRelase"; -import { addQuestion, useQuizStore } from "@/stores/useQuizStore"; -import { AnyTypedQuizQuestion } from ".."; - -function qparse(q: { desc: string; id: string; req: boolean; title: string; typ: string }) { - return { - description: q.desc, - id: q.id, - required: q.req, - title: q.title, - type: q.typ, - page: 0, - content: { - answerType: "single", - autofill: false, - back: "", - hint: { text: "", video: "" }, - id: "", - innerName: "", - innerNameCheck: false, - onlyNumbers: false, - originalBack: "", - placeholder: "", - required: false, - rule: { - children: [], - default: "", - main: [], - parentId: "", - }, - }, - }; -} - -export const useQuizGetNext = () => { - const { quizId, settings } = useQuizStore(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [currentPage, setCurrentPage] = useState(1); - - const loadMoreQuestions = async () => { - console.log("STATUS loadMoreQuestions"); - console.log(settings); - console.log(settings.status); - if (settings.status === "ai") { - console.log("STATUS after IF"); - setIsLoading(true); - setError(null); - - try { - console.log("STATUS after TRY TRY TRY"); - const data = await getQuizDataAI(quizId); - console.log("data"); - console.log(data); - const newQuestion = qparse(data[0]); - console.log("newQuestion"); - console.log(newQuestion); - if (newQuestion) { - newQuestion.page = currentPage; - //@ts-ignore - addQuestion(newQuestion as AnyTypedQuizQuestion); - setCurrentPage((old) => old++); - console.log("newQuestion + page"); - console.log(newQuestion); - return newQuestion; - } - } catch (err) { - setError(err as Error); - } finally { - setIsLoading(false); - } - } - }; - - return { loadMoreQuestions, isLoading, error, currentPage }; -}; diff --git a/lib/stores/useQuizStore.ts b/lib/stores/useQuizStore.ts index 1228c70..4a98738 100644 --- a/lib/stores/useQuizStore.ts +++ b/lib/stores/useQuizStore.ts @@ -42,6 +42,7 @@ export const addquizid = (id: string) => export const changeQuizStep = (step: number) => useQuizStore.setState( produce((state: QuizStore) => { - state.quizStep += step; + //Дополнительная проверка что мы не вышли на более чем +1 вопрос + if (state.questions.length - step < 2) state.quizStep += step; }) ); diff --git a/lib/utils/hooks/FlowControlLogic/useAIQuiz.ts b/lib/utils/hooks/FlowControlLogic/useAIQuiz.ts index e69de29..9a2539e 100644 --- a/lib/utils/hooks/FlowControlLogic/useAIQuiz.ts +++ b/lib/utils/hooks/FlowControlLogic/useAIQuiz.ts @@ -0,0 +1,271 @@ +import { useCallback, useDebugValue, useEffect, useMemo, useState } from "react"; +import { enqueueSnackbar } from "notistack"; +import moment from "moment"; + +import { isResultQuestionEmpty } from "@/components/ViewPublicationPage/tools/checkEmptyData"; +import { useQuizStore } from "@/stores/useQuizStore"; + +import { useQuizViewStore } from "@stores/quizView"; + +import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals"; +import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals"; + +export function useAIQuiz() { + //Получаем инфо о квизе и список вопросов. + const { settings, questions, quizId, cnt } = useQuizStore(); + + useEffect(() => { + console.log("useQuestionFlowControl useEffect"); + console.log(questions); + }, [questions]); + console.log(questions); + + //Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page. + //За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page + const sortedQuestions = useMemo(() => { + return [...questions].sort((a, b) => a.page - b.page); + }, [questions]); + //React сам будет менять визуал - главное говорить из какого вопроса ему брать инфо. Изменение этой переменной меняет визуал. + const [currentQuestionId, setCurrentQuestionId] = useState(getFirstQuestionId); + const [headAI, setHeadAI] = useState(0); + //Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах + const answers = useQuizViewStore((state) => state.answers); + //Список засчитанных баллов для балловых квизов + const pointsSum = useQuizViewStore((state) => state.pointsSum); + //Текущий шаг "startpage" | "question" | "contactform" + const setCurrentQuizStep = useQuizViewStore((state) => state.setCurrentQuizStep); + //Получение возможности управлять состоянием метрик + const vkMetrics = useVkMetricsGoals(settings.cfg.vkMetricsNumber); + const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber); + + //Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке + const currentQuestion = sortedQuestions.find((question) => question.id === currentQuestionId) ?? sortedQuestions[0]; + + //Индекс текущего вопроса только если квиз линейный + const linearQuestionIndex = //: number | null + currentQuestion && sortedQuestions.every(({ content }) => content.rule.parentId !== "root") // null when branching enabled + ? sortedQuestions.indexOf(currentQuestion) + : null; + + //Индекс первого вопроса + function getFirstQuestionId() { + //: string | null + if (sortedQuestions.length === 0) return null; //Если нету сортированного списка, то и не рыпаемся + + if (settings.cfg.haveRoot) { + // Если есть ветвление, то settings.cfg.haveRoot будет заполнен + //Если заполнен, то дерево растёт с root и это 1 вопрос :) + const nextQuestion = sortedQuestions.find( + //Функция ищет первое совпадение по массиву + (question) => question.id === settings.cfg.haveRoot || question.content.id === settings.cfg.haveRoot + ); + if (!nextQuestion) return null; + + return nextQuestion.id; + } + + //Если не возникло исключительных ситуаций - первый вопрос - нулевой элемент сортированного массива + return sortedQuestions[0].id; + } + + const nextQuestionIdPointsLogic = useCallback(() => { + return sortedQuestions.find((question) => question.type === "result" && question.content.rule.parentId === "line"); + }, [sortedQuestions]); + + //Анализируем какой вопрос должен быть следующим. Это главная логика + const nextQuestionIdMainLogic = useCallback(() => { + //Список ответов данных этому вопросу. Вернёт QuestionAnswer | undefined + const questionAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id); + + //Если questionAnswer не undefined и ответ на вопрос не является временем: + if (questionAnswer && !moment.isMoment(questionAnswer.answer)) { + //Вопрос типизации. Получаем список строк ответов на этот вопрос + const userAnswers = Array.isArray(questionAnswer.answer) ? questionAnswer.answer : [questionAnswer.answer]; + + //цикл. Перебираем список условий .main и обзываем их переменной branchingRule + for (const branchingRule of currentQuestion.content.rule.main) { + // Перебираем список ответов. Если хоть один ответ из списка совпадает с прописанным правилом из условий - этот вопрос нужный нам. Его и дадимкак следующий + if (userAnswers.some((answer) => branchingRule.rules[0].answers.includes(answer))) { + return branchingRule.next; + } + } + } + + //Не помню что это, но чёт при первом взгляде оно true только у результатов + if (!currentQuestion.required) { + //Готовим себе дефолтный путь + const defaultNextQuestionId = currentQuestion.content.rule.default; + + //Если строка не пустая и не пробел. (Обычно при получении данных мы сразу чистим пустые строки только с пробелом на просто пустые строки. Это прост доп защита) + if (defaultNextQuestionId.length > 1 && defaultNextQuestionId !== " ") return defaultNextQuestionId; + //Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт + //Кинуть на ребёнка надо даже если там нет дефолта + if ( + ["date", "page", "text", "number"].includes(currentQuestion.type) && + currentQuestion.content.rule.children.length === 1 + ) + return currentQuestion.content.rule.children[0]; + } + + //ничё не нашли, ищем резулт + return sortedQuestions.find((q) => { + return q.type === "result" && q.content.rule.parentId === currentQuestion.content.id; + })?.id; + }, [answers, currentQuestion, sortedQuestions]); + + //Анализ следующего вопроса. Это логика для вопроса с баллами + const nextQuestionId = useMemo(() => { + if (settings.cfg.score) { + return nextQuestionIdPointsLogic(); + } + return nextQuestionIdMainLogic(); + }, [nextQuestionIdMainLogic, nextQuestionIdPointsLogic, settings.cfg.score, questions]); + + //Поиск предыдущго вопроса либо по индексу либо по id родителя + const prevQuestion = + linearQuestionIndex !== null + ? sortedQuestions[linearQuestionIndex - 1] + : sortedQuestions.find( + (q) => + q.id === currentQuestion?.content.rule.parentId || q.content.id === currentQuestion?.content.rule.parentId + ); + + //Анализ результата по количеству баллов + const findResultPointsLogic = useCallback(() => { + //Отбираем из массива только тип резулт И результы с информацией о ожидаемых баллах И те результы, чьи суммы баллов меньше или равны насчитанным баллам юзера + + const results = sortedQuestions.filter( + (e) => e.type === "result" && e.content.rule.minScore !== undefined && e.content.rule.minScore <= pointsSum + ); + //Создаём массив строк из результатов. У кого есть инфо о баллах - дают свои, остальные 0 + const numbers = results.map((e) => + e.type === "result" && e.content.rule.minScore !== undefined ? e.content.rule.minScore : 0 + ); + //Извлекаем самое большое число + const indexOfNext = Math.max(...numbers); + //Отдаём индекс нужного нам результата + return results[numbers.indexOf(indexOfNext)]; + }, [pointsSum, sortedQuestions]); + + //Ищем следующий вопрос (не его индекс, или id). Сам вопрос + const nextQuestion = useMemo(() => { + let next; + + if (settings.cfg.score) { + //Ессли квиз балловый + if (linearQuestionIndex !== null) { + next = sortedQuestions[linearQuestionIndex + 1]; //ищем по индексу + if (next?.type === "result" || next == undefined) next = findResultPointsLogic(); //если в поисках пришли к результату - считаем нужный + } + } else { + //иначе + if (linearQuestionIndex !== null) { + //для линейных ищем по индексу + next = + sortedQuestions[linearQuestionIndex + 1] ?? + sortedQuestions.find((question) => question.type === "result" && question.content.rule.parentId === "line"); + } else { + // для нелинейных ищем по вычесленному id + next = sortedQuestions.find((q) => q.id === nextQuestionId || q.content.id === nextQuestionId); + } + } + + return next; + }, [nextQuestionId, findResultPointsLogic, linearQuestionIndex, sortedQuestions, settings.cfg.score]); + + //Показать визуалом юзеру результат + const showResult = useCallback(() => { + if (nextQuestion?.type !== "result") throw new Error("Current question is not result"); + + //Записать в переменную ид текущего вопроса + setCurrentQuestionId(nextQuestion.id); + //Смотрим по настройкам показывать ли вообще форму контактов. Показывать ли страницу результатов до или после формы контактов (ФК) + if ( + settings.cfg.showfc !== false && + (settings.cfg.resultInfo.showResultForm === "after" || isResultQuestionEmpty(nextQuestion)) + ) + setCurrentQuizStep("contactform"); + }, [nextQuestion, setCurrentQuizStep, settings.cfg.resultInfo.showResultForm, settings.cfg.showfc]); + + //рычаг управления из визуала в эту функцию + const showResultAfterContactForm = useCallback(() => { + if (currentQuestion?.type !== "result") throw new Error("Current question is not result"); + if (isResultQuestionEmpty(currentQuestion)) { + enqueueSnackbar("Данные отправлены"); + return; + } + + setCurrentQuizStep("question"); + }, [currentQuestion, setCurrentQuizStep]); + + //рычаг управления из визуала в эту функцию + const moveToPrevQuestion = useCallback(() => { + if (!prevQuestion) throw new Error("Previous question not found"); + + if (settings.status === "ai" && headAI > 0) setHeadAI((old) => old--); + setCurrentQuestionId(prevQuestion.id); + }, [prevQuestion]); + + //рычаг управления из визуала в эту функцию + const moveToNextQuestion = useCallback(async () => { + // Если есть следующий вопрос в уже загруженных - используем его + + if (nextQuestion) { + vkMetrics.questionPassed(currentQuestion.id); + yandexMetrics.questionPassed(currentQuestion.id); + + if (nextQuestion.type === "result") return showResult(); + setCurrentQuestionId(nextQuestion.id); + return; + } + }, [currentQuestion.id, nextQuestion, showResult, vkMetrics, yandexMetrics, linearQuestionIndex, questions]); + + //рычаг управления из визуала в эту функцию + const setQuestion = useCallback( + (questionId: string) => { + const question = sortedQuestions.find((q) => q.id === questionId); + if (!question) return; + + setCurrentQuestionId(question.id); + }, + [sortedQuestions] + ); + + //Анализ дисаблить ли кнопки навигации + const isPreviousButtonEnabled = Boolean(prevQuestion); + + //Анализ дисаблить ли кнопки навигации + const isNextButtonEnabled = useMemo(() => { + const hasAnswer = answers.some(({ questionId }) => questionId === currentQuestion.id); + + if ("required" in currentQuestion.content && currentQuestion.content.required) { + return hasAnswer; + } + + console.log(linearQuestionIndex); + console.log(questions.length); + console.log(cnt); + if (linearQuestionIndex !== null && questions.length < cnt) return true; + return Boolean(nextQuestion); + }, [answers, currentQuestion, nextQuestion]); + + useDebugValue({ + linearQuestionIndex, + currentQuestion: currentQuestion, + prevQuestion: prevQuestion, + nextQuestion: nextQuestion, + }); + + return { + currentQuestion, + currentQuestionStepNumber: + settings.status === "ai" ? null : linearQuestionIndex === null ? null : linearQuestionIndex + 1, + nextQuestion, + isNextButtonEnabled, + isPreviousButtonEnabled, + moveToPrevQuestion, + moveToNextQuestion, + showResultAfterContactForm, + setQuestion, + }; +} diff --git a/lib/utils/hooks/FlowControlLogic/useBranchingQuiz.ts b/lib/utils/hooks/FlowControlLogic/useBranchingQuiz.ts index d172644..d7388fd 100644 --- a/lib/utils/hooks/FlowControlLogic/useBranchingQuiz.ts +++ b/lib/utils/hooks/FlowControlLogic/useBranchingQuiz.ts @@ -9,13 +9,9 @@ import { useQuizViewStore } from "@stores/quizView"; import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals"; import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals"; -import { useQuizGetNext } from "@/api/useQuizGetNext"; - -let isgetting = false; export function useBranchingQuiz() { //Получаем инфо о квизе и список вопросов. - const { loadMoreQuestions } = useQuizGetNext(); const { settings, questions, quizId, cnt } = useQuizStore(); useEffect(() => { @@ -45,11 +41,6 @@ export function useBranchingQuiz() { //Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке const currentQuestion = sortedQuestions.find((question) => question.id === currentQuestionId) ?? sortedQuestions[0]; - console.log("currentQuestion"); - console.log(currentQuestion); - console.log("filted"); - console.log(sortedQuestions.find((question) => question.id === currentQuestionId)); - //Индекс текущего вопроса только если квиз линейный const linearQuestionIndex = //: number | null currentQuestion && sortedQuestions.every(({ content }) => content.rule.parentId !== "root") // null when branching enabled @@ -227,35 +218,7 @@ export function useBranchingQuiz() { setCurrentQuestionId(nextQuestion.id); return; } - - // Если следующего нет - загружаем новый - try { - const newQuestion = await loadMoreQuestions(); - console.log("Ффункция некст вопрос получила его с бека: "); - console.log(newQuestion); - if (newQuestion) { - vkMetrics.questionPassed(currentQuestion.id); - yandexMetrics.questionPassed(currentQuestion.id); - console.log("МЫ ПАЛУЧИЛИ НОВЫЙ ВОПРОС"); - console.log(newQuestion); - console.log("typeof newQuestion.id"); - console.log(typeof newQuestion.id); - setCurrentQuestionId(newQuestion.id); - setHeadAI((old) => old++); - } - } catch (error) { - enqueueSnackbar("Ошибка загрузки следующего вопроса"); - } - }, [ - currentQuestion.id, - nextQuestion, - showResult, - vkMetrics, - yandexMetrics, - linearQuestionIndex, - loadMoreQuestions, - questions, - ]); + }, [currentQuestion.id, nextQuestion, showResult, vkMetrics, yandexMetrics, linearQuestionIndex, questions]); //рычаг управления из визуала в эту функцию const setQuestion = useCallback( diff --git a/lib/utils/hooks/FlowControlLogic/useLinearQuiz.ts b/lib/utils/hooks/FlowControlLogic/useLinearQuiz.ts index 529d525..f0e1636 100644 --- a/lib/utils/hooks/FlowControlLogic/useLinearQuiz.ts +++ b/lib/utils/hooks/FlowControlLogic/useLinearQuiz.ts @@ -9,11 +9,9 @@ import { useQuizViewStore } from "@stores/quizView"; import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals"; import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals"; -import { useQuizGetNext } from "@/api/useQuizGetNext"; export function useLinearQuiz() { //Получаем инфо о квизе и список вопросов. - const { loadMoreQuestions } = useQuizGetNext(); const { settings, questions, quizId, cnt } = useQuizStore(); useEffect(() => { @@ -43,11 +41,6 @@ export function useLinearQuiz() { //Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке const currentQuestion = sortedQuestions.find((question) => question.id === currentQuestionId) ?? sortedQuestions[0]; - console.log("currentQuestion"); - console.log(currentQuestion); - console.log("filted"); - console.log(sortedQuestions.find((question) => question.id === currentQuestionId)); - //Индекс текущего вопроса только если квиз линейный const linearQuestionIndex = //: number | null currentQuestion && sortedQuestions.every(({ content }) => content.rule.parentId !== "root") // null when branching enabled @@ -225,35 +218,7 @@ export function useLinearQuiz() { setCurrentQuestionId(nextQuestion.id); return; } - - // Если следующего нет - загружаем новый - try { - const newQuestion = await loadMoreQuestions(); - console.log("Ффункция некст вопрос получила его с бека: "); - console.log(newQuestion); - if (newQuestion) { - vkMetrics.questionPassed(currentQuestion.id); - yandexMetrics.questionPassed(currentQuestion.id); - console.log("МЫ ПАЛУЧИЛИ НОВЫЙ ВОПРОС"); - console.log(newQuestion); - console.log("typeof newQuestion.id"); - console.log(typeof newQuestion.id); - setCurrentQuestionId(newQuestion.id); - setHeadAI((old) => old++); - } - } catch (error) { - enqueueSnackbar("Ошибка загрузки следующего вопроса"); - } - }, [ - currentQuestion.id, - nextQuestion, - showResult, - vkMetrics, - yandexMetrics, - linearQuestionIndex, - loadMoreQuestions, - questions, - ]); + }, [currentQuestion.id, nextQuestion, showResult, vkMetrics, yandexMetrics, linearQuestionIndex, questions]); //рычаг управления из визуала в эту функцию const setQuestion = useCallback(