mention feature addd
This commit is contained in:
parent
61ba649ac4
commit
7efc5c5d94
@ -105,9 +105,10 @@ const getStatusText = (status: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatMessage = (content: 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
|
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 />');
|
.replace(/\n/g, '<br />');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -689,6 +690,26 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
|
|||||||
|
|
||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async () => {
|
||||||
if (message.trim() || selectedFiles.length > 0) {
|
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 => ({
|
const attachments = selectedFiles.map(file => ({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
url: URL.createObjectURL(file),
|
url: URL.createObjectURL(file),
|
||||||
@ -707,19 +728,26 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
|
|||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
hour12: true
|
hour12: true
|
||||||
}),
|
}),
|
||||||
mentions: extractMentions(message),
|
mentions: mentions,
|
||||||
isHighPriority: message.includes('!important') || message.includes('urgent'),
|
isHighPriority: message.includes('!important') || message.includes('urgent'),
|
||||||
attachments: attachments.length > 0 ? attachments : undefined,
|
attachments: attachments.length > 0 ? attachments : undefined,
|
||||||
isCurrentUser: true
|
isCurrentUser: true
|
||||||
};
|
};
|
||||||
// console.log('new message ->', newMessage, onSend);
|
|
||||||
// If external onSend provided, delegate to caller (RequestDetail will POST and refresh)
|
// If external onSend provided, delegate to caller (RequestDetail will POST and refresh)
|
||||||
if (onSend) {
|
if (onSend) {
|
||||||
try { await onSend(message, selectedFiles); } catch { /* ignore */ }
|
try { await onSend(message, selectedFiles); } catch { /* ignore */ }
|
||||||
} else {
|
} else {
|
||||||
// Fallback: call backend directly
|
// Fallback: call backend directly with mentions
|
||||||
try {
|
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 rows = await getWorkNotes(effectiveRequestId);
|
||||||
const mapped = Array.isArray(rows) ? rows.map((m: any) => {
|
const mapped = Array.isArray(rows) ? rows.map((m: any) => {
|
||||||
const noteUserId = m.userId || m.user_id;
|
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 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[] = [];
|
const mentions: string[] = [];
|
||||||
let match;
|
let match;
|
||||||
while ((match = mentionRegex.exec(text)) !== null) {
|
while ((match = mentionRegex.exec(text)) !== null) {
|
||||||
@ -1033,6 +1062,7 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
|
|||||||
mentions.push(match[1].trim());
|
mentions.push(match[1].trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log('[Extract Mentions] Found:', mentions, 'from text:', text);
|
||||||
return mentions;
|
return mentions;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1361,8 +1391,95 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Textarea with Emoji Picker */}
|
{/* Textarea with Mention Dropdown and Emoji Picker */}
|
||||||
<div className="relative mb-2">
|
<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
|
<Textarea
|
||||||
placeholder="Type your message... Use @username to mention someone"
|
placeholder="Type your message... Use @username to mention someone"
|
||||||
value={message}
|
value={message}
|
||||||
@ -1427,7 +1544,7 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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 + '@')}
|
onClick={() => setMessage(prev => prev + '@')}
|
||||||
title="Mention someone"
|
title="Mention someone"
|
||||||
>
|
>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user