Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins: - F1: Evaluation progress indicator with touch tracking in sticky status bar - F2: Export filtering results as CSV with dynamic AI column flattening - F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure) Batch 2 - Jury Experience: - F4: Countdown timer component with urgency colors + email reminder service with cron endpoint - F5: Conflict of interest declaration system (dialog, admin management, review workflow) Batch 3 - Admin & AI Enhancements: - F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording - F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns - F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking) Batch 4 - Form Flexibility & Applicant Portal: - F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility) - F10: Applicant portal (status timeline, per-round documents, mentor messaging) Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
177
src/components/shared/mentor-chat.tsx
Normal file
177
src/components/shared/mentor-chat.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Send, MessageSquare } from 'lucide-react'
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
message: string
|
||||
createdAt: Date | string
|
||||
isRead: boolean
|
||||
sender: {
|
||||
id: string
|
||||
name: string | null
|
||||
email: string
|
||||
role?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface MentorChatProps {
|
||||
messages: Message[]
|
||||
currentUserId: string
|
||||
onSendMessage: (message: string) => Promise<void>
|
||||
isLoading?: boolean
|
||||
isSending?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MentorChat({
|
||||
messages,
|
||||
currentUserId,
|
||||
onSendMessage,
|
||||
isLoading,
|
||||
isSending,
|
||||
className,
|
||||
}: MentorChatProps) {
|
||||
const [newMessage, setNewMessage] = useState('')
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [messages])
|
||||
|
||||
const handleSend = async () => {
|
||||
const text = newMessage.trim()
|
||||
if (!text || isSending) return
|
||||
setNewMessage('')
|
||||
await onSendMessage(text)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (date: Date | string) => {
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
return d.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-3', className)}>
|
||||
<Skeleton className="h-16 w-3/4" />
|
||||
<Skeleton className="h-16 w-3/4 ml-auto" />
|
||||
<Skeleton className="h-16 w-3/4" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)}>
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto max-h-[400px] space-y-3 p-4">
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<MessageSquare className="h-10 w-10 mb-3 opacity-50" />
|
||||
<p className="text-sm font-medium">No messages yet</p>
|
||||
<p className="text-xs mt-1">Send a message to start the conversation</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg) => {
|
||||
const isOwn = msg.sender.id === currentUserId
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={cn(
|
||||
'flex',
|
||||
isOwn ? 'justify-end' : 'justify-start'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-[80%] rounded-lg px-4 py-2.5',
|
||||
isOwn
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted'
|
||||
)}
|
||||
>
|
||||
{!isOwn && (
|
||||
<p className={cn(
|
||||
'text-xs font-medium mb-1',
|
||||
isOwn ? 'text-primary-foreground/70' : 'text-foreground/70'
|
||||
)}>
|
||||
{msg.sender.name || msg.sender.email}
|
||||
{msg.sender.role === 'MENTOR' && (
|
||||
<span className="ml-1.5 text-[10px] font-normal opacity-70">
|
||||
Mentor
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm whitespace-pre-wrap break-words">
|
||||
{msg.message}
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
'text-[10px] mt-1',
|
||||
isOwn
|
||||
? 'text-primary-foreground/60'
|
||||
: 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{formatTime(msg.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="border-t p-3">
|
||||
<div className="flex gap-2">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message..."
|
||||
className="min-h-[40px] max-h-[120px] resize-none"
|
||||
rows={1}
|
||||
disabled={isSending}
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={handleSend}
|
||||
disabled={!newMessage.trim() || isSending}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground mt-1.5">
|
||||
Press Enter to send, Shift+Enter for new line
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user