1761 lines
78 KiB
Markdown
1761 lines
78 KiB
Markdown
# Mentor UI Redesign (Complete Workspace Experience)
|
||
|
||
## 1. Overview
|
||
|
||
Mentors are a **first-class role** in the MOPC platform. They guide finalist teams through the mentoring round (Round 6) — a collaboration layer between Jury 2 finalist selection and the Live Finals. The redesigned mentor experience provides a dedicated dashboard and full workspace for file collaboration, threaded comments, and file promotion to official submissions.
|
||
|
||
### What Changes
|
||
|
||
| Area | Current State | Redesigned State |
|
||
|------|---------------|------------------|
|
||
| **Dashboard** | Basic list of assigned projects | Rich dashboard with activity feed, status cards, metrics |
|
||
| **Messaging** | Simple chat via `MentorMessage` model | Enhanced messaging integrated into workspace |
|
||
| **File Sharing** | None — teams upload to official submission only | Full private workspace with file upload/download |
|
||
| **Comments** | None | Threaded file comments with @mentions |
|
||
| **File Promotion** | None | Promote workspace files to official submission slots |
|
||
| **Milestones** | Basic `MentorMilestone` tracking | Interactive milestone progress UI |
|
||
| **Navigation** | `/mentor` and `/mentor/projects` | Full IA with team-specific workspaces |
|
||
|
||
### Design Principles
|
||
|
||
1. **Privacy-first** — Workspace is private (mentor + team + admin only; jury can't see it)
|
||
2. **No duplication** — Promoted files reuse MinIO objects (no storage duplication)
|
||
3. **Provenance tracking** — Full audit trail from workspace upload → promotion → submission
|
||
4. **Mobile-friendly** — File review on tablets is a primary use case
|
||
5. **Integrated** — Workspace is tightly integrated with existing submission system
|
||
|
||
---
|
||
|
||
## 2. Current Mentor Experience (Baseline)
|
||
|
||
### Existing Components
|
||
|
||
| File | Purpose |
|
||
|------|---------|
|
||
| `src/components/layouts/mentor-nav.tsx` | Top navigation (Dashboard, My Projects, Resources) |
|
||
| `src/components/shared/mentor-chat.tsx` | Chat component for mentor-team messaging |
|
||
| `src/server/routers/mentor.ts` | tRPC procedures (assign, getSuggestions, etc.) |
|
||
| `src/server/services/mentor-matching.ts` | AI-based mentor-project matching |
|
||
|
||
### Current Data Models
|
||
|
||
```
|
||
MentorAssignment {
|
||
id, projectId, mentorId, method, assignedAt, assignedBy
|
||
aiConfidenceScore, expertiseMatchScore, aiReasoning
|
||
completionStatus, lastViewedAt
|
||
}
|
||
|
||
MentorMessage {
|
||
id, mentorAssignmentId, senderId, message, createdAt, isRead
|
||
}
|
||
|
||
MentorNote {
|
||
id, mentorAssignmentId, note, isVisibleToAdmin, createdAt
|
||
}
|
||
|
||
MentorMilestone {
|
||
id, competitionId, name, description, sortOrder
|
||
}
|
||
|
||
MentorMilestoneCompletion {
|
||
id, mentorAssignmentId, milestoneId, completedAt, notes
|
||
}
|
||
```
|
||
|
||
### Current Mentor Procedures (src/server/routers/mentor.ts)
|
||
|
||
| Procedure | Purpose |
|
||
|-----------|---------|
|
||
| `getSuggestions` | Get AI-suggested mentors for a project |
|
||
| `assign` | Manually assign mentor to project |
|
||
| `unassign` | Remove mentor assignment |
|
||
| `getMyAssignments` | List mentor's assigned projects |
|
||
| `getAssignmentDetail` | Get detailed view of a single assignment |
|
||
| `sendMessage` | Send message to team |
|
||
| `getMessages` | Retrieve chat history |
|
||
| `addNote` | Add private mentor note |
|
||
| `getNotes` | Retrieve mentor's notes |
|
||
| `completeMilestone` | Mark a milestone as complete |
|
||
| `getMilestones` | Get milestone list + completion status |
|
||
|
||
### What's Missing
|
||
|
||
- No file workspace
|
||
- No threaded comments
|
||
- No file promotion mechanism
|
||
- No visual file preview
|
||
- No mentor dashboard metrics
|
||
- No admin monitoring of mentor activity
|
||
- No file versioning in workspace
|
||
- No mobile-optimized file review
|
||
|
||
---
|
||
|
||
## 3. Mentor Dashboard (Landing Page)
|
||
|
||
The mentor dashboard is the entry point for all mentoring activities. It provides an at-a-glance view of assigned teams, workspace activity, and upcoming deadlines.
|
||
|
||
### Route
|
||
|
||
```
|
||
/mentor (dashboard)
|
||
```
|
||
|
||
### ASCII Mockup
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────────┐
|
||
│ MOPC 2026 — Mentor Dashboard [🔔 3] [Profile ▾]│
|
||
├──────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Mentoring Period: June 1 – June 30, 2026 │
|
||
│ ⏱ 18 days remaining │
|
||
│ │
|
||
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
|
||
│ │ 3 │ │ 12 │ │ 5 │ │
|
||
│ │ Assigned Teams │ │ Unread Messages│ │ Files Reviewed │ │
|
||
│ └────────────────┘ └────────────────┘ └────────────────┘ │
|
||
│ │
|
||
│ ┌─ My Assigned Teams ───────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ 🚀 OceanClean AI — Startup │ │ │
|
||
│ │ │ Team Lead: Sarah Johnson · sjohnson@oceanclean.ai │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ 💬 2 unread messages · 📁 3 files uploaded │ │ │
|
||
│ │ │ 🔄 Last activity: 2 hours ago │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ Status: ✅ Active Milestones: 2/4 completed │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ [Open Workspace] [View Details] │ │ │
|
||
│ │ └────────────────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ 💡 Blue Carbon Hub — Business Concept │ │ │
|
||
│ │ │ Team Lead: Dr. Maria Santos · msantos@bluecarbonhub.org │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ 💬 0 unread · 📁 1 file uploaded │ │ │
|
||
│ │ │ 🔄 Last activity: 2 days ago │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ Status: ⚠ Needs Attention Milestones: 1/4 completed │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ [Open Workspace] [View Details] │ │ │
|
||
│ │ └────────────────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ 🌊 SeaWatch Monitor — Startup │ │ │
|
||
│ │ │ Team Lead: John Chen · jchen@seawatch.io │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ 💬 0 unread · 📁 0 files │ │ │
|
||
│ │ │ 🔄 Last activity: Never │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ Status: ⚠ No Activity Yet Milestones: 0/4 completed │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ [Open Workspace] [View Details] │ │ │
|
||
│ │ └────────────────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─ Milestone Progress ───────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ ☑ Initial Review (3/3 teams completed) │ │
|
||
│ │ ├─ OceanClean AI: ✅ Jun 2 │ │
|
||
│ │ ├─ Blue Carbon Hub: ✅ Jun 3 │ │
|
||
│ │ └─ SeaWatch Monitor: ✅ Jun 4 │ │
|
||
│ │ │ │
|
||
│ │ ☐ Business Plan Feedback (1/3 teams completed) │ │
|
||
│ │ ├─ OceanClean AI: ✅ Jun 5 │ │
|
||
│ │ ├─ Blue Carbon Hub: ⏳ In progress │ │
|
||
│ │ └─ SeaWatch Monitor: ⏳ Not started │ │
|
||
│ │ │ │
|
||
│ │ ☐ Pitch Deck Review (0/3 teams) │ │
|
||
│ │ ☐ Final Submission Review (0/3 teams) │ │
|
||
│ │ │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─ Recent Activity ────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ 2 hours ago │ │
|
||
│ │ 📁 Sarah Johnson uploaded "Business Plan v3.pdf" │ │
|
||
│ │ in OceanClean AI workspace │ │
|
||
│ │ │ │
|
||
│ │ 5 hours ago │ │
|
||
│ │ 💬 You sent a message to Blue Carbon Hub │ │
|
||
│ │ │ │
|
||
│ │ Yesterday │ │
|
||
│ │ ✅ You completed "Initial Review" for SeaWatch Monitor │ │
|
||
│ │ │ │
|
||
│ │ 2 days ago │ │
|
||
│ │ 📁 You uploaded "Financial Model Template.xlsx" │ │
|
||
│ │ in OceanClean AI workspace │ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Data Requirements
|
||
|
||
**Team Cards:**
|
||
```typescript
|
||
// Query: mentor.getMyDashboard
|
||
{
|
||
teams: [
|
||
{
|
||
projectId: string,
|
||
projectTitle: string,
|
||
projectCategory: "STARTUP" | "BUSINESS_CONCEPT",
|
||
teamLead: { name, email },
|
||
mentorAssignmentId: string,
|
||
status: "active" | "needs_attention" | "no_activity" | "completed",
|
||
unreadMessageCount: number,
|
||
fileCount: number,
|
||
lastActivity: Date | null,
|
||
milestonesCompleted: number,
|
||
milestonesTotal: number,
|
||
}
|
||
],
|
||
stats: {
|
||
totalTeams: number,
|
||
unreadMessages: number,
|
||
filesReviewed: number,
|
||
filesPromoted: number,
|
||
},
|
||
mentoring: {
|
||
windowOpenAt: Date,
|
||
windowCloseAt: Date,
|
||
daysRemaining: number,
|
||
}
|
||
}
|
||
```
|
||
|
||
**Status Logic:**
|
||
- `no_activity` — lastActivity is null (team hasn't engaged)
|
||
- `needs_attention` — lastActivity > 3 days ago OR unreadMessageCount > 0
|
||
- `active` — Regular engagement
|
||
- `completed` — All milestones done
|
||
|
||
**Activity Feed:**
|
||
```typescript
|
||
// Query: mentor.getRecentActivity (limit: 10)
|
||
{
|
||
activities: [
|
||
{
|
||
id: string,
|
||
type: "FILE_UPLOADED" | "MESSAGE_SENT" | "MILESTONE_COMPLETED" | "FILE_PROMOTED",
|
||
actor: { id, name },
|
||
projectTitle: string,
|
||
timestamp: Date,
|
||
metadata: { fileName?, milestoneId? }
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### Component Breakdown
|
||
|
||
```
|
||
MentorDashboard (page)
|
||
├─ MentorHeader (period info, countdown, stats cards)
|
||
├─ TeamCardList
|
||
│ └─ TeamCard (reusable, shows status + quick actions)
|
||
├─ MilestoneProgressSection
|
||
│ └─ MilestoneExpandableList (shows per-team breakdown)
|
||
└─ RecentActivityFeed
|
||
└─ ActivityItem (icon + text + timestamp)
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Workspace (Per-Team Private Collaboration Space)
|
||
|
||
The workspace is the heart of the mentor experience. It's a split-pane interface with files on the left and detail view on the right.
|
||
|
||
### Route
|
||
|
||
```
|
||
/mentor/team/[projectId]
|
||
/mentor/team/[projectId]/files
|
||
/mentor/team/[projectId]/file/[fileId]
|
||
/mentor/team/[projectId]/chat
|
||
/mentor/team/[projectId]/milestones
|
||
```
|
||
|
||
### ASCII Mockup — File List View
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────────┐
|
||
│ ← Back to Dashboard [🔔 3] [Profile ▾]│
|
||
├──────────────────────────────────────────────────────────────────────────┤
|
||
│ OceanClean AI — Workspace │
|
||
│ Team Lead: Sarah Johnson · sjohnson@oceanclean.ai │
|
||
│ Mentoring Period: June 1 – June 30 · ⏱ 18 days remaining │
|
||
├──────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ [📁 Files] [💬 Chat (2)] [📋 Milestones (2/4)] │
|
||
│ │
|
||
│ ┌─────────────────────┬────────────────────────────────────────────────┐│
|
||
│ │ Files (5) │ File Detail ││
|
||
│ │ [📤 Upload File] │ ││
|
||
│ │ │ ┌────────────────────────────────────────────┐ ││
|
||
│ │ ───────────────────│ │ 📄 Business Plan v3.pdf │ ││
|
||
│ │ │ │ Uploaded by Sarah Johnson (Team) │ ││
|
||
│ │ 📄 Business Plan.. │ │ Jun 8, 2026 · 2.4 MB │ ││
|
||
│ │ Sarah · Jun 8 │ │ │ ││
|
||
│ │ 💬 4 comments │ │ [📥 Download] [💬 Comments (4)] │ ││
|
||
│ │ [Open →] │ │ [🚀 Promote to Submission] │ ││
|
||
│ │ │ │ │ ││
|
||
│ │ 📊 Financial Mod.. │ │ ───────────────────────────────────────── │ ││
|
||
│ │ You · Jun 6 │ │ │ ││
|
||
│ │ 💬 2 comments │ │ 💬 Comments (4) │ ││
|
||
│ │ [Open →] │ │ │ ││
|
||
│ │ │ │ Dr. Martin Duval (Mentor) · Jun 8, 14:30 │ ││
|
||
│ │ 📄 Market Analys.. │ │ Section 3.2 needs stronger market │ ││
|
||
│ │ Sarah · Jun 5 │ │ analysis. Consider adding competitor │ ││
|
||
│ │ 💬 1 comment │ │ comparisons and market size estimates. │ ││
|
||
│ │ [Open →] │ │ │ ││
|
||
│ │ │ │ └─ Sarah Johnson (Team) · Jun 8, 16:00 │ ││
|
||
│ │ 📄 Pitch Deck Dr.. │ │ Good point! We'll add a competitive │ ││
|
||
│ │ Sarah · Jun 4 │ │ landscape section in the next ver. │ ││
|
||
│ │ ✅ PROMOTED │ │ │ ││
|
||
│ │ [View] │ │ Dr. Martin Duval (Mentor) · Jun 9, 10:00 │ ││
|
||
│ │ │ │ Financial projections look much better. │ ││
|
||
│ │ 🎥 Video Pitch │ │ Revenue model is solid. Ready to promote? │ ││
|
||
│ │ Sarah · Jun 3 │ │ │ ││
|
||
│ │ 💬 0 comments │ │ └─ Sarah Johnson (Team) · Jun 9, 11:30 │ ││
|
||
│ │ [Open →] │ │ Yes! Please promote to the Business │ ││
|
||
│ │ │ │ Plan requirement slot. │ ││
|
||
│ │ │ │ │ ││
|
||
│ │ │ │ [Add comment... ] │ ││
|
||
│ │ │ │ [Post Comment] │ ││
|
||
│ │ │ └────────────────────────────────────────────┘ ││
|
||
│ │ │ ││
|
||
│ └─────────────────────┴────────────────────────────────────────────────┘│
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Split-Pane Behavior
|
||
|
||
| State | Left Pane | Right Pane |
|
||
|-------|-----------|------------|
|
||
| **Default** | File list | File upload instructions + quick stats |
|
||
| **File selected** | File list (selected file highlighted) | File detail (preview, comments, actions) |
|
||
| **Mobile** | Stack vertically — file list on top, detail below when file selected |
|
||
|
||
### File List Item Structure
|
||
|
||
```typescript
|
||
interface WorkspaceFile {
|
||
id: string
|
||
fileName: string
|
||
uploadedBy: { id, name, role: "MENTOR" | "TEAM" }
|
||
uploadedAt: Date
|
||
size: number
|
||
mimeType: string
|
||
commentCount: number
|
||
isPromoted: boolean
|
||
promotedToSlot?: { requirementName: string, submissionWindowName: string }
|
||
}
|
||
```
|
||
|
||
### File Actions
|
||
|
||
| Action | Visibility | Behavior |
|
||
|--------|------------|----------|
|
||
| **Download** | Always | Get MinIO pre-signed URL, trigger browser download |
|
||
| **Comment** | Always | Open comment thread in right pane |
|
||
| **Promote** | Only if not already promoted | Show promotion dialog (see section 5) |
|
||
| **Delete** | Uploader or admin only | Soft delete (mark as deleted, don't remove from MinIO) |
|
||
|
||
---
|
||
|
||
## 5. File Review & Comments (Threaded Discussion)
|
||
|
||
### Comment Thread UI
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ 💬 Comments on: Business Plan v3.pdf │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Dr. Martin Duval (Mentor) Jun 8, 14:30 │
|
||
│ Section 3.2 needs stronger market analysis. Consider │
|
||
│ adding competitor comparisons and market size estimates. │
|
||
│ │
|
||
│ └─ Sarah Johnson (Team) Jun 8, 16:00 │
|
||
│ Good point! We'll add a competitive landscape │
|
||
│ section in the next version. @Dr. Martin would you │
|
||
│ prefer a SWOT matrix or Porter's 5 Forces analysis? │
|
||
│ │
|
||
│ └─ Dr. Martin Duval (Mentor) Jun 8, 17:00 │
|
||
│ Porter's 5 Forces would be stronger for this │
|
||
│ type of technology play. Focus on threat of │
|
||
│ substitutes and competitive rivalry. │
|
||
│ │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Dr. Martin Duval (Mentor) Jun 9, 10:00 │
|
||
│ Financial projections look much better. Revenue model is │
|
||
│ solid. Ready to promote this to official submission? │
|
||
│ │
|
||
│ └─ Sarah Johnson (Team) Jun 9, 11:30 │
|
||
│ Yes! Please promote to the Business Plan slot. │
|
||
│ │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ [Add comment... ] │
|
||
│ │
|
||
│ [@mention team member] [Post Comment] │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Comment Features
|
||
|
||
**Threading:**
|
||
- Top-level comments (parentCommentId = null)
|
||
- Nested replies (parentCommentId = parent's ID)
|
||
- Maximum nesting depth: 3 levels (prevents deep threading hell)
|
||
|
||
**@Mentions:**
|
||
- Typing `@` triggers autocomplete of team members + mentor
|
||
- Mentioned users get in-app notification
|
||
- Mention is rendered as a link to user profile
|
||
|
||
**Formatting:**
|
||
- Plain text only (no rich text editor — keep it simple)
|
||
- Auto-linkify URLs
|
||
- Preserve line breaks
|
||
|
||
**Deletion:**
|
||
- Author can delete own comments within 1 hour of posting
|
||
- Admin can delete any comment
|
||
- Deleted comments show "[Comment deleted]" placeholder (preserve thread structure)
|
||
|
||
### Comment API
|
||
|
||
| Procedure | Purpose |
|
||
|-----------|---------|
|
||
| `mentorWorkspace.addComment(fileId, content, parentCommentId?)` | Add comment |
|
||
| `mentorWorkspace.listComments(fileId)` | Get threaded comments |
|
||
| `mentorWorkspace.deleteComment(commentId)` | Delete comment |
|
||
|
||
### Comment Data Structure
|
||
|
||
```typescript
|
||
interface MentorFileComment {
|
||
id: string
|
||
mentorFileId: string
|
||
authorId: string
|
||
author: { id, name, role }
|
||
content: string
|
||
parentCommentId: string | null
|
||
replies: MentorFileComment[] // Nested replies
|
||
createdAt: Date
|
||
updatedAt: Date
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. File Promotion (Workspace → Official Submission)
|
||
|
||
File promotion is the **key value add** of the mentor workspace. It allows refined documents to become official submissions without manual re-upload.
|
||
|
||
### Promotion Flow
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────────┐
|
||
│ Step 1: Mentor/Team clicks "Promote →" on a workspace file │
|
||
└────────────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌────────────────────────────────────────────────────────────────┐
|
||
│ Step 2: Promotion Dialog Appears │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────┐ │
|
||
│ │ Promote File to Official Submission │ │
|
||
│ │ │ │
|
||
│ │ File: Business Plan v3.pdf │ │
|
||
│ │ Size: 2.4 MB │ │
|
||
│ │ │ │
|
||
│ │ Target Submission Window: │ │
|
||
│ │ ┌────────────────────────────────────────────────┐ │ │
|
||
│ │ │ Round 2 Docs (Semi-finalist Submissions) ▾ │ │ │
|
||
│ │ └────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ Replaces Requirement Slot: │ │
|
||
│ │ ┌────────────────────────────────────────────────┐ │ │
|
||
│ │ │ Business Plan ▾ │ │ │
|
||
│ │ └────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ Current file in this slot: │ │
|
||
│ │ "Business Plan v2.pdf" uploaded Jun 3, 2026 │ │
|
||
│ │ │ │
|
||
│ │ ⚠ This will replace the existing file. The old file │ │
|
||
│ │ will remain in version history but won't be shown │ │
|
||
│ │ to jurors. │ │
|
||
│ │ │ │
|
||
│ │ [Cancel] [Promote & Replace File] │ │
|
||
│ └────────────────────────────────────────────────────────┘ │
|
||
└────────────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌────────────────────────────────────────────────────────────────┐
|
||
│ Step 3: Server Executes Promotion │
|
||
│ │
|
||
│ 1. Validate: │
|
||
│ ✓ Workspace is active │
|
||
│ ✓ File not already promoted │
|
||
│ ✓ Submission window exists and is accessible │
|
||
│ ✓ User has permission (team lead or admin) │
|
||
│ │
|
||
│ 2. Find existing ProjectFile for that requirement slot: │
|
||
│ ProjectFile.where(projectId, requirementId) │
|
||
│ │
|
||
│ 3. Create new ProjectFile: │
|
||
│ { │
|
||
│ projectId: project.id, │
|
||
│ submissionWindowId: selected window, │
|
||
│ requirementId: selected requirement, │
|
||
│ fileName: "Business Plan v3.pdf", │
|
||
│ mimeType: "application/pdf", │
|
||
│ size: 2.4MB, │
|
||
│ bucket: same as MentorFile, │
|
||
│ objectKey: same as MentorFile, // NO DUPLICATION │
|
||
│ version: 3, // increment from previous │
|
||
│ } │
|
||
│ │
|
||
│ 4. Update old file (if exists): │
|
||
│ oldFile.replacedById = newFile.id │
|
||
│ │
|
||
│ 5. Update MentorFile: │
|
||
│ { │
|
||
│ isPromoted: true, │
|
||
│ promotedToFileId: newFile.id, │
|
||
│ promotedAt: now, │
|
||
│ promotedByUserId: actor.id, │
|
||
│ } │
|
||
│ │
|
||
│ 6. Audit log entry: │
|
||
│ { │
|
||
│ action: "MENTOR_FILE_PROMOTED", │
|
||
│ actorId: actor.id, │
|
||
│ targetType: "PROJECT_FILE", │
|
||
│ targetId: newFile.id, │
|
||
│ metadata: { │
|
||
│ mentorFileId: mentorFile.id, │
|
||
│ submissionWindowId: window.id, │
|
||
│ requirementId: requirement.id, │
|
||
│ replacedFileId: oldFile?.id, │
|
||
│ } │
|
||
│ } │
|
||
└────────────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌────────────────────────────────────────────────────────────────┐
|
||
│ Step 4: UI Updates │
|
||
│ │
|
||
│ - MentorFile shows "✅ PROMOTED" badge │
|
||
│ - Promote button disabled │
|
||
│ - Toast: "File promoted to Business Plan slot" │
|
||
│ - ProjectFile now visible to jury in evaluation round │
|
||
└────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Promotion Dialog Component
|
||
|
||
```tsx
|
||
// components/mentor/file-promotion-dialog.tsx
|
||
|
||
interface FilePromotionDialogProps {
|
||
mentorFile: WorkspaceFile
|
||
availableWindows: SubmissionWindow[]
|
||
currentWindowId: string // Default selection
|
||
onPromote: (windowId: string, requirementId: string | null) => Promise<void>
|
||
}
|
||
|
||
// Logic:
|
||
// 1. User selects target submission window
|
||
// 2. Load requirements for that window (via tRPC query)
|
||
// 3. Show dropdown of available requirement slots
|
||
// 4. Show warning if slot already has a file
|
||
// 5. Confirm and execute promotion
|
||
```
|
||
|
||
### Promotion Rules
|
||
|
||
| Rule | Enforcement |
|
||
|------|-------------|
|
||
| **Only unpromoted files** | isPromoted = false |
|
||
| **Workspace must be active** | workspaceOpenAt <= now <= workspaceCloseAt |
|
||
| **User must have permission** | Team lead OR admin OR (mentor if config allows) |
|
||
| **Target window must exist** | SubmissionWindow.id valid |
|
||
| **No file duplication** | ProjectFile.objectKey = MentorFile.objectKey (same MinIO object) |
|
||
| **Versioning** | New file increments version number |
|
||
| **Audit trail** | Full provenance logged |
|
||
|
||
### Unpromote (Revert)
|
||
|
||
Admin can revert a promotion:
|
||
|
||
```
|
||
1. Find ProjectFile created by promotion (via MentorFile.promotedToFileId)
|
||
2. If it replaced a previous file, restore that file (set replacedById = null)
|
||
3. Delete the promoted ProjectFile
|
||
4. Reset MentorFile flags:
|
||
- isPromoted = false
|
||
- promotedToFileId = null
|
||
- promotedAt = null
|
||
- promotedByUserId = null
|
||
5. Audit log: "MENTOR_FILE_UNPROMOTED"
|
||
```
|
||
|
||
**API:**
|
||
- `mentorWorkspace.unpromoteFile(mentorFileId)` — admin only
|
||
|
||
---
|
||
|
||
## 7. Messaging (Team Communication)
|
||
|
||
The workspace includes integrated chat for real-time communication between mentor and team.
|
||
|
||
### Chat Tab UI
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ [📁 Files] [💬 Chat (2)] [📋 Milestones] │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────┐ │
|
||
│ │ Dr. Martin Duval (Mentor) Jun 8, 10:30 AM │ │
|
||
│ │ Welcome to the mentoring workspace! I've reviewed │ │
|
||
│ │ your initial application. Let's focus on strengthening │ │
|
||
│ │ the financial projections first. I'll upload a │ │
|
||
│ │ template to the Files tab. │ │
|
||
│ └────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────┐ │
|
||
│ │ Jun 8, 2:15 PM │ │
|
||
│ │ Sarah Johnson (Team) │ │
|
||
│ │ Thank you! We've uploaded the │ │
|
||
│ │ revised business plan to the │ │
|
||
│ │ Files section. Looking forward │ │
|
||
│ │ to your feedback. │ │
|
||
│ └────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────┐ │
|
||
│ │ Dr. Martin Duval (Mentor) Jun 9, 9:00 AM │ │
|
||
│ │ Excellent work on the revisions! I've left detailed │ │
|
||
│ │ comments on the file. The market analysis is much │ │
|
||
│ │ stronger now. One more iteration should get us to │ │
|
||
│ │ a promotion-ready version. │ │
|
||
│ └────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ─────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ [Type a message... ] │
|
||
│ │
|
||
│ [📎 Attach File] [Send] │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Message Features
|
||
|
||
**Real-time Updates:**
|
||
- Polling every 10 seconds for new messages
|
||
- WebSocket support if available
|
||
- Unread count badge on tab
|
||
|
||
**Message Types:**
|
||
| Type | Description |
|
||
|------|-------------|
|
||
| `text` | Plain text message |
|
||
| `file_reference` | Link to a workspace file ("I've uploaded X — see Files tab") |
|
||
| `milestone` | Milestone completion notification |
|
||
|
||
**Notifications:**
|
||
- In-app notification on new message
|
||
- Email notification if recipient hasn't viewed workspace in 24 hours
|
||
- Push notification if enabled
|
||
|
||
**Read Receipts:**
|
||
- Messages marked as read when chat tab is viewed
|
||
- No "typing..." indicator (keep it simple)
|
||
|
||
---
|
||
|
||
## 8. Milestones (Progress Tracking)
|
||
|
||
Milestones are optional admin-defined checkpoints for mentoring progress. They're displayed in the workspace and on the mentor dashboard.
|
||
|
||
### Milestones Tab UI
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ [📁 Files] [💬 Chat] [📋 Milestones (2/4)] │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Mentoring Progress │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ ✅ Initial Review │ │
|
||
│ │ Completed: Jun 2, 2026 by Dr. Martin Duval │ │
|
||
│ │ Notes: Reviewed application materials. Strong concept │ │
|
||
│ │ but needs financial model refinement. │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ ✅ Business Plan Feedback │ │
|
||
│ │ Completed: Jun 5, 2026 by Dr. Martin Duval │ │
|
||
│ │ Notes: Provided detailed comments on v2. Team responded │ │
|
||
│ │ with v3 incorporating all feedback. │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ ⏳ Pitch Deck Review │ │
|
||
│ │ │ │
|
||
│ │ [Mark as Complete] │ │
|
||
│ │ │ │
|
||
│ │ Optional Notes: │ │
|
||
│ │ [Reviewed draft deck. Suggested stronger opening... ] │ │
|
||
│ │ │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ ⏳ Final Submission Review │ │
|
||
│ │ │ │
|
||
│ │ [Mark as Complete] │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Milestone Data Model
|
||
|
||
```prisma
|
||
model MentorMilestone {
|
||
id String @id @default(cuid())
|
||
competitionId String
|
||
name String // "Initial Review", "Business Plan Feedback"
|
||
description String? @db.Text
|
||
sortOrder Int @default(0)
|
||
isRequired Boolean @default(false) // If true, must be completed before workspace closes
|
||
|
||
competition Competition @relation(...)
|
||
completions MentorMilestoneCompletion[]
|
||
}
|
||
|
||
model MentorMilestoneCompletion {
|
||
id String @id @default(cuid())
|
||
mentorAssignmentId String
|
||
milestoneId String
|
||
completedAt DateTime @default(now())
|
||
notes String? @db.Text
|
||
|
||
mentorAssignment MentorAssignment @relation(...)
|
||
milestone MentorMilestone @relation(...)
|
||
|
||
@@unique([mentorAssignmentId, milestoneId])
|
||
}
|
||
```
|
||
|
||
### Milestone API
|
||
|
||
| Procedure | Purpose |
|
||
|-----------|---------|
|
||
| `mentor.getMilestones(mentorAssignmentId)` | Get milestones + completion status for a team |
|
||
| `mentor.completeMilestone(assignmentId, milestoneId, notes)` | Mark milestone complete |
|
||
| `mentor.uncompleteMilestone(completionId)` | Revert completion (admin only) |
|
||
|
||
---
|
||
|
||
## 9. Navigation & Information Architecture
|
||
|
||
### Route Structure
|
||
|
||
```
|
||
/mentor/ → Dashboard (landing page)
|
||
/mentor/team/[projectId] → Workspace (redirects to /files)
|
||
/mentor/team/[projectId]/files → File list + detail split-pane
|
||
/mentor/team/[projectId]/file/[fileId] → Full-screen file detail (mobile)
|
||
/mentor/team/[projectId]/chat → Chat tab
|
||
/mentor/team/[projectId]/milestones → Milestones tab
|
||
/mentor/notifications → Notification center
|
||
/mentor/settings → Mentor profile/preferences
|
||
```
|
||
|
||
### Navigation Component
|
||
|
||
```tsx
|
||
// components/layouts/mentor-workspace-nav.tsx
|
||
|
||
interface MentorWorkspaceNavProps {
|
||
projectId: string
|
||
currentTab: "files" | "chat" | "milestones"
|
||
unreadMessageCount: number
|
||
milestonesCompleted: number
|
||
milestonesTotal: number
|
||
}
|
||
|
||
// Renders:
|
||
// [📁 Files] [💬 Chat (2)] [📋 Milestones (2/4)]
|
||
```
|
||
|
||
### Breadcrumb
|
||
|
||
```
|
||
Dashboard > My Projects > OceanClean AI > Files
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Admin View of Mentoring (Monitoring & Override)
|
||
|
||
Admin can monitor all mentoring activity and intervene when needed.
|
||
|
||
### Admin Dashboard — Mentoring Overview
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────────┐
|
||
│ Admin Dashboard > Mentoring Overview │
|
||
├──────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Mentoring Period: June 1 – June 30, 2026 · ⏱ 18 days remaining │
|
||
│ │
|
||
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
|
||
│ │ 12 │ │ 8 │ │ 4 │ │
|
||
│ │ Active Pairs │ │ Files Promoted │ │ Inactive Teams │ │
|
||
│ └────────────────┘ └────────────────┘ └────────────────┘ │
|
||
│ │
|
||
│ ┌─ Mentor-Team Pairings ────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ Search: [Filter by mentor, project, or status...] │ │
|
||
│ │ │ │
|
||
│ │ Mentor | Project | Files | Msgs | Last Act │ │
|
||
│ │ ────────────────────┼──────────────────┼───────┼──────┼────────── │ │
|
||
│ │ Dr. Martin Duval | OceanClean AI | 5 | 8 | 2 hrs ago │ │
|
||
│ │ | Blue Carbon Hub | 1 | 3 | 2 days │ │
|
||
│ │ | SeaWatch Monitor | 0 | 0 | Never │ │
|
||
│ │ | | | | [Reassign]│ │
|
||
│ │ │ │
|
||
│ │ Dr. Lisa Chen | WaveGen Pro | 3 | 12 | 5 hrs ago │ │
|
||
│ │ | Reef Restore AI | 2 | 6 | 1 day │ │
|
||
│ │ │ │
|
||
│ │ Prof. Ahmed Hassan | PlasticTrack | 4 | 10 | 3 hrs ago │ │
|
||
│ │ | CoralSense | 1 | 2 | 4 days │ │
|
||
│ │ | | | | ⚠ Flag │ │
|
||
│ │ │ │
|
||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─ Alerts ──────────────────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ ⚠ 4 teams have no activity in the last 5 days │ │
|
||
│ │ ⚠ 2 mentors are at max capacity (3 teams each) │ │
|
||
│ │ ⚠ 1 team has unread messages for 48+ hours │ │
|
||
│ │ │ │
|
||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ [Export Mentoring Report] [Bulk Reassign] [Extend Deadline] │
|
||
└──────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Admin Actions
|
||
|
||
| Action | Purpose |
|
||
|--------|---------|
|
||
| **View any workspace** | Full read/write access to all workspaces |
|
||
| **Reassign mentor** | Change mentor for a project mid-stream |
|
||
| **Promote files on behalf of team** | Force promotion if team is unresponsive |
|
||
| **Extend deadline for specific team** | Grant extra time |
|
||
| **Flag inactive pairs** | Mark for follow-up |
|
||
| **Export workspace data** | Download all files, messages, comments for audit |
|
||
|
||
### Admin Workspace Access
|
||
|
||
Admin sees an additional "Admin Actions" section in any workspace:
|
||
|
||
```
|
||
┌─ Admin Actions ─────────────────────────────────────────┐
|
||
│ │
|
||
│ [Reassign Mentor] [Extend Deadline] [Export Data] │
|
||
│ [Promote File (Override)] [Close Workspace] │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Mobile Experience (Tablet-Optimized)
|
||
|
||
The workspace is designed to work well on tablets (iPad, Android tablets) for file review on the go.
|
||
|
||
### Mobile Layout
|
||
|
||
**Portrait Mode:**
|
||
```
|
||
┌─────────────────────────────┐
|
||
│ ← Back OceanClean AI │
|
||
├─────────────────────────────┤
|
||
│ [Files] [Chat] [Milestones] │
|
||
├─────────────────────────────┤
|
||
│ │
|
||
│ Files (5) │
|
||
│ │
|
||
│ 📄 Business Plan v3.pdf │
|
||
│ Sarah · Jun 8 · 💬 4 │
|
||
│ [Open] │
|
||
│ │
|
||
│ 📊 Financial Model.xlsx │
|
||
│ You · Jun 6 · 💬 2 │
|
||
│ [Open] │
|
||
│ │
|
||
│ 📄 Market Analysis.pdf │
|
||
│ Sarah · Jun 5 · 💬 1 │
|
||
│ [Open] │
|
||
│ │
|
||
│ ... │
|
||
│ │
|
||
└─────────────────────────────┘
|
||
|
||
Tap a file →
|
||
|
||
┌─────────────────────────────┐
|
||
│ ← Back to Files │
|
||
├─────────────────────────────┤
|
||
│ 📄 Business Plan v3.pdf │
|
||
│ Sarah Johnson (Team) │
|
||
│ Jun 8, 2026 · 2.4 MB │
|
||
├─────────────────────────────┤
|
||
│ │
|
||
│ [📥 Download] │
|
||
│ [🚀 Promote] │
|
||
│ │
|
||
│ ─────────────────────── │
|
||
│ │
|
||
│ 💬 Comments (4) │
|
||
│ │
|
||
│ Dr. Martin · Jun 8, 14:30 │
|
||
│ Section 3.2 needs stronger │
|
||
│ market analysis... │
|
||
│ │
|
||
│ └─ Sarah · Jun 8, 16:00 │
|
||
│ Good point! We'll add │
|
||
│ a competitive section. │
|
||
│ │
|
||
│ [Add comment...] │
|
||
│ │
|
||
└─────────────────────────────┘
|
||
```
|
||
|
||
**Landscape Mode:**
|
||
```
|
||
┌───────────────────────────────────────────────────────────┐
|
||
│ ← Back to Dashboard OceanClean AI [🔔 3] [Menu ▾]│
|
||
├──────────────┬────────────────────────────────────────────┤
|
||
│ Files (5) │ 📄 Business Plan v3.pdf │
|
||
│ │ Sarah Johnson · Jun 8 · 2.4 MB │
|
||
│ 📄 Business..│ [📥 Download] [🚀 Promote] │
|
||
│ 💬 4 │ │
|
||
│ │ ────────────────────────────────────── │
|
||
│ 📊 Financia..│ 💬 Comments (4) │
|
||
│ 💬 2 │ │
|
||
│ │ Dr. Martin · Jun 8, 14:30 │
|
||
│ 📄 Market A..│ Section 3.2 needs stronger... │
|
||
│ 💬 1 │ └─ Sarah · Jun 8, 16:00 │
|
||
│ │ Good point! We'll add... │
|
||
│ │ │
|
||
└──────────────┴────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Mobile-Specific Features
|
||
|
||
- **Swipe gestures** — Swipe left on file to reveal delete/promote actions
|
||
- **Pull to refresh** — Refresh file list and comments
|
||
- **Touch-friendly targets** — Minimum 44px tap targets
|
||
- **Offline mode** — Download files for offline review, sync comments when back online
|
||
- **File preview** — In-app PDF preview (no external app required)
|
||
|
||
---
|
||
|
||
## 12. Accessibility
|
||
|
||
### WCAG 2.1 AA Compliance
|
||
|
||
| Feature | Implementation |
|
||
|---------|----------------|
|
||
| **Keyboard navigation** | Full keyboard access to all workspace features |
|
||
| **Screen reader support** | ARIA labels on all interactive elements |
|
||
| **Color contrast** | 4.5:1 minimum for all text |
|
||
| **Focus indicators** | Visible focus ring on all focusable elements |
|
||
| **Alt text** | File type icons have descriptive labels |
|
||
| **Error messaging** | Clear, descriptive error messages on validation failures |
|
||
|
||
### File Preview Accessibility
|
||
|
||
For PDF file previews:
|
||
- Use `<iframe>` with PDF.js (accessible)
|
||
- Provide "Download" button as alternative to in-browser preview
|
||
- Alt text: "PDF preview of [filename]"
|
||
|
||
For image files:
|
||
- Show full-size image with alt text from file description
|
||
- Zoom controls for low vision users
|
||
|
||
For other file types (Excel, PowerPoint):
|
||
- No in-browser preview — download only
|
||
- Clear file type indicator and size
|
||
|
||
---
|
||
|
||
## 13. Component Library
|
||
|
||
### New Components
|
||
|
||
| Component | Path | Purpose |
|
||
|-----------|------|---------|
|
||
| `MentorDashboard` | `src/app/(mentor)/mentor/page.tsx` | Landing page |
|
||
| `TeamCard` | `src/components/mentor/team-card.tsx` | Reusable team summary card |
|
||
| `WorkspaceLayout` | `src/app/(mentor)/mentor/team/[projectId]/layout.tsx` | Workspace shell with tabs |
|
||
| `FileList` | `src/components/mentor/file-list.tsx` | Left pane file list |
|
||
| `FileDetail` | `src/components/mentor/file-detail.tsx` | Right pane file detail |
|
||
| `FilePromotionDialog` | `src/components/mentor/file-promotion-dialog.tsx` | Promotion confirmation dialog |
|
||
| `FileUploadZone` | `src/components/mentor/file-upload-zone.tsx` | Drag-and-drop upload area |
|
||
| `CommentThread` | `src/components/mentor/comment-thread.tsx` | Threaded comment display |
|
||
| `CommentInput` | `src/components/mentor/comment-input.tsx` | Comment textarea with @mentions |
|
||
| `MilestoneList` | `src/components/mentor/milestone-list.tsx` | Milestone progress view |
|
||
| `MentorActivityFeed` | `src/components/mentor/activity-feed.tsx` | Recent activity stream |
|
||
| `WorkspaceStatusBadge` | `src/components/mentor/workspace-status-badge.tsx` | Status indicator (active, needs attention, etc.) |
|
||
|
||
### Modified Existing Components
|
||
|
||
| Component | Changes |
|
||
|-----------|---------|
|
||
| `MentorChat` | Integrate into workspace tabs, add file reference support |
|
||
| `MentorNav` | Add "Workspaces" navigation item |
|
||
|
||
---
|
||
|
||
## 14. API Reference (New tRPC Procedures)
|
||
|
||
### Router: `mentorWorkspace`
|
||
|
||
All procedures require mentor, team lead, or admin authentication.
|
||
|
||
```typescript
|
||
// src/server/routers/mentor-workspace.ts
|
||
|
||
export const mentorWorkspaceRouter = router({
|
||
// File Upload
|
||
getUploadUrl: mentorOrTeamProcedure
|
||
.input(z.object({
|
||
mentorAssignmentId: z.string(),
|
||
fileName: z.string(),
|
||
mimeType: z.string(),
|
||
}))
|
||
.mutation(async ({ ctx, input }) => {
|
||
// Generate MinIO pre-signed PUT URL
|
||
return { uploadUrl, objectKey }
|
||
}),
|
||
|
||
saveFile: mentorOrTeamProcedure
|
||
.input(z.object({
|
||
mentorAssignmentId: z.string(),
|
||
fileName: z.string(),
|
||
mimeType: z.string(),
|
||
size: z.number(),
|
||
bucket: z.string(),
|
||
objectKey: z.string(),
|
||
description: z.string().optional(),
|
||
}))
|
||
.mutation(async ({ ctx, input }) => {
|
||
// Create MentorFile record
|
||
return mentorFile
|
||
}),
|
||
|
||
// File Management
|
||
listFiles: mentorOrTeamProcedure
|
||
.input(z.object({
|
||
mentorAssignmentId: z.string(),
|
||
}))
|
||
.query(async ({ ctx, input }) => {
|
||
// Return MentorFile[] with comment counts
|
||
return { files }
|
||
}),
|
||
|
||
getFileDownloadUrl: mentorOrTeamProcedure
|
||
.input(z.object({
|
||
mentorFileId: z.string(),
|
||
}))
|
||
.query(async ({ ctx, input }) => {
|
||
// Generate MinIO pre-signed GET URL
|
||
return { downloadUrl, expiresAt }
|
||
}),
|
||
|
||
deleteFile: mentorOrTeamProcedure
|
||
.input(z.object({
|
||
mentorFileId: z.string(),
|
||
}))
|
||
.mutation(async ({ ctx, input }) => {
|
||
// Soft delete (mark as deleted)
|
||
// Only uploader or admin can delete
|
||
return { success: true }
|
||
}),
|
||
|
||
// Comments
|
||
addComment: mentorOrTeamProcedure
|
||
.input(z.object({
|
||
mentorFileId: z.string(),
|
||
content: z.string().min(1),
|
||
parentCommentId: z.string().optional(),
|
||
}))
|
||
.mutation(async ({ ctx, input }) => {
|
||
// Create MentorFileComment
|
||
// Parse @mentions and create notifications
|
||
return comment
|
||
}),
|
||
|
||
listComments: mentorOrTeamProcedure
|
||
.input(z.object({
|
||
mentorFileId: z.string(),
|
||
}))
|
||
.query(async ({ ctx, input }) => {
|
||
// Return threaded comments (with nested replies)
|
||
return { comments }
|
||
}),
|
||
|
||
deleteComment: mentorOrTeamProcedure
|
||
.input(z.object({
|
||
commentId: z.string(),
|
||
}))
|
||
.mutation(async ({ ctx, input }) => {
|
||
// Only author or admin can delete
|
||
// Replace with "[Comment deleted]" placeholder
|
||
return { success: true }
|
||
}),
|
||
|
||
// File Promotion
|
||
promoteFile: protectedProcedure
|
||
.input(z.object({
|
||
mentorFileId: z.string(),
|
||
submissionWindowId: z.string(),
|
||
requirementId: z.string().optional(),
|
||
}))
|
||
.mutation(async ({ ctx, input }) => {
|
||
// Validate permissions (team lead or admin)
|
||
// Execute promotion (create ProjectFile, update MentorFile)
|
||
// Audit log
|
||
return { mentorFile, projectFile }
|
||
}),
|
||
|
||
unpromoteFile: adminProcedure
|
||
.input(z.object({
|
||
mentorFileId: z.string(),
|
||
}))
|
||
.mutation(async ({ ctx, input }) => {
|
||
// Revert promotion (admin only)
|
||
return { success: true }
|
||
}),
|
||
|
||
// Workspace Status
|
||
getWorkspaceStatus: mentorOrTeamProcedure
|
||
.input(z.object({
|
||
mentorAssignmentId: z.string(),
|
||
}))
|
||
.query(async ({ ctx, input }) => {
|
||
// Summary stats for workspace
|
||
return {
|
||
fileCount,
|
||
unreadMessageCount,
|
||
milestonesCompleted,
|
||
milestonesTotal,
|
||
lastActivity,
|
||
}
|
||
}),
|
||
})
|
||
```
|
||
|
||
### Authorization Helper
|
||
|
||
```typescript
|
||
// Check if user can access workspace
|
||
async function canAccessWorkspace(
|
||
userId: string,
|
||
mentorAssignmentId: string,
|
||
prisma: PrismaClient
|
||
): Promise<boolean> {
|
||
const assignment = await prisma.mentorAssignment.findUnique({
|
||
where: { id: mentorAssignmentId },
|
||
include: {
|
||
mentor: true,
|
||
project: {
|
||
include: {
|
||
teamMembers: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!assignment) return false
|
||
|
||
// Mentor
|
||
if (assignment.mentorId === userId) return true
|
||
|
||
// Team lead
|
||
const teamLead = assignment.project.teamMembers.find(
|
||
(tm) => tm.role === 'LEAD'
|
||
)
|
||
if (teamLead?.userId === userId) return true
|
||
|
||
// Any team member (for read access)
|
||
const isTeamMember = assignment.project.teamMembers.some(
|
||
(tm) => tm.userId === userId
|
||
)
|
||
if (isTeamMember) return true
|
||
|
||
// Admin (checked in middleware)
|
||
return false
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 15. Service Layer
|
||
|
||
### New Service: `mentor-workspace.ts`
|
||
|
||
```typescript
|
||
// src/server/services/mentor-workspace.ts
|
||
|
||
import { PrismaClient, MentorFile, ProjectFile } from '@prisma/client'
|
||
import { generatePresignedUrl } from './minio-client'
|
||
import { logAudit } from '../utils/audit'
|
||
|
||
/**
|
||
* Generate MinIO pre-signed PUT URL for workspace file upload
|
||
*/
|
||
export async function getWorkspaceUploadUrl(
|
||
mentorAssignmentId: string,
|
||
fileName: string,
|
||
mimeType: string,
|
||
actorId: string,
|
||
prisma: PrismaClient
|
||
): Promise<{ uploadUrl: string; objectKey: string }> {
|
||
// Validate workspace is active
|
||
const assignment = await prisma.mentorAssignment.findUniqueOrThrow({
|
||
where: { id: mentorAssignmentId },
|
||
})
|
||
|
||
if (!assignment.workspaceEnabled) {
|
||
throw new Error('Workspace is not enabled for this assignment')
|
||
}
|
||
|
||
const now = new Date()
|
||
if (
|
||
assignment.workspaceOpenAt &&
|
||
assignment.workspaceCloseAt &&
|
||
(now < assignment.workspaceOpenAt || now > assignment.workspaceCloseAt)
|
||
) {
|
||
throw new Error('Workspace is not currently open')
|
||
}
|
||
|
||
// Generate object key
|
||
const objectKey = `mentor-workspace/${mentorAssignmentId}/${Date.now()}-${fileName}`
|
||
|
||
// Generate pre-signed PUT URL (15-minute expiry)
|
||
const uploadUrl = await generatePresignedUrl(
|
||
process.env.MINIO_BUCKET!,
|
||
objectKey,
|
||
'PUT',
|
||
15 * 60,
|
||
mimeType
|
||
)
|
||
|
||
return { uploadUrl, objectKey }
|
||
}
|
||
|
||
/**
|
||
* Save file metadata after upload
|
||
*/
|
||
export async function saveWorkspaceFile(
|
||
mentorAssignmentId: string,
|
||
uploadedByUserId: string,
|
||
file: {
|
||
fileName: string
|
||
mimeType: string
|
||
size: number
|
||
bucket: string
|
||
objectKey: string
|
||
},
|
||
description: string | null,
|
||
prisma: PrismaClient
|
||
): Promise<MentorFile> {
|
||
const mentorFile = await prisma.mentorFile.create({
|
||
data: {
|
||
mentorAssignmentId,
|
||
uploadedByUserId,
|
||
fileName: file.fileName,
|
||
mimeType: file.mimeType,
|
||
size: file.size,
|
||
bucket: file.bucket,
|
||
objectKey: file.objectKey,
|
||
description,
|
||
},
|
||
})
|
||
|
||
// Audit log
|
||
await logAudit(prisma, {
|
||
action: 'MENTOR_FILE_UPLOADED',
|
||
actorId: uploadedByUserId,
|
||
targetType: 'MENTOR_FILE',
|
||
targetId: mentorFile.id,
|
||
metadata: {
|
||
fileName: file.fileName,
|
||
size: file.size,
|
||
mentorAssignmentId,
|
||
},
|
||
})
|
||
|
||
return mentorFile
|
||
}
|
||
|
||
/**
|
||
* Promote workspace file to official submission
|
||
*/
|
||
export async function promoteFileToSubmission(
|
||
mentorFileId: string,
|
||
submissionWindowId: string,
|
||
requirementId: string | null,
|
||
actorId: string,
|
||
prisma: PrismaClient
|
||
): Promise<{ mentorFile: MentorFile; projectFile: ProjectFile }> {
|
||
// 1. Validate mentor file
|
||
const mentorFile = await prisma.mentorFile.findUniqueOrThrow({
|
||
where: { id: mentorFileId },
|
||
include: {
|
||
mentorAssignment: {
|
||
include: {
|
||
project: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (mentorFile.isPromoted) {
|
||
throw new Error('This file has already been promoted')
|
||
}
|
||
|
||
// 2. Validate workspace is active
|
||
const { mentorAssignment } = mentorFile
|
||
const now = new Date()
|
||
if (
|
||
!mentorAssignment.workspaceEnabled ||
|
||
(mentorAssignment.workspaceCloseAt && now > mentorAssignment.workspaceCloseAt)
|
||
) {
|
||
throw new Error('Workspace is closed — cannot promote files')
|
||
}
|
||
|
||
// 3. Find existing file in target slot (if requirementId provided)
|
||
let replacedFile: ProjectFile | null = null
|
||
if (requirementId) {
|
||
replacedFile = await prisma.projectFile.findFirst({
|
||
where: {
|
||
projectId: mentorAssignment.projectId,
|
||
requirementId,
|
||
replacedById: null,
|
||
},
|
||
})
|
||
}
|
||
|
||
// 4. Determine version number
|
||
const existingVersions = await prisma.projectFile.findMany({
|
||
where: {
|
||
projectId: mentorAssignment.projectId,
|
||
submissionWindowId,
|
||
requirementId,
|
||
},
|
||
orderBy: { version: 'desc' },
|
||
take: 1,
|
||
})
|
||
const nextVersion = existingVersions.length > 0 ? existingVersions[0].version + 1 : 1
|
||
|
||
// 5. Create new ProjectFile (reusing same MinIO object)
|
||
const projectFile = await prisma.projectFile.create({
|
||
data: {
|
||
projectId: mentorAssignment.projectId,
|
||
submissionWindowId,
|
||
requirementId,
|
||
fileType: 'SUBMISSION',
|
||
fileName: mentorFile.fileName,
|
||
mimeType: mentorFile.mimeType,
|
||
size: mentorFile.size,
|
||
bucket: mentorFile.bucket,
|
||
objectKey: mentorFile.objectKey, // NO DUPLICATION
|
||
version: nextVersion,
|
||
isLate: false,
|
||
},
|
||
})
|
||
|
||
// 6. Update old file if it exists
|
||
if (replacedFile) {
|
||
await prisma.projectFile.update({
|
||
where: { id: replacedFile.id },
|
||
data: { replacedById: projectFile.id },
|
||
})
|
||
}
|
||
|
||
// 7. Update MentorFile
|
||
const updatedMentorFile = await prisma.mentorFile.update({
|
||
where: { id: mentorFileId },
|
||
data: {
|
||
isPromoted: true,
|
||
promotedToFileId: projectFile.id,
|
||
promotedAt: now,
|
||
promotedByUserId: actorId,
|
||
},
|
||
})
|
||
|
||
// 8. Audit log
|
||
await logAudit(prisma, {
|
||
action: 'MENTOR_FILE_PROMOTED',
|
||
actorId,
|
||
targetType: 'PROJECT_FILE',
|
||
targetId: projectFile.id,
|
||
metadata: {
|
||
mentorFileId,
|
||
submissionWindowId,
|
||
requirementId,
|
||
replacedFileId: replacedFile?.id,
|
||
version: nextVersion,
|
||
},
|
||
})
|
||
|
||
return { mentorFile: updatedMentorFile, projectFile }
|
||
}
|
||
|
||
/**
|
||
* Revert promotion (admin only)
|
||
*/
|
||
export async function unpromoteFile(
|
||
mentorFileId: string,
|
||
actorId: string,
|
||
prisma: PrismaClient
|
||
): Promise<void> {
|
||
const mentorFile = await prisma.mentorFile.findUniqueOrThrow({
|
||
where: { id: mentorFileId },
|
||
include: { promotedFile: true },
|
||
})
|
||
|
||
if (!mentorFile.isPromoted || !mentorFile.promotedFile) {
|
||
throw new Error('File is not promoted')
|
||
}
|
||
|
||
const promotedFile = mentorFile.promotedFile
|
||
|
||
// 1. If promoted file replaced a previous file, restore that file
|
||
if (promotedFile.replacedById) {
|
||
const previousFile = await prisma.projectFile.findUnique({
|
||
where: { replacedById: promotedFile.id },
|
||
})
|
||
if (previousFile) {
|
||
await prisma.projectFile.update({
|
||
where: { id: previousFile.id },
|
||
data: { replacedById: null },
|
||
})
|
||
}
|
||
}
|
||
|
||
// 2. Delete the promoted ProjectFile
|
||
await prisma.projectFile.delete({
|
||
where: { id: promotedFile.id },
|
||
})
|
||
|
||
// 3. Reset MentorFile flags
|
||
await prisma.mentorFile.update({
|
||
where: { id: mentorFileId },
|
||
data: {
|
||
isPromoted: false,
|
||
promotedToFileId: null,
|
||
promotedAt: null,
|
||
promotedByUserId: null,
|
||
},
|
||
})
|
||
|
||
// 4. Audit log
|
||
await logAudit(prisma, {
|
||
action: 'MENTOR_FILE_UNPROMOTED',
|
||
actorId,
|
||
targetType: 'MENTOR_FILE',
|
||
targetId: mentorFileId,
|
||
metadata: {
|
||
deletedProjectFileId: promotedFile.id,
|
||
},
|
||
})
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 16. Edge Cases & Error Handling
|
||
|
||
| Scenario | Handling |
|
||
|----------|----------|
|
||
| **Team doesn't want mentoring but admin assigns anyway** | Assignment created; team sees mentor in dashboard with explanation |
|
||
| **Mentor goes inactive during period** | Admin can reassign; previous workspace preserved (read-only for old mentor) |
|
||
| **File promoted then mentor period closes** | Promoted file remains as official submission; MentorFile remains in workspace |
|
||
| **Team tries to promote file for requirement that doesn't exist** | Error — must select valid requirement or leave requirementId null |
|
||
| **Two files promoted to same requirement slot** | Second promotion replaces first (versioning applies) |
|
||
| **Mentoring file is larger than requirement maxSizeMB** | Warning shown but promotion allowed (admin override implicit) |
|
||
| **Workspace closed but team needs one more upload** | Admin can extend via round window adjustment or grant grace period |
|
||
| **Promoted file deleted from workspace** | ProjectFile remains (separate record); MentorFile keeps audit trail |
|
||
| **Mentor uploads file then leaves program** | Files remain accessible in workspace; admin can reassign mentor |
|
||
| **File upload fails mid-way** | MinIO object exists but no MentorFile record → cleanup job deletes orphaned objects weekly |
|
||
| **Comment with @mention for user not in workspace** | @mention rendered as plain text, no notification sent |
|
||
| **Workspace accessed after mentoring round ends** | Read-only mode — can view files and messages, can't upload or comment |
|
||
|
||
---
|
||
|
||
## 17. Performance Considerations
|
||
|
||
### File Upload Optimization
|
||
|
||
- **Direct MinIO upload** — Files never touch the Next.js server
|
||
- **Pre-signed URLs** — 15-minute expiry, client uploads directly
|
||
- **Chunked upload** — For files > 5 MB, use multipart upload
|
||
- **Progress feedback** — Real-time upload progress bar
|
||
|
||
### File List Performance
|
||
|
||
- **Pagination** — Load 20 files at a time (infinite scroll)
|
||
- **Comment count caching** — Denormalized comment count on MentorFile
|
||
- **Thumbnail generation** — For images, generate thumbnails on upload
|
||
|
||
### Real-time Updates
|
||
|
||
- **Polling strategy** — Poll every 10s for new messages, 30s for file list
|
||
- **WebSocket upgrade** — If available, use WebSocket for real-time updates
|
||
- **Optimistic UI** — Show uploaded file immediately, sync in background
|
||
|
||
---
|
||
|
||
## 18. Security
|
||
|
||
### Access Control
|
||
|
||
```typescript
|
||
// Every workspace procedure checks:
|
||
1. User is authenticated
|
||
2. User is mentor OR team member OR admin
|
||
3. Workspace is active (or admin override)
|
||
```
|
||
|
||
### File Storage Security
|
||
|
||
- **Private bucket** — All workspace files in private MinIO bucket
|
||
- **Pre-signed URLs** — Expire after 15 minutes
|
||
- **No directory listing** — Can't enumerate workspace files without database access
|
||
- **Audit trail** — Every file upload, download, promotion logged
|
||
|
||
### XSS Prevention
|
||
|
||
- **Sanitize filenames** — Strip HTML, escape special chars
|
||
- **Sanitize comments** — Plain text only, no HTML tags
|
||
- **Validate MIME types** — Only allow whitelisted file types
|
||
|
||
---
|
||
|
||
## 19. Testing Strategy
|
||
|
||
### Unit Tests
|
||
|
||
```typescript
|
||
// Test file promotion logic
|
||
describe('promoteFileToSubmission', () => {
|
||
it('should create ProjectFile with same objectKey', async () => {
|
||
const { mentorFile, projectFile } = await promoteFileToSubmission(...)
|
||
expect(projectFile.objectKey).toBe(mentorFile.objectKey)
|
||
expect(mentorFile.isPromoted).toBe(true)
|
||
})
|
||
|
||
it('should increment version number', async () => {
|
||
// Upload file v1
|
||
// Promote to create ProjectFile v1
|
||
// Upload file v2
|
||
// Promote to create ProjectFile v2
|
||
expect(projectFile.version).toBe(2)
|
||
})
|
||
|
||
it('should replace previous file in slot', async () => {
|
||
// Promote file1 to slot A
|
||
// Promote file2 to slot A
|
||
expect(file1.replacedById).toBe(file2.id)
|
||
})
|
||
})
|
||
```
|
||
|
||
### Integration Tests
|
||
|
||
```typescript
|
||
// Test full workspace flow
|
||
describe('Workspace Integration', () => {
|
||
it('should allow mentor to upload, team to comment, and promote', async () => {
|
||
// 1. Mentor uploads file
|
||
const uploadUrl = await trpc.mentorWorkspace.getUploadUrl(...)
|
||
// Upload to MinIO
|
||
const file = await trpc.mentorWorkspace.saveFile(...)
|
||
|
||
// 2. Team adds comment
|
||
const comment = await trpc.mentorWorkspace.addComment(...)
|
||
expect(comment.authorId).toBe(teamMemberId)
|
||
|
||
// 3. Promote file
|
||
const { projectFile } = await trpc.mentorWorkspace.promoteFile(...)
|
||
expect(projectFile.projectId).toBe(project.id)
|
||
})
|
||
})
|
||
```
|
||
|
||
### E2E Tests (Playwright)
|
||
|
||
```typescript
|
||
// Test workspace UI
|
||
test('Mentor can upload file and view comments', async ({ page }) => {
|
||
await page.goto('/mentor/team/project-123/files')
|
||
|
||
// Upload file
|
||
await page.click('text=Upload File')
|
||
await page.setInputFiles('input[type=file]', 'test-file.pdf')
|
||
await page.click('text=Upload')
|
||
|
||
// Wait for file to appear
|
||
await page.waitForSelector('text=test-file.pdf')
|
||
|
||
// Open file detail
|
||
await page.click('text=test-file.pdf')
|
||
|
||
// Add comment
|
||
await page.fill('textarea[placeholder="Add comment..."]', 'Looks good!')
|
||
await page.click('text=Post Comment')
|
||
|
||
// Verify comment appears
|
||
await page.waitForSelector('text=Looks good!')
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 20. Rollout Plan
|
||
|
||
### Phase 1: Foundation (Week 1-2)
|
||
|
||
- [ ] Create data models (MentorFile, MentorFileComment)
|
||
- [ ] Migration script
|
||
- [ ] Basic tRPC procedures (upload, list, download)
|
||
- [ ] Service layer (`mentor-workspace.ts`)
|
||
|
||
### Phase 2: Core UI (Week 3-4)
|
||
|
||
- [ ] Mentor dashboard
|
||
- [ ] Workspace layout (split-pane)
|
||
- [ ] File list component
|
||
- [ ] File detail component
|
||
- [ ] File upload zone
|
||
|
||
### Phase 3: Comments & Chat (Week 5)
|
||
|
||
- [ ] Comment thread component
|
||
- [ ] @mention support
|
||
- [ ] Enhanced chat integration
|
||
|
||
### Phase 4: File Promotion (Week 6)
|
||
|
||
- [ ] Promotion dialog
|
||
- [ ] Promotion service logic
|
||
- [ ] Unpromote (admin override)
|
||
- [ ] Audit trail
|
||
|
||
### Phase 5: Admin Tools (Week 7)
|
||
|
||
- [ ] Admin monitoring dashboard
|
||
- [ ] Reassignment flow
|
||
- [ ] Workspace export
|
||
- [ ] Activity alerts
|
||
|
||
### Phase 6: Polish & Mobile (Week 8)
|
||
|
||
- [ ] Mobile responsive design
|
||
- [ ] Accessibility audit
|
||
- [ ] Performance optimization
|
||
- [ ] E2E tests
|
||
|
||
---
|
||
|
||
## 21. Success Metrics
|
||
|
||
| Metric | Target |
|
||
|--------|--------|
|
||
| **Mentor engagement** | 80%+ of mentors upload at least 1 file |
|
||
| **Team engagement** | 90%+ of finalist teams engage in workspace |
|
||
| **File promotion rate** | 50%+ of workspace files promoted to submission |
|
||
| **Comment activity** | Average 3+ comments per promoted file |
|
||
| **Milestone completion** | 70%+ of mentors complete all milestones |
|
||
| **Mobile usage** | 30%+ of workspace access from mobile/tablet |
|
||
| **Time to first file upload** | < 24 hours after mentoring round opens |
|
||
| **Admin intervention rate** | < 10% of assignments require admin reassignment |
|
||
|
||
---
|
||
|
||
## 22. Future Enhancements (Post-MVP)
|
||
|
||
### Video Integration
|
||
|
||
- Loom/Zoom integration for mentor video feedback
|
||
- Record screen share reviews of files
|
||
|
||
### Real-time Collaboration
|
||
|
||
- Live document editing (Google Docs-style)
|
||
- Shared whiteboard for brainstorming
|
||
|
||
### AI-Powered Features
|
||
|
||
- AI-generated file summaries
|
||
- Suggested comments based on file content
|
||
- Auto-tagging files by topic
|
||
|
||
### Analytics Dashboard
|
||
|
||
- Mentor performance metrics
|
||
- Team engagement heatmaps
|
||
- File promotion success rates
|
||
|
||
### Templates Library
|
||
|
||
- Mentor-created templates (financial models, pitch decks)
|
||
- Template marketplace
|
||
|
||
---
|
||
|
||
## Document Complete
|
||
|
||
This document provides a comprehensive blueprint for the Mentor UI redesign. It covers:
|
||
|
||
- Current baseline (existing mentor features)
|
||
- Dashboard design (landing page with team cards, metrics, activity feed)
|
||
- Workspace architecture (split-pane file management)
|
||
- File review & comments (threaded discussion)
|
||
- File promotion (workspace → official submission)
|
||
- Messaging integration
|
||
- Milestones tracking
|
||
- Navigation & IA
|
||
- Admin monitoring
|
||
- Mobile experience
|
||
- Accessibility
|
||
- Component library
|
||
- API reference
|
||
- Service layer
|
||
- Edge cases & security
|
||
- Testing strategy
|
||
- Rollout plan
|
||
- Success metrics
|
||
|
||
The redesigned mentor experience transforms mentoring from basic messaging into a full collaboration workspace with file management, threaded comments, and seamless integration with the submission system.
|