import { UpkQuestionChoice } from "@/api_fsb"; import { ChevronRightIcon } from "lucide-react"; import { KeyboardEvent, MouseEvent, useEffect, useRef, useState } from "react"; import { useAppDispatch, useAppSelector } 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 bpuid = useAppSelector(state => state.app.bpuid) 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 (!bpuid) return; if (event.key === 'Enter') { dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) } }; 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 bpuid = useAppSelector(state => state.app.bpuid) const keyboardSelectionIdxRef = useRef(0); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!activeQuestion) return; if (!bpuid) 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: bpuid })) } 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 bpuid = useAppSelector(state => state.app.bpuid) const handleOnClick = () => { if (!activeQuestion) return if (!activeQuestionValidation?.isComplete) return if (!bpuid) return; dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) } 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 bpuid = useAppSelector(state => state.app.bpuid) useEffect(() => { if (!bpuid) return; dispatch(fetchNewQuestions(bpuid)); }, [bpid, bpuid]) if (!bpuid) return null; return (
setShowProfiling(!showProfiling)} >

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

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