import { UpkQuestionChoice } from "@/api_fsb"; import { useGetMeQuery } from "@/models/authAPI"; import { ChevronRightIcon } from "lucide-react"; import { KeyboardEvent, MouseEvent, useEffect, useRef, useState } from "react"; import { useAppDispatch } from "@/hooks"; import { bpid } from "@/lib/utils"; import { createColumnHelper, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, RowModel, RowSelectionState, useReactTable, } from "@tanstack/react-table"; import { clsx } from "clsx"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, } from "@/components/ui/pagination"; import { addAnswers, fetchNewQuestions, goToNextQuestion, ProfileQuestion, questionsSelectors, saveAnswer, selectActiveQuestion, selectActiveQuestionValidation, selectAllQuestions, setActiveQuestion, } from "@/models/profilingQuestionsSlice"; import { useSelector } from "react-redux"; // import {motion} from "framer-motion" const TextEntry = () => { const dispatch = useAppDispatch(); const activeQuestion = useSelector(selectActiveQuestion); // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); // const selectAnswer = useSelector(selectAnswerForQuestion(question)); const metadata = activeQuestion?._metadata; const validation = activeQuestion?._validation; const { data: user } = useGetMeQuery(); if (!metadata) return null; if (!validation) return null; const error = validation?.errors.length > 0; const handleInputChange = (event: React.KeyboardEvent) => { if (!activeQuestion.question_id) return; const target = event.target as HTMLInputElement; dispatch( addAnswers({ questionId: activeQuestion.question_id!, answers: [target.value] as string[], }), ); }; const handleKeyPress = (event: KeyboardEvent) => { if (!activeQuestion.question_id) return; if (!user?.product_user_id) return; if (event.key === "Enter") { dispatch( saveAnswer({ questionId: activeQuestion.question_id!, bpuid: user.product_user_id, }), ); } }; return (
{validation.errors.map((e) => { return

{e.message}

; })}
); }; export default function useKeyboardMode(rowModel: RowModel) { // const [keyboardEnabled, setKeyboardEnabled] = useState( // localStorage.getItem(keyboardMode) === "enabled", // ); const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]); const dispatch = useAppDispatch(); const activeQuestion = useSelector(selectActiveQuestion); const { data: user } = useGetMeQuery(); const keyboardSelectionIdxRef = useRef(0); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!activeQuestion) return; if (!user?.product_user_id) return; // if (!keyboardEnabled) { // return; // } const { key } = event; const totalRows = rowRefs.current.length; const prevIdx = keyboardSelectionIdxRef.current; let nextIdx = prevIdx; if (key === "ArrowDown") nextIdx = Math.min(prevIdx + 1, totalRows - 1); if (key === "ArrowUp") nextIdx = Math.max(prevIdx - 1, 0); if (key === " ") { const active = document.activeElement as HTMLElement | null; if (active?.tabIndex === 0) { return; } else { rowModel.rows[keyboardSelectionIdxRef.current].toggleSelected(); } } if (event.key === "Enter") { dispatch( saveAnswer({ questionId: activeQuestion.question_id!, bpuid: user.product_user_id, }), ); } if (nextIdx !== prevIdx) { rowRefs.current[prevIdx]?.classList.remove("bg-blue-400"); rowRefs.current[nextIdx]?.classList.add("bg-blue-400"); rowRefs.current[nextIdx]?.scrollIntoView({ behavior: "smooth", block: "center", }); keyboardSelectionIdxRef.current = nextIdx; } }; // I really don't know, and I don't really care enough to make this // technically type'd correctly. - Max // @ts-ignore window.addEventListener("keydown", handleKeyDown); // @ts-ignore return () => window.removeEventListener("keydown", handleKeyDown); }, [rowModel.rows]); return { // keyboardEnabled, // setKeyboardEnabled, rowRefs, keyboardSelectionIdxRef, }; } const MultipleChoice = () => { const dispatch = useAppDispatch(); const activeQuestion = useSelector(selectActiveQuestion); // const error: Boolean = answer?.error_msg.length > 0 // console.log("Rendering MultipleChoice with answer:", answer, error) // TODO! Retrieve store values to use as the initial state for the table // This is useful for if they tab around before submitting the answer const [rowSelection, setRowSelection] = useState({}); const [globalFilter, setGlobalFilter] = useState(""); const handleRowSelectionChange = ( updaterOrValue: | RowSelectionState | ((old: RowSelectionState) => RowSelectionState), ) => { setRowSelection((oldSelection) => { // Get the new selection state const newSelection = typeof updaterOrValue === "function" ? updaterOrValue(oldSelection) : updaterOrValue; // Find which row was just selected/deselected const changedRowId = Object.keys(newSelection).find( (key) => newSelection[key] !== oldSelection[key], ); if (!changedRowId) return newSelection; // Get the actual row data const row = table.getRow(changedRowId); const rowData = row.original; // Your row data object // Check if "None of the Above" was selected const normalized = rowData.choice_text.trim().toLowerCase(); const isNoneOfTheAbove = normalized === "none of the above" || (rowData.exclusive ?? false); if (isNoneOfTheAbove && newSelection[changedRowId]) { // Clear all other selections, keep only this one return { [changedRowId]: true }; } // Check if a regular option was selected while "None of the Above" exists const noneOfTheAboveId = Object.keys(newSelection).find((id) => { const r = table.getRow(id); const rowData = r.original as UpkQuestionChoice; const normalized = rowData.choice_text.trim().toLowerCase(); const isNoneOfTheAbove = normalized === "none of the above" || (rowData.exclusive ?? false); return isNoneOfTheAbove && newSelection[id]; }); if (noneOfTheAboveId && changedRowId !== noneOfTheAboveId) { // Remove "None of the Above" selection const { [noneOfTheAboveId]: _, ...rest } = newSelection; return rest; } return newSelection; }); }; useEffect(() => { if (!activeQuestion?.question_id) return; // Run this anytime a rowSelection changes, to update // the answer in the store const selectedChoices = Object.keys(rowSelection); dispatch( addAnswers({ questionId: activeQuestion.question_id, answers: selectedChoices, }), ); }, [rowSelection]); // Define columns const columnHelper = createColumnHelper(); const columns = [ columnHelper.display({ id: "select", cell: ({ row }) => { const canSelected = row.getCanSelect(); return ( ); }, size: 20, meta: { align: "center", }, }), columnHelper.accessor("choice_text", { header: () => "Choice Text", cell: (info) => { const text = info.getValue(); const selected = info.row.getIsSelected(); const canSelected = info.row.getCanSelect(); return (

{text}

); }, size: 380, meta: { align: "left", }, }), ]; const table = useReactTable({ data: activeQuestion!.choices ?? [], columns, getRowId: (row) => row.choice_id, state: { rowSelection, globalFilter, }, // enableRowSelection: true, enableRowSelection: (row) => { // Allow deselection always, but limit new selections const currentSelectionCount = Object.keys(rowSelection).length; const isRowSelected = rowSelection[row.id]; const max_selection = activeQuestion?.selector === "SA" ? 1 : (activeQuestion?.choices?.length ?? 10); // Allow if: row is already selected OR we haven't hit the limit return isRowSelected || currentSelectionCount < max_selection; }, onRowSelectionChange: handleRowSelectionChange, onGlobalFilterChange: setGlobalFilter, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), }); const selectedCount = table.getSelectedRowModel().rows.length; //const { rowRefs, setKeyboardEnabled, keyboardSelectionIdxRef } = useKeyboardMode(table.getRowModel()); const { rowRefs } = useKeyboardMode(table.getRowModel()); return ( <> {/* Controls */}
{/* Search */}
setGlobalFilter(e.target.value)} className="w-full px-4 py-1 border border-blue-900 bg-blue-400 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" />
{/* Selected Count */} {selectedCount > 0 && (
setRowSelection({})} > {selectedCount} selected
)}
{/* Table */}
{table.getRowModel().rows.map((row, idx) => ( { rowRefs.current[idx] = el; }} onClick={() => row.toggleSelected()} className={`cursor-pointer transition-colors w-full ${ row.getIsSelected() ? "bg-blue-600 " : " " }`} > {row.getVisibleCells().map((cell) => { const align = cell.column.columnDef.meta?.align || "left"; const alignClass = align === "center" ? "text-center" : align === "right" ? "text-right" : "text-left"; return ( ); })} ))}
{flexRender( cell.column.columnDef.cell, cell.getContext(), )}
{/* Empty State */} {table.getRowModel().rows.length === 0 && (
No items found matching your search
)} {/* Footer */}
Showing {table.getRowModel().rows.length} of{" "} {activeQuestion?.choices?.length} choices {selectedCount > 0 && ( {selectedCount} choice{selectedCount !== 1 ? "s" : ""} selected )}
); }; const ProfilingQuestionTitle = () => { const activeQuestion = useSelector(selectActiveQuestion); const question_active_tasks = activeQuestion?.importance?.task_count ?? 0; if (!activeQuestion) return null; let text = activeQuestion?.question_text; if ( activeQuestion.explanation_template && activeQuestion._validation?.isComplete && activeQuestion._metadata.answers.length == 1 ) { const template = activeQuestion.explanation_template; const pre_code = activeQuestion._metadata.answers[0]; const choice = activeQuestion.choices?.find( (c) => c.choice_id === pre_code, ); const choice_text = choice ? choice.choice_text : "your answer"; text = template.replace("{answer}", choice_text); } return (

{text}

{question_active_tasks.toLocaleString()}

Surveys use this question
); }; const ProfilingQuestionSubmitButton = () => { const dispatch = useAppDispatch(); const activeQuestion = useSelector(selectActiveQuestion); const activeQuestionValidation = useSelector(selectActiveQuestionValidation); const { data: user } = useGetMeQuery(); const handleOnClick = () => { if (!activeQuestion) return; if (!activeQuestionValidation?.isComplete) return; if (!user?.product_user_id) return; dispatch( saveAnswer({ questionId: activeQuestion.question_id!, bpuid: user.product_user_id, }), ); }; return ( ); }; export const ProfileQuestionFull = () => { const activeQuestion = useSelector(selectActiveQuestion); const renderContent = () => { if (!activeQuestion) return null; switch (activeQuestion.question_type) { case "TE": return ; case "MC": return ; default: return ; } }; if (!activeQuestion) return null; return ( <> {/* {answer && answer.processing && ( setLoading(false)} /> )} */} {renderContent()} ); }; const PaginationIcon: React.FC<{ question: ProfileQuestion; idx: number; }> = ({ question, idx }) => { const dispatch = useAppDispatch(); // const answers = useAppSelector(state => state.answers) const isActive = question._metadata?.isActive ?? false; const completed = question._validation?.isComplete ?? false; const handleSetQuestion = (evt: MouseEvent) => { if (completed) { evt.preventDefault(); } else { dispatch(setActiveQuestion(question.question_id!)); } }; return ( {idx + 1} ); }; const ProfilingQuestionPagination = () => { const dispatch = useAppDispatch(); const questions = useSelector(selectAllQuestions); const activeQuestion = useSelector(selectActiveQuestion); // All the variables needed for a sliding window const q_idx = questions.findIndex( (q) => q.question_id === activeQuestion!.question_id, ); const handleNextClick = () => { dispatch(goToNextQuestion()); }; const questionsWindow = ( items: ProfileQuestion[], currentIndex: number, windowSize: number = 5, ): ProfileQuestion[] => { const half: number = Math.floor(windowSize / 2); const total: number = items.length; let start: number = currentIndex - half; let end: number = currentIndex + half + 1; if (start < 0) { end += Math.abs(start); start = 0; } // Adjust if window goes past the end if (end > total) { const overflow: number = end - total; start = Math.max(0, start - overflow); end = total; } return items.slice(start, end); }; return ( {questionsWindow(questions, q_idx).map((q) => { return ( qq.question_id === q.question_id, )} /> ); })} Next ); }; const ProfilingHeader = () => { const questions = useSelector(questionsSelectors.selectAll); return (

{questions.length} Profiling Questions

); }; const ProfilingModalBody = () => { const activeQuestion = useSelector(selectActiveQuestion); if (!activeQuestion) { return (

No Profiling Questions Available

Please check back later.

); } return (
{/* */}
{/* Scrollable Content */}
{/* Sticky Footer */}
); }; const Profiling = () => { /* This is the main UI for any User Profiling (before entering a task). It's primary window and position should be dedictated by it's parent so this can be positioned as a window or a sidebar as needed. */ const [showProfiling, setShowProfiling] = useState(false); const dispatch = useAppDispatch(); const { data: user } = useGetMeQuery(); useEffect(() => { if (!user?.product_user_id) return; dispatch(fetchNewQuestions(user.product_user_id)); }, [bpid, user?.product_user_id]); if (!user?.product_user_id) return null; return (
setShowProfiling(!showProfiling)} >

(Click to {showProfiling ? "hide" : "show"})

{showProfiling && }
); }; export { Profiling };