"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import type { DeviceImportDraft, DeviceImportResolution, MasterAgentTask } from "@/lib/boss-data"; type ImportDraftResponse = { ok: boolean; draft?: DeviceImportDraft | null; resolution?: DeviceImportResolution | null; reviewTask?: MasterAgentTask | null; message?: string; }; type FeedbackTone = "info" | "success" | "error"; type Feedback = { tone: FeedbackTone; text: string; }; export type DeviceImportDraftViewCopy = { statusTitle: string; statusBody: string; recommendationHint: string; resultTitle: string; resultBody: string; candidateCount: number; selectedCount: number; recommendedCount: number; appliedProjectNames: string[]; }; function isDeviceImportReviewPending( draft: DeviceImportDraft | null, resolution: DeviceImportResolution | null, reviewTask: MasterAgentTask | null, ) { if (!draft || draft.status !== "pending_resolution" || resolution) { return false; } return reviewTask?.status === "queued" || reviewTask?.status === "running"; } function groupCandidates(draft: DeviceImportDraft | null) { const groups = new Map>(); for (const candidate of draft?.candidates ?? []) { const key = candidate.codexFolderRef?.trim() || candidate.folderRef?.trim() || candidate.folderName; const bucket = groups.get(key) ?? []; bucket.push(candidate); groups.set(key, bucket); } return [...groups.entries()].map(([key, items]) => ({ key, folderName: items[0]?.folderName ?? "未命名项目", items: [...items].sort((a, b) => b.lastActiveAt.localeCompare(a.lastActiveAt)), })); } function joinProjectNames(projectNames: string[]) { return projectNames.length > 0 ? projectNames.join("、") : ""; } export function describeDeviceImportDraft( draft: DeviceImportDraft | null, resolution: DeviceImportResolution | null, reviewTask: MasterAgentTask | null = null, ): DeviceImportDraftViewCopy { const candidateCount = draft?.candidates.length ?? 0; const selectedCount = draft?.selectedCandidateIds.length ?? 0; const recommendedCount = draft?.candidates.filter((candidate) => candidate.suggestedImport).length ?? 0; const appliedProjectNames = draft?.appliedProjectNames ?? []; const appliedProjectCount = appliedProjectNames.length; if (!draft) { return { statusTitle: "等待导入草稿", statusBody: "先让设备完成首次 heartbeat 并上报候选线程,导入草稿就会出现在这里。", recommendationHint: "拿到候选线程后,先从标记为推荐导入的项目开始。", resultTitle: "导入结果", resultBody: "生成导入建议并应用后,这里会显示真正导入到会话首页的线程。", candidateCount, selectedCount, recommendedCount, appliedProjectNames, }; } let statusTitle = "等待勾选"; let statusBody = "先勾选要导入的线程,再生成导入建议。"; let resultTitle = "导入建议"; let resultBody = "应用导入前,这里会先显示主 Agent 风格的导入建议。"; switch (draft.status) { case "pending_candidates": statusTitle = "等待候选线程"; statusBody = "设备已经就绪,等 heartbeat 带回线程候选后,就可以开始勾选。"; resultTitle = "导入结果"; resultBody = "候选线程出现后,这里会显示推荐和建议。"; break; case "pending_selection": statusTitle = "等待勾选"; statusBody = "先勾选想导入的线程,再生成导入建议。"; break; case "pending_resolution": if (isDeviceImportReviewPending(draft, resolution, reviewTask)) { statusTitle = "主 Agent 审核中"; statusBody = "勾选已保存,主 Agent 正在整理导入建议,页面会自动刷新。"; } else if (reviewTask?.status === "failed") { statusTitle = "建议生成失败"; statusBody = "主 Agent 这次没能生成导入建议。可以稍后重新生成,当前勾选会保留。"; } else { statusTitle = "建议生成中"; statusBody = "勾选已保存,接下来会生成导入建议。"; } resultTitle = "导入建议"; resultBody = reviewTask?.status === "failed" ? "导入建议生成失败后,可以直接重新生成,不需要重新勾选。" : "导入建议生成后,会先显示每个线程的处理方式和原因。"; break; case "resolved": statusTitle = "建议已生成"; statusBody = "可以先看建议,再点应用导入把线程落成会话窗口。"; resultTitle = "导入建议"; resultBody = resolution?.summary ?? "主 Agent 已给出导入建议。"; break; case "applied": statusTitle = "已导入"; statusBody = appliedProjectCount > 0 ? `已导入 ${appliedProjectCount} 个线程:${joinProjectNames(appliedProjectNames)}。` : "导入已完成,线程已经落到会话首页。"; resultTitle = "应用结果"; resultBody = appliedProjectCount > 0 ? `已把 ${appliedProjectCount} 个线程导入到会话首页。` : "应用导入后,线程已经出现在会话首页。"; break; default: break; } const recommendationHint = recommendedCount > 0 ? `推荐 ${recommendedCount} 项,优先勾选带“推荐导入”的线程。` : candidateCount > 0 ? "当前没有显式推荐项,按最近活跃度挑选也可以。" : "当前还没有可选线程。"; return { statusTitle, statusBody, recommendationHint, resultTitle, resultBody, candidateCount, selectedCount, recommendedCount, appliedProjectNames, }; } export function DeviceImportDraftManager({ deviceId, deviceName, }: { deviceId: string; deviceName?: string; }) { const router = useRouter(); const [loading, setLoading] = useState(false); const [feedback, setFeedback] = useState(null); const [draft, setDraft] = useState(null); const [resolution, setResolution] = useState(null); const [reviewTask, setReviewTask] = useState(null); const [selectedCandidateIds, setSelectedCandidateIds] = useState([]); const loadDraft = useCallback(async () => { setLoading(true); try { const response = await fetch(`/api/v1/devices/${deviceId}/import-draft`, { cache: "no-store" }); const data = (await response.json()) as ImportDraftResponse; setDraft(data.draft ?? null); setResolution(data.resolution ?? null); setReviewTask(data.reviewTask ?? null); setSelectedCandidateIds(data.draft?.selectedCandidateIds ?? []); setFeedback( data.ok ? null : { tone: "error", text: data.message ?? "导入草稿加载失败" }, ); } catch (error) { setFeedback({ tone: "error", text: error instanceof Error ? error.message : "导入草稿加载失败", }); } finally { setLoading(false); } }, [deviceId]); useEffect(() => { const timer = window.setTimeout(() => { void loadDraft(); }, 0); return () => window.clearTimeout(timer); }, [loadDraft]); useEffect(() => { if (!isDeviceImportReviewPending(draft, resolution, reviewTask)) { return; } const timer = window.setTimeout(() => { void loadDraft(); }, 2000); return () => window.clearTimeout(timer); }, [draft, resolution, reviewTask, loadDraft]); const groups = useMemo(() => groupCandidates(draft), [draft]); const copy = useMemo( () => describeDeviceImportDraft(draft, resolution, reviewTask), [draft, resolution, reviewTask], ); function toggle(candidateId: string) { setSelectedCandidateIds((current) => current.includes(candidateId) ? current.filter((item) => item !== candidateId) : [...current, candidateId], ); } async function clearSelection() { setLoading(true); try { const response = await fetch(`/api/v1/devices/${deviceId}/import-draft/select`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ selectedCandidateIds: [] }), }); const result = (await response.json()) as { ok: boolean; message?: string; draft?: DeviceImportDraft; }; if (!result.ok) { setFeedback({ tone: "error", text: result.message ?? "清空勾选失败" }); return; } setDraft(result.draft ?? draft); setResolution(null); setReviewTask(null); setSelectedCandidateIds([]); setFeedback({ tone: "success", text: "已清空当前勾选,你可以重新选择要导入的线程。" }); } catch (error) { setFeedback({ tone: "error", text: error instanceof Error ? error.message : "清空勾选失败", }); } finally { setLoading(false); } } async function reviewSelection() { setLoading(true); try { const selectResponse = await fetch(`/api/v1/devices/${deviceId}/import-draft/select`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ selectedCandidateIds }), }); const selectResult = (await selectResponse.json()) as { ok: boolean; message?: string; draft?: DeviceImportDraft; }; if (!selectResult.ok) { setFeedback({ tone: "error", text: selectResult.message ?? "勾选保存失败" }); return; } setDraft(selectResult.draft ?? draft); setResolution(null); setReviewTask(null); setSelectedCandidateIds(selectResult.draft?.selectedCandidateIds ?? selectedCandidateIds); const reviewResponse = await fetch(`/api/v1/devices/${deviceId}/import-draft/review`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); const reviewResult = (await reviewResponse.json()) as { ok: boolean; message?: string; draft?: DeviceImportDraft; resolution?: DeviceImportResolution; reviewTask?: MasterAgentTask; task?: MasterAgentTask; }; if (!reviewResult.ok) { setFeedback({ tone: "error", text: reviewResult.message ?? "导入建议生成失败,已保留当前勾选。" }); return; } setDraft(reviewResult.draft ?? selectResult.draft ?? null); setResolution(reviewResult.resolution ?? null); setReviewTask(reviewResult.reviewTask ?? reviewResult.task ?? null); setSelectedCandidateIds( reviewResult.draft?.selectedCandidateIds ?? selectResult.draft?.selectedCandidateIds ?? selectedCandidateIds, ); setFeedback({ tone: reviewResult.resolution ? "success" : "info", text: reviewResult.resolution ? "已生成导入建议,先看推荐理由再应用导入。" : "已提交给主 Agent 审核,建议生成后会自动刷新。", }); } catch (error) { setFeedback({ tone: "error", text: error instanceof Error ? error.message : "导入建议生成失败", }); } finally { setLoading(false); } } async function applyResolution() { setLoading(true); try { const response = await fetch(`/api/v1/devices/${deviceId}/import-draft/apply`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); const result = (await response.json()) as { ok: boolean; message?: string; draft?: DeviceImportDraft; resolution?: DeviceImportResolution; }; if (!result.ok) { setFeedback({ tone: "error", text: result.message ?? "导入应用失败" }); return; } setDraft(result.draft ?? draft); setResolution(result.resolution ?? resolution); setReviewTask(null); setSelectedCandidateIds(result.draft?.selectedCandidateIds ?? draft?.selectedCandidateIds ?? []); setFeedback({ tone: "success", text: result.draft?.appliedProjectNames?.length ? `已导入 ${result.draft.appliedProjectNames.length} 个线程:${joinProjectNames(result.draft.appliedProjectNames)}。` : "已把选中的项目线程导入到会话首页。", }); router.refresh(); } catch (error) { setFeedback({ tone: "error", text: error instanceof Error ? error.message : "导入应用失败", }); } finally { setLoading(false); } } const candidateCount = copy.candidateCount; const recommendedCount = copy.recommendedCount; return (
导入 Codex 项目
{deviceName ?? deviceId} 完成首次 heartbeat 后,这里会出现可导入项目和线程;如果暂时为空,先刷新等待下一次发现。
{copy.statusTitle}
{copy.statusBody}
候选 {candidateCount} · 已选 {selectedCandidateIds.length} · 推荐 {recommendedCount}
{copy.recommendationHint}
{draft ? (
候选线程:{draft.candidates.length}
当前状态:{draft.status}
已勾选:{selectedCandidateIds.length} {reviewTask ? ( <>
审核任务:{reviewTask.status} ) : null} {draft.status === "pending_candidates" ? ( <>
设备已在线,但还没有发现可导入线程。 ) : null}
) : (
当前还没有导入草稿。请先让设备端完成配对并保持在线,然后回到这里点击刷新。
)} {groups.map((group) => (
{group.folderName}
{group.items.length} 个线程
{group.items.some((candidate) => candidate.suggestedImport) ? ( 推荐导入 ) : null}
{group.items.map((candidate) => { const selected = selectedCandidateIds.includes(candidate.candidateId); return ( ); })}
))}
{copy.resultTitle}
{copy.resultBody}
{resolution ? (
{resolution.summary}
{resolution.items.map((item) => (
{item.threadDisplayName} · {item.folderName} · {item.action}
))}
) : null} {draft?.appliedProjectNames?.length ? (
已导入到会话首页
{draft.appliedProjectNames.length} 个线程:{joinProjectNames(draft.appliedProjectNames)}
) : null}
{feedback ? (
{feedback.text}
) : null}
); }