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(line, dic, show)}
) } i++ } return ({elements}
{dic[match[2]].value}
{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 (