import React, { useState } from 'react'; import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuShortcut, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Eye, EyeClosed } from "lucide-react" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { Button } from "@/components/ui/button" import React from "react" import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell, } from "@/components/ui/table" function parseTable(lines, startIndex) { const headerLine = lines[startIndex] const separatorLine = lines[startIndex + 1] if (!separatorLine || !separatorLine.match(/^\s*\|[-\s|]+\|\s*$/)) { return null } const headers = headerLine .split("|") .slice(1, -1) .map(h => h.trim()) const rows = [] let i = startIndex + 2 while (i < lines.length && lines[i].includes("|")) { const cells = lines[i] .split("|") .slice(1, -1) .map(c => c.trim()) rows.push(cells) i++ } return { table: { headers, rows }, nextIndex: i, } } export function Markdown({ children, dic, show }) { const [localMessage, setLocalMessage] = useState(children); const [open, setOpen] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); const [selectionData, setSelectionData] = useState(null); const lines = children.split("\n") const elements = [] let i = 0 const handleMouseUp = () => { const selection = window.getSelection(); // 1. Check basic validity if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { // If just a click (collapsed), we assume menu shouldn't open or should close // But we don't want to close immediately if clicking inside the menu, handled by DropdownMenu logic return; } // 2. Expand Selection to Word Boundaries const anchorNode = selection.anchorNode; const focusNode = selection.focusNode; // Only support single text node selection for simplicity to map back to data-index if (anchorNode !== focusNode || anchorNode.nodeType !== 3) { setOpen(false); return; } const textContent = anchorNode.textContent; let start = Math.min(selection.anchorOffset, selection.focusOffset); let end = Math.max(selection.anchorOffset, selection.focusOffset); // Expand left while (start > 0 && /\S/.test(textContent[start - 1])) { start--; } // Expand right while (end < textContent.length && /\S/.test(textContent[end])) { end++; } // Update visual selection const newRange = document.createRange(); newRange.setStart(anchorNode, start); newRange.setEnd(anchorNode, end); selection.removeAllRanges(); selection.addRange(newRange); const newText = textContent.slice(start, end); if (!newText.trim()) return; // 3. Validation Logic // Check 1: Less than 4 words const words = newText.split(/\s+/); if (words.length >= 4) { setOpen(false); return; } // Check 2: Does not contain highlighted word (brackets) if (newText.includes('[') || newText.includes(']')) { setOpen(false); return; } // Check 3: Check if selection is inside a Badge const parentElement = anchorNode.parentElement; if (parentElement.closest('.bg-pii')) { setOpen(false); return; } // Identify Part Index const partIndex = parseInt(parentElement.getAttribute('data-index'), 10); // if (isNaN(partIndex)) { // setOpen(false); // return; // } const rect = newRange.getBoundingClientRect(); setPosition({ x: rect.left, y: rect.bottom }); setSelectionData({ partIndex, start, end, text: newText }); setOpen(true); }; const handleAnonymize = () => { if (!selectionData) return; const { partIndex, start, end, text } = selectionData; // Split by brackets to reconstruct parts array as seen in render const parts = localMessage.split(/(\[[^\]]+\])/g); if (parts[partIndex] === undefined) return; const originalPart = parts[partIndex]; const newPart = originalPart.substring(0, start) + `[ANONYME_${text}]` + originalPart.substring(end); parts[partIndex] = newPart; const newMessage = parts.join(''); setLocalMessage(newMessage); setOpen(false); // Clear selection window.getSelection().removeAllRanges(); }; while (i < lines.length) { const line = lines[i] // HEADERS const headerMatch = line.match(/^(#{1,6})\s+(.*)$/) if (headerMatch) { const level = headerMatch[1].length const text = headerMatch[2] const Tag = `h${level}` elements.push( {renderInline(text, dic, show)} ) i++ continue } // TABLE if (line.trim().startsWith("|")) { const parsed = parseTable(lines, i) if (parsed) { const { headers, rows } = parsed.table elements.push( {headers.map((h, idx) => ( {renderInline(h, dic, show)} ))} {rows.map((row, rIdx) => ( {row.map((cell, cIdx) => ( {renderInline(cell, dic, show)} ))} ))}
) i = parsed.nextIndex continue } } // PARAGRAPH if (line.trim() !== "") { elements.push(

