"use client"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useState } from "react"; import clsx from "clsx"; import { sendAppLog } from "@/components/app-runtime"; import { DeviceImportDraftManager } from "@/components/device-import-draft-manager"; import { clearNativeSessionSnapshot, currentAppLocation, isNativeBossApp, persistNativeSessionSnapshot, popAppHistoryEntry, resolveAppBackAction, } from "@/lib/boss-app-client"; import { getMasterAgentChatMenuItems } from "@/lib/master-agent-chat-menu"; import { extractApprovedTargetProjectIds, summarizeDispatchPlan, } from "@/lib/dispatch-plan-ui"; import type { Device, DeviceEnrollment, GoalItem, MasterIdentitySummary, Message, OtaUpdate, OtaUpdateLog, OpsRepairTicket, OpsRepairVerification, ProjectOrchestrationBackendState, OrchestrationBackendId, ThreadContextSnapshot, UserProfile, UserSettings, } from "@/lib/boss-data"; import type { ConversationItem } from "@/lib/boss-projections"; import { formatTimestampLabel } from "@/lib/boss-projections"; function formatClock(value: string) { return formatTimestampLabel(value); } function formatBytes(value?: number) { if (!value || value <= 0) return "未知大小"; if (value >= 1024 * 1024) return `${(value / 1024 / 1024).toFixed(2)} MB`; if (value >= 1024) return `${(value / 1024).toFixed(1)} KB`; return `${value} B`; } function boundDeviceIdFromDom() { return document.body.dataset.boundDeviceId || "mac-studio"; } async function waitForLoginSessionReady(nativeClient: boolean) { for (let attempt = 0; attempt < 5; attempt += 1) { const response = await fetch("/api/auth/session", { cache: "no-store", headers: nativeClient ? { "x-boss-native-app": "1" } : undefined, }).catch(() => null); if (response?.ok) { return true; } await new Promise((resolve) => window.setTimeout(resolve, 120)); } return false; } function navigateToConversations(router: ReturnType) { router.replace("/conversations", { scroll: false }); router.refresh(); window.setTimeout(() => { if (window.location.pathname !== "/conversations") { window.location.replace("/conversations"); } }, 180); } export function AppShell({ children, bottomNav = true, }: { children: React.ReactNode; bottomNav?: boolean; }) { return (
{children}
{bottomNav ? : null}
); } export function StatusBar() { return
; } export function PageNav({ title, backHref, rightLabel, rightHref, rightNode, }: { title: string; backHref?: string; rightLabel?: string; rightHref?: string; rightNode?: React.ReactNode; }) { const router = useRouter(); async function handleBack() { const currentPath = currentAppLocation(); const action = resolveAppBackAction(currentPath, backHref); if (action.mode === "history") { popAppHistoryEntry(currentPath); router.back(); return; } if (action.mode === "replace") { popAppHistoryEntry(currentPath); router.replace(action.target, { scroll: false }); } } return (
{backHref ? ( ) : ( )}

{title}

{rightNode ? ( rightNode ) : rightLabel ? ( {rightLabel} ) : ( )}
); } export function BottomNav() { const pathname = usePathname(); const items = [ { href: "/conversations", label: "会话", icon: "◌" }, { href: "/devices", label: "设备", icon: "◫" }, { href: "/me", label: "我的", icon: "◍" }, ]; return (
{items.map((item) => { const active = pathname.startsWith(item.href); return ( {item.icon} {item.label} ); })}
); } export function HeaderTitle({ title, extra, }: { title: string; extra?: React.ReactNode; }) { return (

{title}

{extra}
); } export function LogoutButton({ label = "退出登录", compact = false, }: { label?: string; compact?: boolean; }) { const router = useRouter(); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(""); async function handleLogout() { setLoading(true); const response = await fetch("/api/auth/logout", { method: "POST", }); const result = (await response.json()) as { ok: boolean; message?: string }; setLoading(false); setMessage(result.message ?? (result.ok ? "已退出登录。" : "退出失败。")); if (result.ok) { await clearNativeSessionSnapshot(); router.push("/auth/login"); router.refresh(); } } if (compact) { return ( ); } return (
{label}
清除当前会话,回到登录页重新选择账号或验证码方式。
{message ? (
{message}
) : null}
); } export function AvatarStack({ primary, secondary, overflowCount, }: { primary: string; secondary?: string; overflowCount?: number; }) { if (!secondary) { return (
{primary}
); } return (
{primary}
{overflowCount ? `+${overflowCount}` : secondary}
); } export function ContextRing({ value, label, level, }: { value: number; label: string; level?: "safe" | "watch" | "urgent" | "critical"; }) { const color = level === "critical" ? "#FF4D4F" : level === "urgent" ? "#FF8A00" : level === "watch" ? "#1C7ED6" : "#07C160"; const background = `conic-gradient(${color} ${value * 3.6}deg, #D9D9D9 0deg)`; return (
{label}
); } function riskBadgeColor(level: ConversationItem["riskLevel"]) { switch (level) { case "high": return "bg-[#FFF1F0] text-[#CF1322]"; case "medium": return "bg-[#FFF7E6] text-[#D46B08]"; default: return "bg-[#F6FFED] text-[#389E0D]"; } } function conversationActionsPath(projectId: string) { return `/api/v1/conversations/${projectId}/actions`; } function ConversationActionButtons({ conversation, }: { conversation: ConversationItem; }) { const router = useRouter(); const [loading, setLoading] = useState<"toggle_pin" | "mark_read" | null>(null); if (conversation.conversationType === "folder_archive") { return
; } async function runAction(action: "toggle_pin" | "mark_read") { setLoading(action); await fetch(conversationActionsPath(conversation.projectId), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action }), }); setLoading(null); router.refresh(); } return (
{conversation.projectId !== "master-agent" ? ( ) : null} {conversation.unreadCount > 0 ? ( ) : null}
); } export function ConversationList({ conversations, }: { conversations: ConversationItem[]; }) { return (
{conversations.map((conversation) => (
{conversation.conversationType === "folder_archive" ? conversation.threadTitle : conversation.projectTitle}
{conversation.conversationType === "folder_archive" ? ( {conversation.threadCount ?? 0} 个线程 ) : ( {conversation.riskLevel === "high" ? "高风险" : conversation.riskLevel === "medium" ? "关注" : "稳定"} )} {conversation.unreadCount > 0 ? ( {conversation.unreadCount} ) : null}
{conversation.conversationType === "folder_archive" ? conversation.folderLabel : conversation.deviceNamesPreview.join(" / ")}
{conversation.preview}
{conversation.projectId === "master-agent" ? "置顶" : conversation.manualPinned ? "置顶" : ""}
{conversation.latestReplyLabel}
{conversation.contextBudgetIndicator.visible && conversation.contextBudgetIndicator.percent !== undefined ? ( ) : (
{conversation.activeDeviceCount > 1 ? `${conversation.activeDeviceCount} 台协作` : ""}
)}
))}
); } export function DeviceList({ devices }: { devices: Device[] }) { const router = useRouter(); return (
{devices.map((device) => ( ))}
); } export function DeviceEditorCard({ device, relatedThreads, activeEnrollment, }: { device: Device; relatedThreads: ThreadContextSnapshot[]; activeEnrollment?: DeviceEnrollment; }) { const router = useRouter(); const [name, setName] = useState(device.name); const [avatar, setAvatar] = useState(device.avatar); const [account, setAccount] = useState(device.account); const [status, setStatus] = useState(device.status); const [endpoint, setEndpoint] = useState(device.endpoint ?? ""); const [note, setNote] = useState(device.note ?? ""); const [projects, setProjects] = useState(device.projects.join(", ")); const [message, setMessage] = useState(""); async function save() { const response = await fetch(`/api/v1/devices/${device.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, avatar, account, status, endpoint, note, projects: projects .split(",") .map((item) => item.trim()) .filter(Boolean), }), }); const result = (await response.json()) as { ok: boolean; message?: string }; setMessage(result.ok ? "设备信息已更新。" : result.message ?? "更新失败"); if (result.ok) router.refresh(); } return (
设备详情
ID: {device.id}
当前状态
{(["online", "abnormal", "offline"] as const).map((item) => ( ))}
{message ? (
{message}
) : null} {activeEnrollment ? (
Pairing code: {activeEnrollment.pairingCode}
Token: {activeEnrollment.token}
过期时间:{formatClock(activeEnrollment.expiresAt)}
) : null}
活跃线程:{relatedThreads.length}
{relatedThreads.map((thread) => `${thread.title} (${thread.contextBudgetRemainingPct}%)`).join(";") || "当前无活跃线程。"}
); } export function ProfileHero({ user }: { user: UserProfile }) { return (
{user.avatar}
{user.name}
{user.accountType}
账号:{user.account}
绑定节点:{user.boundCodexNodeLabel ?? "未绑定"}
二维码:{user.qrCodeValue}
{user.roleLabel}
); } export function MenuRow({ href, title, description, badge, }: { href: string; title: string; description: string; badge?: React.ReactNode; }) { return (
{title}
{description}
{badge} >
); } function kindLabel(kind?: Message["kind"]) { switch (kind) { case "voice_intent": return "语音"; case "image_intent": return "图片"; case "video_intent": return "视频"; case "forward_notice": return "转发"; default: return null; } } export function ChatBubble({ message }: { message: Message }) { const mine = message.sender === "user"; const green = message.sender === "master"; const tag = kindLabel(message.kind); return (
{message.senderLabel} · {formatClock(message.sentAt)}
{tag ? (
{tag}
) : null} {message.body}
); } export function ProjectHeaderActions({ projectId }: { projectId: string }) { return (
项目目标 版本记录 转发
); } function orchestrationBackendChoiceLabel(choice: ProjectOrchestrationBackendState["availableChoices"][number]) { return choice.backendId === "boss-native-orchestrator" ? "Boss Native Orchestrator" : "OMX Team Runtime"; } function normalizeOrchestrationReasonLabel(value: string) { const trimmed = value.trim(); if (trimmed.endsWith("。") || trimmed.endsWith(".")) { return trimmed.slice(0, -1); } return trimmed; } function orchestrationBackendAvailabilityCopy( state: ProjectOrchestrationBackendState, fallbackActive: boolean, ) { if (state.omxAvailability.selectable) { return { badge: "正常", summary: "OMX Team Runtime 当前可用,当前可切换到该后端。", }; } return { badge: fallbackActive ? "已回退" : "OMX 受限", summary: fallbackActive ? `${normalizeOrchestrationReasonLabel(state.omxAvailability.reasonLabel)},当前已自动回退到 Boss Native Orchestrator。` : `${normalizeOrchestrationReasonLabel(state.omxAvailability.reasonLabel)},切换后会自动回退到 Boss Native Orchestrator。`, }; } export function ProjectOrchestrationBackendCard({ projectId, initialState, }: { projectId: string; initialState: ProjectOrchestrationBackendState; }) { const router = useRouter(); const [state, setState] = useState(initialState); const [savingBackendId, setSavingBackendId] = useState(null); const [message, setMessage] = useState(""); const fallbackActive = state.requestedBackendId !== state.currentBackendId; const availabilityCopy = orchestrationBackendAvailabilityCopy(state, fallbackActive); async function saveBackend(requestedBackendId: OrchestrationBackendId) { setSavingBackendId(requestedBackendId); const response = await fetch(`/api/v1/projects/${projectId}/orchestration-backend`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requestedBackendId }), }); const result = (await response.json()) as { ok: boolean; message?: string; currentBackendId?: OrchestrationBackendId; currentBackendLabel?: string; requestedBackendId?: OrchestrationBackendId; requestedBackendLabel?: string; availableChoices?: ProjectOrchestrationBackendState["availableChoices"]; omxAvailability?: ProjectOrchestrationBackendState["omxAvailability"]; }; setSavingBackendId(null); if ( !result.ok || !result.currentBackendId || !result.currentBackendLabel || !result.requestedBackendId || !result.requestedBackendLabel || !result.availableChoices || !result.omxAvailability ) { setMessage(result.message ?? "保存失败"); return; } setState({ projectId, currentBackendId: result.currentBackendId, currentBackendLabel: result.currentBackendLabel, requestedBackendId: result.requestedBackendId, requestedBackendLabel: result.requestedBackendLabel, availableChoices: result.availableChoices, omxAvailability: result.omxAvailability, }); setMessage( requestedBackendId === "omx-team" ? "已切换到 OMX Team Runtime。" : "已切换回 Boss Native Orchestrator。", ); router.refresh(); } return (
编排后端
当前生效:{state.currentBackendLabel}
当前请求:{state.requestedBackendLabel}
{availabilityCopy.badge}
{state.availableChoices.map((choice) => { const active = choice.current; const selectable = choice.selectable && savingBackendId !== choice.backendId; return ( ); })}
{availabilityCopy.summary}
{message ? (
{message}
) : null}
); } function masterIdentityPillClasses(role: MasterIdentitySummary["role"]) { switch (role) { case "primary": return "border-[#BBD6FF] bg-[#EEF5FF] text-[#2457C5]"; case "backup": return "border-[#FFD9B8] bg-[#FFF5E8] text-[#B54708]"; case "api_fallback": return "border-[#D8DDE6] bg-[#F3F4F6] text-[#4B5563]"; default: return "border-[#D8DDE6] bg-[#F3F4F6] text-[#4B5563]"; } } export function MasterIdentityPill({ identity }: { identity: MasterIdentitySummary }) { return (
{identity.roleLabel}
{(identity.nodeLabel || identity.displayName).slice(0, 18)}
); } export function MasterAgentChatMenu({ projectId }: { projectId: string }) { const router = useRouter(); const items = getMasterAgentChatMenuItems(projectId); if (items.length === 0) { return null; } return (
{items.map((item) => item.href ? ( {item.label} ) : ( ), )}
); } type PendingDispatchPlanState = { planId: string; summary?: string; targets: Array<{ projectId: string; threadDisplayName: string }>; }; export function ChatComposer({ projectId, initialPendingDispatchPlan, initialRejectedDispatchPlan, dispatchPlanRecoveryHint, }: { projectId: string; initialPendingDispatchPlan?: PendingDispatchPlanState | null; initialRejectedDispatchPlan?: PendingDispatchPlanState | null; dispatchPlanRecoveryHint?: string | null; }) { const router = useRouter(); const [value, setValue] = useState(""); const [message, setMessage] = useState(""); const [messageTone, setMessageTone] = useState<"success" | "error">("success"); const [loading, setLoading] = useState(false); const [localPendingDispatchPlan, setLocalPendingDispatchPlan] = useState(null); const [localRejectedDispatchPlan, setLocalRejectedDispatchPlan] = useState(null); const [dismissedPendingPlanId, setDismissedPendingPlanId] = useState(null); const pendingDispatchPlan = localPendingDispatchPlan ?? (initialPendingDispatchPlan && initialPendingDispatchPlan.planId !== dismissedPendingPlanId ? initialPendingDispatchPlan : null); const rejectedDispatchPlan = pendingDispatchPlan ? null : localRejectedDispatchPlan ?? initialRejectedDispatchPlan ?? null; async function confirmDispatchPlan() { if (!pendingDispatchPlan) return; setLoading(true); const response = await fetch( `/api/v1/projects/${projectId}/dispatch-plans/${pendingDispatchPlan.planId}/confirm`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ approvedTargetProjectIds: extractApprovedTargetProjectIds(pendingDispatchPlan), }), }, ); const result = (await response.json()) as { ok: boolean; executions?: Array; message?: string; }; setLoading(false); if (!result.ok) { setMessageTone("error"); setMessage(result.message ?? "确认下发失败,请重试。"); return; } const executionCount = result.executions?.length ?? extractApprovedTargetProjectIds(pendingDispatchPlan).length; setLocalPendingDispatchPlan(null); setLocalRejectedDispatchPlan(null); setDismissedPendingPlanId(pendingDispatchPlan.planId); setMessageTone("success"); setMessage(`已确认下发到 ${executionCount} 个线程。`); router.refresh(); } async function retryDispatchPlan() { if (!rejectedDispatchPlan) return; setLoading(true); const response = await fetch( `/api/v1/projects/${projectId}/dispatch-plans/${rejectedDispatchPlan.planId}/retry`, { method: "POST", headers: { "Content-Type": "application/json" }, }, ); const result = (await response.json()) as { ok: boolean; dispatchPlan?: { planId: string; summary?: string; targets?: Array<{ projectId: string; threadDisplayName: string }>; } | null; collaborationGate?: { requiresMasterAgentApproval?: boolean; }; message?: string; }; setLoading(false); if (!result.ok) { setMessageTone("error"); setMessage(result.message ?? "重新生成推荐失败,请稍后重试。"); return; } setLocalRejectedDispatchPlan(null); setLocalPendingDispatchPlan( result.dispatchPlan ? { planId: result.dispatchPlan.planId, summary: result.dispatchPlan.summary, targets: result.dispatchPlan.targets ?? [], } : null, ); setDismissedPendingPlanId(null); setMessageTone("success"); setMessage( result.collaborationGate?.requiresMasterAgentApproval ? "主 Agent 已重新生成推荐,等待你确认下发。" : "主 Agent 已重新生成推荐。", ); router.refresh(); } async function rejectDispatchPlan() { if (!pendingDispatchPlan) return; setLoading(true); const response = await fetch( `/api/v1/projects/${projectId}/dispatch-plans/${pendingDispatchPlan.planId}/reject`, { method: "POST", headers: { "Content-Type": "application/json" }, }, ); const result = (await response.json()) as { ok: boolean; plan?: { planId: string; summary?: string; targets?: Array<{ projectId: string; threadDisplayName: string }>; } | null; message?: string; }; setLoading(false); if (!result.ok) { setMessageTone("error"); setMessage(result.message ?? "拒绝失败,请稍后重试。"); return; } setLocalPendingDispatchPlan(null); setLocalRejectedDispatchPlan( result.plan ? { planId: result.plan.planId, summary: result.plan.summary, targets: result.plan.targets ?? pendingDispatchPlan.targets, } : pendingDispatchPlan, ); setDismissedPendingPlanId(pendingDispatchPlan.planId); setMessageTone("success"); setMessage("已拒绝主 Agent 推荐。"); router.refresh(); } async function send(kind: "text" | "voice_intent" | "image_intent" | "video_intent") { setLoading(true); const response = await fetch(`/api/v1/projects/${projectId}/messages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body: kind === "text" ? value : undefined, kind }), }); const result = (await response.json()) as { ok: boolean; message?: { body: string }; dispatchPlan?: { planId: string; summary?: string; targets?: Array<{ projectId: string; threadDisplayName: string }>; } | null; collaborationGate?: { requiresMasterAgentApproval?: boolean; }; messageText?: string; }; setLoading(false); if (result.ok) { void sendAppLog({ deviceId: boundDeviceIdFromDom(), projectId, level: "info", category: "chat.message_sent", message: kind === "text" ? `已发送文本消息:${value.trim() || "空文本"}` : `已发送 ${kind} 意图消息。`, mirrorToMaster: false, }); setValue(""); if (result.dispatchPlan) { setLocalPendingDispatchPlan({ planId: result.dispatchPlan.planId, summary: result.dispatchPlan.summary, targets: result.dispatchPlan.targets ?? [], }); setLocalRejectedDispatchPlan(null); setDismissedPendingPlanId(null); setMessage( result.collaborationGate?.requiresMasterAgentApproval ? "消息已发送,等待你批准主 Agent 下发。" : "消息已发送,主 Agent 已给出推荐线程。", ); setMessageTone("success"); } else { setLocalPendingDispatchPlan(null); setMessage(""); } router.refresh(); return; } void sendAppLog({ deviceId: boundDeviceIdFromDom(), projectId, level: "error", category: "chat.message_failed", message: `发送 ${kind} 消息失败。`, detail: "Boss 会话消息接口返回失败。", mirrorToMaster: true, }); setMessageTone("error"); setMessage("消息发送失败,请重试。"); } return (
setValue(event.target.value)} placeholder="向主 Agent 或项目发送消息" className="flex-1 rounded-full bg-[#F5F5F7] px-4 py-3 text-[14px] text-[#111111] outline-none" />
转发
{message ? (
{message}
) : null} {dispatchPlanRecoveryHint ? (
{dispatchPlanRecoveryHint}
) : null} {pendingDispatchPlan ? (
等待你确认主 Agent 推荐
{summarizeDispatchPlan(pendingDispatchPlan)}
) : null} {rejectedDispatchPlan ? (
上次推荐已拒绝
{summarizeDispatchPlan(rejectedDispatchPlan)}
{dispatchPlanRecoveryHint ?? "如果还想继续当前协作,可以直接重新生成新的推荐,不用把整条需求重新打一遍。"}
) : null}
); } export function GoalChecklist({ projectId, goals, }: { projectId: string; goals: GoalItem[]; }) { const router = useRouter(); const [workingId, setWorkingId] = useState(null); const [editingId, setEditingId] = useState(null); const [draft, setDraft] = useState(""); const [newGoal, setNewGoal] = useState(""); async function handleToggle(goalId: string) { setWorkingId(goalId); await fetch(`/api/projects/${projectId}/goals/${goalId}/toggle`, { method: "POST" }); setWorkingId(null); router.refresh(); } async function handleSave(goalId: string) { await fetch(`/api/projects/${projectId}/goals/update`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ goalId, text: draft, action: "update" }), }); setEditingId(null); setDraft(""); router.refresh(); } async function handleCreate() { if (!newGoal.trim()) return; await fetch(`/api/projects/${projectId}/goals/update`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: newGoal, action: "create" }), }); setNewGoal(""); router.refresh(); } return (
新增项目目标