mention feature addd

This commit is contained in:
laxmanhalaki 2025-11-10 20:10:41 +05:30
parent 61ba649ac4
commit 7efc5c5d94

View File

@ -105,9 +105,10 @@ const getStatusText = (status: string) => {
};
const formatMessage = (content: string) => {
// Enhanced mention highlighting with better regex
// Enhanced mention highlighting - Blue color with extra bold font for high visibility
// Matches: @test user11 or @Test User11 (any case, stops before next sentence/punctuation)
return content
.replace(/@([\w\s]+)(?=\s|$|[.,!?])/g, '<span class="inline-flex items-center px-2 py-1 rounded-md bg-blue-100 text-blue-800 font-medium text-sm">@$1</span>')
.replace(/@([A-Za-z0-9]+(?:\s+[A-Za-z0-9]+)*?)(?=\s+(?:[a-z][a-z\s]*)?(?:[.,!?;:]|$))/g, '<span class="inline-flex items-center px-2.5 py-0.5 rounded-md bg-blue-50 text-blue-800 font-black text-base border-2 border-blue-400 shadow-sm">@$1</span>')
.replace(/\n/g, '<br />');
};
@ -689,6 +690,26 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
const handleSendMessage = async () => {
if (message.trim() || selectedFiles.length > 0) {
// Extract mentions from message
const mentions = extractMentions(message);
// Find mentioned user IDs from participants
const mentionedUserIds = mentions
.map(mentionedName => {
const participant = participants.find(p =>
p.name.toLowerCase().includes(mentionedName.toLowerCase())
);
console.log('[Mention Match] Looking for:', mentionedName, 'Found participant:', participant ? `${participant.name} (${(participant as any)?.userId})` : 'NOT FOUND');
return (participant as any)?.userId;
})
.filter(Boolean);
console.log('[WorkNoteChat] 📝 MESSAGE:', message);
console.log('[WorkNoteChat] 👥 ALL PARTICIPANTS:', participants.map(p => ({ name: p.name, userId: (p as any)?.userId })));
console.log('[WorkNoteChat] 🎯 MENTIONS EXTRACTED:', mentions);
console.log('[WorkNoteChat] 🆔 USER IDS FOUND:', mentionedUserIds);
console.log('[WorkNoteChat] 📤 SENDING TO BACKEND:', { message, mentions: mentionedUserIds });
const attachments = selectedFiles.map(file => ({
name: file.name,
url: URL.createObjectURL(file),
@ -707,19 +728,26 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
minute: 'numeric',
hour12: true
}),
mentions: extractMentions(message),
mentions: mentions,
isHighPriority: message.includes('!important') || message.includes('urgent'),
attachments: attachments.length > 0 ? attachments : undefined,
isCurrentUser: true
};
// console.log('new message ->', newMessage, onSend);
// If external onSend provided, delegate to caller (RequestDetail will POST and refresh)
if (onSend) {
try { await onSend(message, selectedFiles); } catch { /* ignore */ }
} else {
// Fallback: call backend directly
// Fallback: call backend directly with mentions
try {
await createWorkNoteMultipart(effectiveRequestId, { message }, selectedFiles);
await createWorkNoteMultipart(
effectiveRequestId,
{
message,
mentions: mentionedUserIds // Send mentioned user IDs to backend
},
selectedFiles
);
const rows = await getWorkNotes(effectiveRequestId);
const mapped = Array.isArray(rows) ? rows.map((m: any) => {
const noteUserId = m.userId || m.user_id;
@ -1025,7 +1053,8 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
];
const extractMentions = (text: string): string[] => {
const mentionRegex = /@([\w\s]+)(?=\s|$|[.,!?])/g;
// Use the SAME regex pattern as formatMessage to ensure consistency
const mentionRegex = /@([A-Za-z0-9]+(?:\s+[A-Za-z0-9]+)*?)(?=\s+(?:[a-z][a-z\s]*)?(?:[.,!?;:]|$))/g;
const mentions: string[] = [];
let match;
while ((match = mentionRegex.exec(text)) !== null) {
@ -1033,6 +1062,7 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
mentions.push(match[1].trim());
}
}
console.log('[Extract Mentions] Found:', mentions, 'from text:', text);
return mentions;
};
@ -1361,8 +1391,95 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
</div>
)}
{/* Textarea with Emoji Picker */}
{/* Textarea with Mention Dropdown and Emoji Picker */}
<div className="relative mb-2">
{/* Mention Suggestions Dropdown - Shows above textarea */}
{(() => {
const lastAtIndex = message.lastIndexOf('@');
const hasAt = lastAtIndex >= 0;
const textAfterAt = hasAt ? message.slice(lastAtIndex + 1) : '';
// Don't show if:
// 1. No @ found
// 2. Text after @ is too long (>20 chars)
// 3. Text after @ ends with a space (completed mention)
// 4. Text after @ contains a space (already selected a user)
const endsWithSpace = textAfterAt.endsWith(' ');
const containsSpace = textAfterAt.trim().includes(' ');
const shouldShowDropdown = hasAt &&
textAfterAt.length <= 20 &&
!endsWithSpace &&
!containsSpace;
console.log('[Mention Debug]', {
hasAt,
textAfterAt: `"${textAfterAt}"`,
endsWithSpace,
containsSpace,
shouldShowDropdown,
participantsCount: participants.length
});
if (!shouldShowDropdown) return null;
const searchTerm = textAfterAt.toLowerCase();
const filteredParticipants = participants.filter(p => {
// Exclude current user from mention suggestions
const isCurrentUserInList = (p as any).userId === currentUserId;
if (isCurrentUserInList) return false;
// Filter by search term (empty search term shows all)
if (searchTerm) {
return p.name.toLowerCase().includes(searchTerm);
}
return true; // Show all if no search term
});
console.log('[Mention Debug] Filtered participants:', filteredParticipants.length);
return (
<div className="absolute bottom-full left-0 mb-2 bg-white border-2 border-blue-300 rounded-lg shadow-2xl p-3 z-[100] w-full sm:max-w-md">
<p className="text-sm font-semibold text-gray-900 mb-2">💬 Mention someone</p>
<div className="max-h-60 overflow-y-auto space-y-1">
{filteredParticipants.length > 0 ? (
filteredParticipants.map((participant, idx) => (
<button
key={idx}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const lastAt = message.lastIndexOf('@');
const before = message.slice(0, lastAt);
setMessage(before + '@' + participant.name + ' ');
}}
className="w-full flex items-center gap-3 p-3 hover:bg-blue-50 rounded-lg text-left transition-colors border border-transparent hover:border-blue-200"
>
<Avatar className="h-10 w-10">
<AvatarFallback className={`text-white text-sm font-semibold ${
participant.role === 'Initiator' ? 'bg-green-600' :
participant.role === 'Approver' ? 'bg-purple-600' :
'bg-blue-500'
}`}>
{participant.avatar}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-gray-900">{participant.name}</p>
<p className="text-xs text-gray-600">{participant.role}</p>
</div>
</button>
))
) : (
<p className="text-sm text-gray-500 text-center py-4">
{searchTerm ? `No participants found matching "${searchTerm}"` : 'No other participants available'}
</p>
)}
</div>
</div>
);
})()}
<Textarea
placeholder="Type your message... Use @username to mention someone"
value={message}
@ -1427,7 +1544,7 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
<Button
variant="ghost"
size="sm"
className="text-gray-500 h-8 w-8 p-0 hidden sm:flex hover:bg-blue-50 hover:text-blue-600 flex-shrink-0"
className="text-gray-500 h-8 w-8 p-0 hover:bg-blue-50 hover:text-blue-600 flex-shrink-0"
onClick={() => setMessage(prev => prev + '@')}
title="Mention someone"
>