diff --git a/apps/web/app/(dashboard)/inbox/page.tsx b/apps/web/app/(dashboard)/inbox/page.tsx index 25ed704e..bfac713f 100644 --- a/apps/web/app/(dashboard)/inbox/page.tsx +++ b/apps/web/app/(dashboard)/inbox/page.tsx @@ -1,10 +1,291 @@ -export default function InboxPage() { +"use client"; + +import { useState } from "react"; +import { + AlertCircle, + Bot, + CheckCircle2, + CircleDot, + GitPullRequest, + MessageSquare, + ArrowRightLeft, +} from "lucide-react"; +import type { InboxItem, InboxItemType, InboxSeverity } from "@multica/types"; + +// --------------------------------------------------------------------------- +// Mock data +// --------------------------------------------------------------------------- + +const MOCK_INBOX_ITEMS: InboxItem[] = [ + { + id: "inb_1", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "agent_blocked", + severity: "action_required", + issue_id: "iss_12", + title: "Agent Claude-1 is blocked on MUL-12", + body: "I need clarification on the authentication flow. The current OAuth implementation uses PKCE, but the design doc references a session-based approach. Which one should I follow?\n\nSpecifically:\n1. Should we keep the PKCE flow for the SPA?\n2. Is the session cookie approach only for the server-rendered pages?\n3. Should I implement both and let the client decide?\n\nBlocked on this decision before I can continue with the login page implementation.", + read: false, + archived: false, + created_at: "2026-03-21T05:32:00Z", + }, + { + id: "inb_2", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "review_requested", + severity: "action_required", + issue_id: "iss_8", + title: "PR #47: Add WebSocket reconnection logic", + body: "Agent Codex-1 has submitted a pull request for review.\n\n**Changes:**\n- Added exponential backoff for WebSocket reconnection\n- Max retry attempts configurable via env var\n- Added connection state to the store\n- Unit tests for reconnection logic\n\n**Files changed:** 6 files (+284, -12)\n\nThe agent notes that it chose exponential backoff over linear retry because of the bursty reconnection pattern observed in the daemon logs.", + read: false, + archived: false, + created_at: "2026-03-21T04:15:00Z", + }, + { + id: "inb_3", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "issue_assigned", + severity: "action_required", + issue_id: "iss_15", + title: "New issue assigned: Design the agent config UI", + body: "You've been assigned to MUL-15: Design the agent config UI.\n\nPriority: High\nCreated by: Bohan\n\nDescription:\nWe need a configuration panel where users can set up their local agents — select runtime type, set concurrency limits, and manage API keys. This should live in the Settings page for now.", + read: true, + archived: false, + created_at: "2026-03-21T02:40:00Z", + }, + { + id: "inb_4", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "agent_completed", + severity: "attention", + issue_id: "iss_6", + title: "Agent Claude-1 completed MUL-6: API error handling", + body: "The task has been completed and all acceptance criteria passed:\n\n✅ Standardized error response format\n✅ Added error codes enum\n✅ Middleware catches panics and returns 500\n✅ All existing tests still pass\n✅ 4 new test cases added\n\nPR #45 has been created and CI is green. Ready for your review when convenient.", + read: false, + archived: false, + created_at: "2026-03-20T22:10:00Z", + }, + { + id: "inb_5", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "mentioned", + severity: "attention", + issue_id: "iss_10", + title: "Yuzhen mentioned you in MUL-10", + body: "@jiayuan Can you take a look at the database schema for the knowledge base? I want to make sure the vector embeddings table is set up correctly before we start indexing.\n\nI'm thinking we should use pgvector with HNSW index for the similarity search. Thoughts?", + read: true, + archived: false, + created_at: "2026-03-20T18:30:00Z", + }, + { + id: "inb_6", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "status_change", + severity: "info", + issue_id: "iss_3", + title: "MUL-3 moved to Done", + body: "Issue \"Set up CI/CD pipeline\" has been moved from In Review to Done by Bohan.\n\nThe GitHub Actions workflow is now running on every push to main. Build, test, and lint checks are all configured.", + read: true, + archived: false, + created_at: "2026-03-20T15:00:00Z", + }, + { + id: "inb_7", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "status_change", + severity: "info", + issue_id: "iss_9", + title: "MUL-9 moved to In Progress", + body: "Agent Codex-1 has started working on \"Implement issue list API endpoint\".\n\nEstimated approach:\n1. Add sqlc queries for listing/filtering issues\n2. Implement Chi handler with pagination\n3. Add sorting by priority, status, created_at\n4. Write integration tests", + read: true, + archived: false, + created_at: "2026-03-20T12:45:00Z", + }, +]; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const severityOrder: Record = { + action_required: 0, + attention: 1, + info: 2, +}; + +const typeIcons: Record = { + agent_blocked: AlertCircle, + review_requested: GitPullRequest, + issue_assigned: CircleDot, + agent_completed: CheckCircle2, + mentioned: MessageSquare, + status_change: ArrowRightLeft, +}; + +const severityColors: Record = { + action_required: "text-red-500", + attention: "text-yellow-500", + info: "text-muted-foreground", +}; + +function timeAgo(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime(); + const minutes = Math.floor(diff / 60000); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +// --------------------------------------------------------------------------- +// Components +// --------------------------------------------------------------------------- + +function InboxListItem({ + item, + isSelected, + onClick, +}: { + item: InboxItem; + isSelected: boolean; + onClick: () => void; +}) { + const Icon = typeIcons[item.type]; + const colorClass = severityColors[item.severity]; + + return ( + + ); +} + +function InboxDetail({ item }: { item: InboxItem }) { + const Icon = typeIcons[item.type]; + const colorClass = severityColors[item.severity]; + + const severityLabel: Record = { + action_required: "Action required", + attention: "Needs attention", + info: "Info", + }; + return (
-

Inbox

-

- Your notifications and action items will appear here. -

+ {/* Header */} +
+ +
+

{item.title}

+
+ {severityLabel[item.severity]} + · + {timeAgo(item.created_at)} + {item.issue_id && ( + <> + · + {item.issue_id} + + )} +
+
+
+ + {/* Body */} + {item.body && ( +
+ {item.body} +
+ )} +
+ ); +} + +// --------------------------------------------------------------------------- +// Page +// --------------------------------------------------------------------------- + +export default function InboxPage() { + const sorted = [...MOCK_INBOX_ITEMS].sort( + (a, b) => + severityOrder[a.severity] - severityOrder[b.severity] || + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + + const [selectedId, setSelectedId] = useState(sorted[0]?.id ?? ""); + const selected = sorted.find((i) => i.id === selectedId) ?? null; + + return ( +
+ {/* Left column — inbox list */} +
+
+

Inbox

+ + {sorted.filter((i) => !i.read).length} + +
+
+ {sorted.map((item) => ( + setSelectedId(item.id)} + /> + ))} +
+
+ + {/* Right column — detail */} +
+ {selected ? ( + + ) : ( +
+ Select an item to view details +
+ )} +
); }