import { useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { BellRing, CheckCircle2, Filter, LoaderCircle, ShieldAlert, SlidersHorizontal, Sparkles, } from "lucide-react"; import { aiPmoApi } from "@/api/aiPmo"; import { notificationsApi } from "@/api/notifications"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useToast } from "@/components/ui/toast"; import { buildInboxView, getInboxSectionLabel } from "@/lib/inbox"; import { getApiErrorMessage } from "@/lib/apiError"; import { cn } from "@/lib/utils"; import { toZhPriority } from "@/lib/display"; import { parseBackendDate } from "@/lib/datetime"; import type { InboxActionDescriptor, InboxDisplayKind, InboxItemViewModel, InboxSectionKey } from "@/types/inbox"; const POLL_INTERVAL_MS = 15000; const STATUS_LABELS: Record = { action_required: "需你决策", auto_running: "AI 已接手", auto_done: "AI 执行完成", info: "仅通知", }; const STATUS_STYLES: Record = { action_required: "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300", auto_running: "border-sky-500/40 bg-sky-500/10 text-sky-700 dark:text-sky-300", auto_done: "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", info: "border-slate-400/40 bg-slate-500/10 text-slate-700 dark:text-slate-300", }; const METRIC_CARD_STYLES = [ "bg-blue-50 text-blue-700", "bg-sky-50 text-sky-700", "bg-emerald-50 text-emerald-700", "bg-amber-50 text-amber-700", "bg-violet-50 text-violet-700", ]; const PRIORITY_OPTIONS = [ { value: "all", label: "全部优先级" }, { value: "high", label: "高优先级" }, { value: "medium", label: "中优先级" }, { value: "low", label: "低优先级" }, ]; function matchesPriority(item: InboxItemViewModel, priorityFilter: string): boolean { if (priorityFilter === "all") return true; if (priorityFilter === "high") return item.priority_rank >= 3; if (priorityFilter === "medium") return item.priority_rank === 2; return item.priority_rank <= 1; } function buildScopedSections(items: InboxItemViewModel[]) { const sectionKeys: InboxSectionKey[] = ["all", "action_required", "auto_running", "auto_done", "info"]; return sectionKeys.map((key) => ({ key, label: getInboxSectionLabel(key), count: key === "all" ? items.length : items.filter((item) => item.display_kind === key).length, })); } function isToday(value?: string | null) { const date = parseBackendDate(value); if (!date) return false; const now = new Date(); return ( date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ); } function formatDetailTime(value?: string | null) { const date = parseBackendDate(value); if (!date) return value || "-"; return date.toLocaleString("zh-CN", { hour12: false }); } function formatJsonBlock(value: unknown): string { if (!value) return "暂无"; try { return JSON.stringify(value, null, 2); } catch { return String(value); } } function buildActionKey(item: InboxItemViewModel, action: InboxActionDescriptor): string { return `${item.id}:${action.kind}:${action.label}`; } function renderMiniList(title: string, description: string, items: InboxItemViewModel[], onOpen: (item: InboxItemViewModel) => void) { return ( {title} {description} {items.length === 0 ? (
当前为空
) : ( items.map((item) => ( )) )}
); } function InboxCard({ item, onAction, pendingActionKey, }: { item: InboxItemViewModel; onAction: (item: InboxItemViewModel, action: InboxActionDescriptor) => void; pendingActionKey: string | null; }) { const stageCards = [ { label: "当前阶段", value: item.stage_text || STATUS_LABELS[item.display_kind] }, { label: "预计下一步", value: item.next_step_text || item.ai_plan || "-" }, { label: "撤销状态", value: item.can_undo ? `可撤销,截止 ${item.undo_deadline || "-"}` : "不可撤销" }, ]; const primaryActionKey = buildActionKey(item, item.primary_action); const secondaryActionKey = buildActionKey(item, item.secondary_action); const weakActionKey = buildActionKey(item, item.weak_action); const primaryPending = pendingActionKey === primaryActionKey; const secondaryPending = pendingActionKey === secondaryActionKey; const weakPending = pendingActionKey === weakActionKey; return (
{STATUS_LABELS[item.display_kind]} {toZhPriority(item.priority)} {item.project_id && ( {item.project_name || `项目 #${item.project_id}`} )} {item.dedupe_count > 1 && ( 重复 {item.dedupe_count} 次 )}
{item.updated_at || item.created_at || "-"}
{item.title} {item.summary}
{item.ai_plan && (
AI 已做/将做什么

{item.ai_plan}

)} {item.blocking_reason && (
为什么需要你

{item.blocking_reason}

)} {(item.stage_text || item.next_step_text || item.can_undo) && (
{stageCards.map((stage) => (
{stage.label}

{stage.value}

))}
)}
{item.weak_action.kind === "none" ? ( {item.weak_action.label} ) : ( )}
); } export default function InboxPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const { toast } = useToast(); const [searchParams, setSearchParams] = useSearchParams(); const [notificationDetailItem, setNotificationDetailItem] = useState(null); const [workItemDetailItem, setWorkItemDetailItem] = useState(null); const [pendingActionKey, setPendingActionKey] = useState(null); const [priorityFilter, setPriorityFilter] = useState("all"); const [onlyDecision, setOnlyDecision] = useState(false); const [onlyUnread, setOnlyUnread] = useState(false); const activeSection = (searchParams.get("section") as InboxSectionKey) || "all"; const projectFilter = searchParams.get("project_id") || "all"; const searchValue = searchParams.get("q") || ""; const notificationsQuery = useQuery({ queryKey: ["notifications", "inbox"], queryFn: () => notificationsApi.list({ limit: 60 }), refetchInterval: POLL_INTERVAL_MS, }); const workItemsQuery = useQuery({ queryKey: ["ai-pmo-work-items", "inbox"], queryFn: () => aiPmoApi.listWorkItems({ limit: 60 }), refetchInterval: POLL_INTERVAL_MS, }); const workItemDetailQuery = useQuery({ queryKey: ["ai-pmo-work-item-detail", workItemDetailItem?.source_work_item?.id || null], queryFn: () => aiPmoApi.getWorkItem(Number(workItemDetailItem?.source_work_item?.id)), enabled: Boolean(workItemDetailItem?.source_work_item?.id), }); const inboxData = useMemo(() => { return buildInboxView({ notifications: notificationsQuery.data || { items: [], unread_count: 0 }, workItems: workItemsQuery.data || { items: [] }, }); }, [notificationsQuery.data, workItemsQuery.data]); const projectOptions = useMemo(() => { const map = new Map(); for (const item of inboxData.items) { if (!item.project_id) continue; map.set(String(item.project_id), item.project_name || `项目 #${item.project_id}`); } return Array.from(map.entries()).map(([value, label]) => ({ value, label })); }, [inboxData.items]); const projectScopedItems = useMemo(() => { return inboxData.items.filter((item) => { if (projectFilter === "all") return true; return String(item.project_id || "") === projectFilter; }); }, [inboxData.items, projectFilter]); const filteredItems = useMemo(() => { const query = searchValue.trim().toLowerCase(); return projectScopedItems.filter((item) => { if (activeSection !== "all" && item.display_kind !== activeSection) return false; if (onlyDecision && item.display_kind !== "action_required") return false; if (onlyUnread && item.is_read) return false; if (!matchesPriority(item, priorityFilter)) return false; if (!query) return true; return `${item.title} ${item.summary} ${item.ai_plan || ""} ${item.project_name || ""}`.toLowerCase().includes(query); }); }, [activeSection, onlyDecision, onlyUnread, priorityFilter, projectScopedItems, searchValue]); const scopedSections = useMemo(() => buildScopedSections(projectScopedItems), [projectScopedItems]); const scopedLeaderSummary = useMemo(() => { return { decision_count: projectScopedItems.filter((item) => item.display_kind === "action_required").length, auto_running_count: projectScopedItems.filter((item) => item.display_kind === "auto_running").length, auto_done_today: projectScopedItems.filter( (item) => item.display_kind === "auto_done" && isToday(item.updated_at || item.created_at) ).length, high_priority_blocked_count: projectScopedItems.filter( (item) => item.display_kind === "action_required" && item.priority_rank >= 3 && Boolean(item.blocking_reason) ).length, external_authorization_count: projectScopedItems.filter( (item) => item.display_kind === "action_required" && /(github|gitlab|jira|接入|授权|跨系统|外部系统)/i.test(`${item.title} ${item.summary} ${item.blocking_reason || ""}`) ).length, top_decisions: projectScopedItems.filter((item) => item.display_kind === "action_required").slice(0, 3), recent_completions: projectScopedItems.filter((item) => item.display_kind === "auto_done").slice(0, 3), }; }, [projectScopedItems]); const setSection = (section: string) => { const next = new URLSearchParams(searchParams); next.set("section", section); setSearchParams(next); }; const setProject = (projectId: string) => { const next = new URLSearchParams(searchParams); if (projectId === "all") next.delete("project_id"); else next.set("project_id", projectId); setSearchParams(next); }; const setSearch = (value: string) => { const next = new URLSearchParams(searchParams); if (!value.trim()) next.delete("q"); else next.set("q", value.trim()); setSearchParams(next); }; const refreshAll = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["notifications", "inbox"] }), queryClient.invalidateQueries({ queryKey: ["ai-pmo-work-items", "inbox"] }), ]); }; const markNotificationRead = async (item: InboxItemViewModel) => { if (item.source_kind === "notification" && item.source_notification && !item.source_notification.is_read) { try { await notificationsApi.markRead(Number(item.source_notification.id)); } catch { // 不阻断后续跳转。 } } }; const openNotificationDetail = async (item: InboxItemViewModel) => { await markNotificationRead(item); setNotificationDetailItem(item); await refreshAll(); }; const openWorkItemDetail = async (item: InboxItemViewModel) => { setWorkItemDetailItem(item); await refreshAll(); }; const openTarget = async (item: InboxItemViewModel, options?: { forceDirect?: boolean }) => { if (!options?.forceDirect && item.source_kind === "notification" && item.display_kind === "info") { await openNotificationDetail(item); return; } await markNotificationRead(item); if (item.target_url) { if (item.target_url.startsWith("/")) navigate(item.target_url); else window.location.assign(item.target_url); await refreshAll(); return; } if (item.source_kind === "ai_pmo_work_item") { await openWorkItemDetail(item); return; } toast({ title: "当前卡片暂无可打开的详情入口", variant: "error" }); }; const handleAction = async (item: InboxItemViewModel, action: InboxActionDescriptor) => { const actionKey = buildActionKey(item, action); setPendingActionKey(actionKey); try { if (item.source_kind === "notification" && action.label === "查看详情") { await openNotificationDetail(item); return; } if (action.kind === "open_target") { await openTarget(item); return; } if (action.kind === "open_detail") { if (item.source_kind === "notification") { await openNotificationDetail(item); return; } await openWorkItemDetail(item); return; } if (action.kind === "confirm" && item.source_kind === "ai_pmo_work_item" && item.source_work_item) { const result = await aiPmoApi.decideWorkItem(item.source_work_item.id, { action: "approve", project_id: item.project_id ?? undefined, }); await refreshAll(); const followupReply = String(result.followup_reply || "").trim(); if (result.status === "succeeded") { toast({ title: followupReply || "AI PMO 已继续执行并完成该工作项", variant: "success" }); } else if (result.status === "pending_manual") { toast({ title: followupReply || "继续执行后仍需补充信息或人工确认", variant: "default" }); } else { toast({ title: followupReply || "继续执行失败,请查看详情", variant: "error" }); } setWorkItemDetailItem({ ...item, display_kind: result.status === "succeeded" ? "auto_done" : result.status === "pending_manual" ? "action_required" : "info", stage_text: String(result.target_state || item.stage_text || "").trim() || item.stage_text, next_step_text: String(result.followup_reply || item.next_step_text || "").trim() || item.next_step_text, }); return; } if (action.kind === "reject" && item.source_kind === "ai_pmo_work_item" && item.source_work_item) { const result = await aiPmoApi.decideWorkItem(item.source_work_item.id, { action: "reject", reason: "人工审批拒绝,已标记失效", }); await refreshAll(); const followupReply = String(result.followup_reply || "").trim(); toast({ title: followupReply || "已标记失效,不再继续执行", variant: "default" }); if (workItemDetailItem?.id === item.id) { setWorkItemDetailItem(null); } return; } if (action.kind === "mark_read" && item.source_kind === "notification" && item.source_notification) { await notificationsApi.markRead(item.source_notification.id); await refreshAll(); return; } if (action.kind === "none") { if (item.source_kind === "ai_pmo_work_item") { await openWorkItemDetail(item); return; } toast({ title: action.label, variant: "default" }); } } catch (error) { toast({ title: getApiErrorMessage(error), variant: "error" }); } finally { setPendingActionKey((current) => (current === actionKey ? null : current)); } }; const markAllRead = async () => { try { await notificationsApi.markAllRead(); toast({ title: "站内通知已全部设为已读", variant: "success" }); await refreshAll(); } catch (error) { toast({ title: getApiErrorMessage(error, "全部已读失败"), variant: "error" }); } }; const isLoading = notificationsQuery.isLoading || workItemsQuery.isLoading; return ( <>

消息台

AI 决策收件箱 · 先看摘要,再处理真正需要你拍板的事项

当前结果:{filteredItems.length} 条 {projectFilter !== "all" ? `(已筛选项目 #${projectFilter})` : "(全租户汇总)"}

AI 决策收件箱

铃铛只看摘要,完整工作流统一沉到 Inbox

只保留真正可执行的事项;查询、闲聊、健康提示默认不进入消息台。
{[ { icon: ShieldAlert, label: "需你决策", value: scopedLeaderSummary.decision_count }, { icon: LoaderCircle, label: "AI处理中", value: scopedLeaderSummary.auto_running_count }, { icon: CheckCircle2, label: "今日自动完成", value: scopedLeaderSummary.auto_done_today }, { icon: Sparkles, label: "高优先级阻塞", value: scopedLeaderSummary.high_priority_blocked_count }, { icon: Filter, label: "待授权外部接入", value: scopedLeaderSummary.external_authorization_count }, ].map(({ icon: Icon, label, value }, index) => (
{label}
{value}
))}
{renderMiniList("最需要你拍板的 3 件事", "优先看高风险、上下文不完整或需要授权的事项。", scopedLeaderSummary.top_decisions, (item) => void handleAction(item, item.primary_action) )} {renderMiniList("AI 刚完成的 3 件事", "自动结果会优先给出产物和影响范围。", scopedLeaderSummary.recent_completions, (item) => void handleAction(item, item.primary_action) )}
setSearch(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") setSearch((event.target as HTMLInputElement).value); }} />
{scopedSections.map((section) => ( {section.label} {section.count} ))}
{isLoading ? (
加载消息台中…
) : filteredItems.length === 0 ? (

当前没有匹配收件项

请调整筛选条件后重试

) : (
{filteredItems.map((item) => ( void handleAction(target, action)} /> ))}
)}
{ if (!open) setWorkItemDetailItem(null); }} > {workItemDetailItem?.title || "工作项详情"} {workItemDetailItem?.project_name || (workItemDetailItem?.project_id ? `项目 #${workItemDetailItem.project_id}` : "未绑定项目")} {" · "} {formatDetailTime(workItemDetailItem?.updated_at || workItemDetailItem?.created_at)} {workItemDetailItem && (
{STATUS_LABELS[workItemDetailItem.display_kind]} {toZhPriority(workItemDetailItem.priority)} {workItemDetailItem.source_work_item?.intent && ( {workItemDetailItem.source_work_item.intent} )} {workItemDetailItem.source_work_item?.source_system && ( {workItemDetailItem.source_work_item.source_system} )}
工作项摘要

{workItemDetailItem.summary || "暂无摘要"}

{workItemDetailItem.source_work_item?.blocking_reason && (
阻塞原因

{workItemDetailItem.source_work_item.blocking_reason}

)} {workItemDetailItem.source_work_item?.ai_plan && (
AI 执行计划

{workItemDetailItem.source_work_item.ai_plan}

)}
当前阶段

{workItemDetailItem.source_work_item?.stage_text || STATUS_LABELS[workItemDetailItem.display_kind]}

下一步

{workItemDetailItem.source_work_item?.next_step_text || "暂无"}

{workItemDetailQuery.isLoading ? (
加载工作项详情中…
) : workItemDetailQuery.data ? ( <>
决策记录

{workItemDetailQuery.data.decision_records.length} 条

执行计划

{workItemDetailQuery.data.plan_bundles.length} 条

回执记录

{workItemDetailQuery.data.reply_receipts.length} 条

执行结果
                      {formatJsonBlock(workItemDetailQuery.data.execution_result)}
                    
复盘摘要
                      {formatJsonBlock(workItemDetailQuery.data.review_summary)}
                    
) : (
工作项详情加载失败,请稍后重试。
)}
)} {workItemDetailItem?.target_url && ( )}
{ if (!open) setNotificationDetailItem(null); }} > {notificationDetailItem?.title || "通知详情"} {notificationDetailItem?.project_name || (notificationDetailItem?.project_id ? `项目 #${notificationDetailItem.project_id}` : "未绑定项目")} {" · "} {formatDetailTime(notificationDetailItem?.updated_at || notificationDetailItem?.created_at)} {notificationDetailItem && (
{STATUS_LABELS[notificationDetailItem.display_kind]} {toZhPriority(notificationDetailItem.priority)} {notificationDetailItem.source_notification?.source && ( {notificationDetailItem.source_notification.source} )} {notificationDetailItem.source_work_item?.source_system && ( {notificationDetailItem.source_work_item.source_system} )}
通知摘要

{notificationDetailItem.source_notification?.content || notificationDetailItem.summary || "暂无摘要"}

{notificationDetailItem.ai_plan && (
AI 已做/将做什么

{notificationDetailItem.ai_plan}

)} {notificationDetailItem.blocking_reason && (
当前阻塞

{notificationDetailItem.blocking_reason}

)}
当前阶段

{notificationDetailItem.stage_text || STATUS_LABELS[notificationDetailItem.display_kind]}

建议下一步

{notificationDetailItem.next_step_text || "先确认通知内容,再进入结果页处理。"}

)} {notificationDetailItem?.target_url && ( )}
); }