{renderInline(line, dic, show)}

) } i++ } return (

{elements}

Anonymiser
) } function parseInlineText(text) { const tokens = [] let i = 0 while (i < text.length) { // CODE ` if (text[i] === "`") { const end = text.indexOf("`", i + 1) if (end !== -1) { tokens.push({ type: "code", value: text.slice(i + 1, end) }) i = end + 1 continue } } // BOLD ** if (text.slice(i, i + 2) === "**") { const end = text.indexOf("**", i + 2) if (end !== -1) { tokens.push({ type: "bold", value: text.slice(i + 2, end) }) i = end + 2 continue } } // ITALIC * if (text[i] === "*") { const end = text.indexOf("*", i + 1) if (end !== -1) { tokens.push({ type: "italic", value: text.slice(i + 1, end) }) i = end + 1 continue } } // TEXT let j = i while ( j < text.length && text[j] !== "`" && text[j] !== "*" ) { j++ } tokens.push({ type: "text", value: text.slice(i, j) }) i = j } return tokens } function renderInline(text, dic, show) { const parts = text.split(/(\[[^\]]+\])/g) return parts.map((part, index) => { // PII const match = part.match(/^\[(\w+)_(.*?)\]$/) if (match && dic[match[2]]) { return show ? ( {dic[match[2]].value} ) : ( {dic[match[2]].value} {/* {/* {part.toUpperCase()} */} {/* {dic[match[2]].value} */} {/* */}

{dic[match[2]].value}

) } // TEXTE NORMAL → inline markdown const tokens = parseInlineText(part) return ( {tokens.map((t, i) => { if (t.type === "bold") { return {t.value} } if (t.type === "italic") { return {t.value} } if (t.type === "code") { return ( {t.value} ) } return {t.value} })} ) }) } const Badge = ({ children, className }) => ( {children} ); function MessageWithHighlight({ message, dic, show }) { if (!message) return null; const [localMessage, setLocalMessage] = useState(message); const [open, setOpen] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); const [selectionData, setSelectionData] = useState(null); const handleMouseUp = () => { const selection = window.getSelection(); // 1. Check basic validity if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { // If just a click (collapsed), we assume menu shouldn't open or should close // But we don't want to close immediately if clicking inside the menu, handled by DropdownMenu logic return; } // 2. Expand Selection to Word Boundaries const anchorNode = selection.anchorNode; const focusNode = selection.focusNode; // Only support single text node selection for simplicity to map back to data-index if (anchorNode !== focusNode || anchorNode.nodeType !== 3) { setOpen(false); return; } const textContent = anchorNode.textContent; let start = Math.min(selection.anchorOffset, selection.focusOffset); let end = Math.max(selection.anchorOffset, selection.focusOffset); // Expand left while (start > 0 && /\S/.test(textContent[start - 1])) { start--; } // Expand right while (end < textContent.length && /\S/.test(textContent[end])) { end++; } // Update visual selection const newRange = document.createRange(); newRange.setStart(anchorNode, start); newRange.setEnd(anchorNode, end); selection.removeAllRanges(); selection.addRange(newRange); const newText = textContent.slice(start, end); if (!newText.trim()) return; // 3. Validation Logic // Check 1: Less than 4 words const words = newText.split(/\s+/); if (words.length >= 4) { setOpen(false); return; } // Check 2: Does not contain highlighted word (brackets) if (newText.includes('[') || newText.includes(']')) { setOpen(false); return; } // Check 3: Check if selection is inside a Badge const parentElement = anchorNode.parentElement; if (parentElement.closest('.bg-pii')) { setOpen(false); return; } // Identify Part Index const partIndex = parseInt(parentElement.getAttribute('data-index'), 10); if (isNaN(partIndex)) { setOpen(false); return; } const rect = newRange.getBoundingClientRect(); setPosition({ x: rect.left, y: rect.bottom }); setSelectionData({ partIndex, start, end, text: newText }); setOpen(true); }; const handleAnonymize = () => { if (!selectionData) return; const { partIndex, start, end, text } = selectionData; // Split by brackets to reconstruct parts array as seen in render const parts = localMessage.split(/(\[[^\]]+\])/g); if (parts[partIndex] === undefined) return; const originalPart = parts[partIndex]; const newPart = originalPart.substring(0, start) + `[ANONYME_${text}]` + originalPart.substring(end); parts[partIndex] = newPart; const newMessage = parts.join(''); setLocalMessage(newMessage); setOpen(false); // Clear selection window.getSelection().removeAllRanges(); }; // Split by brackets, keeping the brackets in the split result for now // Regex: split by [content] const parts = localMessage.split(/(\[[^\]]+\])/g); return (
Anonymiser
); } export default function UserMessage() { const [show, setShow] = useState(true); return (
{props.author == 'user' ? (
{/*
*/} {props.message} {/*
*/}
) : ( {props.message} )}
); }