"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 { extractApprovedTargetProjectIds, summarizeDispatchPlan, } from "@/lib/dispatch-plan-ui"; import type { Device, DeviceEnrollment, GoalItem, MasterIdentitySummary, Message, OtaUpdate, OtaUpdateLog, OpsRepairTicket, OpsRepairVerification, 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 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)}
); } type PendingDispatchPlanState = { planId: string; summary?: string; targets: Array<{ projectId: string; threadDisplayName: string }>; }; export function ChatComposer({ projectId, initialPendingDispatchPlan, }: { projectId: string; initialPendingDispatchPlan?: PendingDispatchPlanState | 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 [dismissedPendingPlanId, setDismissedPendingPlanId] = useState(null); const pendingDispatchPlan = localPendingDispatchPlan ?? (initialPendingDispatchPlan && initialPendingDispatchPlan.planId !== dismissedPendingPlanId ? initialPendingDispatchPlan : 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); setDismissedPendingPlanId(pendingDispatchPlan.planId); setMessageTone("success"); setMessage(`已确认下发到 ${executionCount} 个线程。`); 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 ?? [], }); 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} {pendingDispatchPlan ? (
等待你确认主 Agent 推荐
{summarizeDispatchPlan(pendingDispatchPlan)}
) : 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 (
新增项目目标