/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { cn } from '@/lib/utils' type RequiredTextPart = { type: 'input' | 'static' text: string placeholder?: string } type NormalizedRequiredTextPart = RequiredTextPart & { inputIndex?: number } type RiskAcknowledgementDialogProps = { open: boolean onOpenChange: (open: boolean) => void title: string description?: React.ReactNode items?: string[] checklist?: string[] requiredText?: string requiredTextParts?: RequiredTextPart[] inputPrompt?: string inputPlaceholder?: string mismatchHint?: string confirmText?: string cancelText?: string destructive?: boolean isLoading?: boolean onConfirm: () => void className?: string } function getRequiredTextRows(text: string) { return Math.max(1, Math.ceil(Array.from(text).length / 42)) } export function RiskAcknowledgementDialog({ open, onOpenChange, title, description, items = [], checklist = [], requiredText = '', requiredTextParts = [], inputPrompt, inputPlaceholder, mismatchHint, confirmText, cancelText, destructive = true, isLoading = false, onConfirm, className, }: RiskAcknowledgementDialogProps) { const { t } = useTranslation() const [checkedItems, setCheckedItems] = useState([]) const [typedText, setTypedText] = useState('') const [typedTextParts, setTypedTextParts] = useState([]) const normalizedRequiredTextParts = useMemo< NormalizedRequiredTextPart[] >(() => { return requiredTextParts.reduce<{ parts: NormalizedRequiredTextPart[] inputIndex: number }>( (acc, part) => { if (part.type !== 'input') { return { ...acc, parts: [...acc.parts, part] } } return { parts: [...acc.parts, { ...part, inputIndex: acc.inputIndex }], inputIndex: acc.inputIndex + 1, } }, { parts: [], inputIndex: 0 } ).parts }, [requiredTextParts]) const requiredTextInputCount = useMemo( () => normalizedRequiredTextParts.filter((part) => part.type === 'input') .length, [normalizedRequiredTextParts] ) const hasSegmentedRequiredText = requiredTextInputCount > 0 const requiredTextToDisplay = hasSegmentedRequiredText ? normalizedRequiredTextParts.map((part) => part.text).join('') : requiredText useEffect(() => { if (!open) return const timer = window.setTimeout(() => { setCheckedItems(Array(checklist.length).fill(false)) setTypedText('') setTypedTextParts(Array(requiredTextInputCount).fill('')) }, 0) return () => window.clearTimeout(timer) }, [open, checklist.length, requiredTextInputCount]) const allChecked = useMemo(() => { if (checklist.length === 0) return true return ( checkedItems.length === checklist.length && checkedItems.every((checked) => checked) ) }, [checkedItems, checklist.length]) const typedMatched = useMemo(() => { if (hasSegmentedRequiredText) { return normalizedRequiredTextParts.every((part) => { if (part.type === 'static') return true return typedTextParts[part.inputIndex ?? 0]?.trim() === part.text.trim() }) } if (!requiredText) return true return typedText.trim() === requiredText.trim() }, [ hasSegmentedRequiredText, normalizedRequiredTextParts, requiredText, typedText, typedTextParts, ]) const hasTypedRequiredText = hasSegmentedRequiredText ? typedTextParts.some((part) => part.trim() !== '') : typedText.length > 0 const canConfirm = allChecked && typedMatched && !isLoading const handleChecklistChange = (index: number, checked: boolean) => { setCheckedItems((previous) => { const next = [...previous] next[index] = checked return next }) } const handleTextPartChange = (index: number, value: string) => { setTypedTextParts((previous) => { const next = [...previous] next[index] = value return next }) } return ( {title} {description ? ( } className='mt-1 text-left leading-5' > {description} ) : null}
{items.length > 0 ? (
    {items.map((item) => (
  1. {item}
  2. ))}
) : null} {checklist.length > 0 ? (
{checklist.map((item, index) => { const id = `risk-acknowledgement-${index}` return (
handleChecklistChange(index, checked === true) } className='mt-0.5' />
) })}
) : null} {requiredTextToDisplay ? (
{requiredTextToDisplay}
{hasSegmentedRequiredText ? (
{normalizedRequiredTextParts.map((part, index) => part.type === 'static' ? ( {part.text} ) : (