/**
* 任务深链的「任务工作区」:一个任务挂多个对话(1:N)。三种业务共用本布局:
* - rwfx(任务FX,完整版):左对话列表 + 任务操作(编辑/下一步/清除记录)+ 跳转三情分析按钮;
* 右侧问答带「目的树闸门」(AgentChatPage taskGate)。
* - 3qfx(三情分析)/ xdfx(行动分析)(``lite`` 精简版):只有左对话列表(按 taskId + agentId 隔离),
* 无任务操作、无闸门;新建对话进问答页后,右上角「开始回答」按钮手动发出任务开场信息(AgentChatPage taskBind)。
* xdfx 额外在左上「当前任务」卡片下展示「行动详情」(行动名称/叙事,来自行动详情接口)。
*
* 左侧顶部 = 当前任务卡片(任务名称 + 简介,来自登录深链暂存的 copTaskDetail)+「新建对话」;
* 右侧 = 渲染选中的对话。
*
* - 进入 chats 索引:有历史 → 跳最新;无历史 → 跳 new(rwfx 走闸门 / 3qfx/xdfx 直接带开场信息)。
* - 切换/新建时 AgentChatPage 会停掉老对话的服务端 run。
* - (rwfx)清除任务记录:仅调 delete 接口(聊天对话保留);编辑/下一步:window.open B 系统地址。
* 见 [[task-purpose]] / [[task-deeplink]] / [[task-deeplink-config]]。
*/
import { useEffect, useMemo, useState } from "react";
import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
import { Loader2, MessageSquare, PlusSquare, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { deleteTaskPurpose, readTaskContext } from "@/core/auth/task-purpose";
import {
fetchActionDetail,
readCopTaskDetail,
resolveTaskDeeplinkTarget,
type ActionDetail,
} from "@/core/auth/task-deeplink";
import { fetchSqReportStatus } from "@/core/auth/dashboard-input";
import { TASK_BUSINESSES, type TaskBusiness } from "@/core/auth/task-buttons";
import { useTaskThreads } from "@/core/threads/use-task-threads";
import { titleOfThread } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import AgentChatPage from "@/pages/AgentChatPage";
import { ChatRuntime } from "@/pages/WorkspaceRoutes";
import { TaskJumpBar } from "@/strategy-components/components/task-components/TaskJumpBar";
function useChatsBase(taskBase: string): {
taskId: string;
agentId: string;
/** xdfx(partitionByAction):按「行动id」划分聊天记录用;其它业务为 ""(仍按 taskId 划分)。 */
actionId: string;
chatsBase: string;
} {
const { agent_id } = useParams<{ agent_id: string }>();
const agentId = agent_id ?? "";
const ctx = readTaskContext();
const taskId = ctx?.taskId ?? "";
const actionId = ctx?.partitionByAction ? (ctx.actionId ?? "") : "";
return { taskId, agentId, actionId, chatsBase: `${taskBase}/agent/${agentId}/chats` };
}
/**
* 左侧顶部「当前任务」卡片:展示登录深链查询出的任务名称 + 简介(留存在 sessionStorage 的
* copTaskDetail)。三种业务工作区都展示,放在「新建对话」上面。无任务详情时不渲染。
*/
function TaskDetailHeader() {
const detail = useMemo(() => readCopTaskDetail(), []);
const name = desensitize((detail?.taskName ?? "").trim());
const brief = desensitize(((detail?.overview ?? "").trim() || (detail?.taskContent ?? "").trim()));
if (!name && !brief) return null;
return (
当前任务
{name && (
{name}
)}
{brief && (
{brief}
)}
);
}
/**
* xdfx 专用「行动详情」卡片:放在「当前任务」卡片下面。用任务上下文里的「行动id」
* (xdfx 深链原始 ?taskId=)调行动详情接口,展示行动名称 + 叙事。无数据时不渲染。
*/
function ActionDetailHeader() {
const actionId = useMemo(() => readTaskContext()?.actionId ?? "", []);
const [detail, setDetail] = useState(null);
useEffect(() => {
if (!actionId) return;
let alive = true;
void fetchActionDetail(actionId).then((d) => {
if (alive) setDetail(d);
});
return () => {
alive = false;
};
}, [actionId]);
const name = desensitize((detail?.actionName ?? "").trim());
const narration = desensitize((detail?.narration ?? "").trim());
// 行动名称为空时用「行动 #id」兜底,至少把卡片标出来(mock 行动名/叙事常为空)。
const title = name || (detail?.id ? `行动 #${detail.id}` : "");
if (!title && !narration) return null;
return (
行动详情
{title && (
{title}
)}
{narration && (
{narration}
)}
);
}
/** 从路由路径里解析当前打开的会话 id(布局组件读不到子路由的 :thread_id,故从 path 取)。 */
function parseThreadId(pathname: string, chatsBase: string): string | null {
if (!pathname.startsWith(chatsBase)) return null;
const seg = pathname.slice(chatsBase.length).replace(/^\/+/, "").split("/")[0];
return seg && seg !== "new" ? seg : null;
}
/**
* 左侧任务对话列表:顶部「当前任务」卡片 +「新建对话」;下方对话列表(1:N)。
* 跳转按钮已移到工作区底部「快捷跳转」栏(见 ``TaskJumpBar``)。
*/
function TaskConversationsList({
taskId,
agentId,
actionId,
chatsBase,
business,
lite,
}: {
taskId: string;
agentId: string;
/** xdfx:按「行动id」划分聊天记录;其它业务为 ""。 */
actionId: string;
chatsBase: string;
business: TaskBusiness;
lite?: boolean;
}) {
const { pathname } = useLocation();
const navigate = useNavigate();
const { threads, isLoading } = useTaskThreads(taskId, agentId, actionId);
const currentThreadId = useMemo(() => parseThreadId(pathname, chatsBase), [pathname, chatsBase]);
const businessName = desensitize(TASK_BUSINESSES.find((b) => b.code === business)?.name ?? "");
return (
{/* 左上角标明当前是哪个分析页面 */}
{businessName}
{/* xdfx:任务详情下面再展示行动详情 */}
{business === "xdfx" &&
}
任务对话
{isLoading ? "同步中" : `${threads.length} 个会话`}
{!lite &&
}
{isLoading ? (
加载中…
) : threads.length === 0 ? (
暂无对话
) : (
threads.map((thread) => (
))
)}
);
}
/**
* 「清除记录」按钮(rwfx):放在「新建对话」下面,仅调 delete 接口清空 B 系统目的策划数据
* (聊天对话保留)。带二次确认弹框。
*/
function TaskClearButton({ taskId }: { taskId: string }) {
const [clearOpen, setClearOpen] = useState(false);
const [clearing, setClearing] = useState(false);
const handleClear = async () => {
setClearing(true);
try {
const r = await deleteTaskPurpose(taskId);
if (r.success) toast.success("已清除任务记录");
else toast.error(r.message || "清除失败");
} catch {
toast.error("清除失败,请稍后再试");
} finally {
setClearing(false);
setClearOpen(false);
}
};
return (
<>
>
);
}
/**
* rwfx 专用「3q 完成预检」闸门:进入任务分析工作区时调研判情报报告接口
* (``GET {VITE_AIPI_CONSUMER_URL}/cop/selectSqReport?taskId=``)。若该任务的报告为空
* (数组为空 / 内容全为 null,即 3q 未完成)→ 弹框提示「3q 未完成,是否进行 3qfx」,
* 并提供「前往 3qfx」按钮切换到**同一任务**的三情分析工作区(复用深链解析重绑任务上下文)。
* 报告正常 / 接口不可判定(缺地址、请求失败)时不打扰用户。见 [[dashboard-input]] / [[task-deeplink]]。
*/
function RwfxSqReportGate({ taskId }: { taskId: string }) {
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [jumping, setJumping] = useState(false);
useEffect(() => {
if (!taskId) return;
let alive = true;
void fetchSqReportStatus(taskId).then((status) => {
// 仅「明确为空(3q 未完成)」才弹框;complete / unknown 静默放行。
if (alive && status === "incomplete") setOpen(true);
});
return () => {
alive = false;
};
}, [taskId]);
const handleGo3qfx = async () => {
const detail = readCopTaskDetail();
if (!detail || !taskId) {
toast.error("缺少任务信息,无法跳转");
return;
}
setJumping(true);
try {
// 复用深链解析:会清掉当前 rwfx 任务上下文并按 3qfx 业务重新绑定(同一 taskId)。
const target = await resolveTaskDeeplinkTarget("3qfx", detail, taskId);
if (target) {
setOpen(false);
navigate(target.path, { state: target.state });
} else {
toast.error("未配置三情分析(3qfx)对应的业务智能体");
}
} catch {
toast.error("跳转失败,请稍后再试");
} finally {
setJumping(false);
}
};
return (
);
}
/**
* 任务工作区布局:左列表 + 右 Outlet。
* ``lite``(3qfx/xdfx)= 精简版:左列表无跳转按钮、无底部任务操作面板。
*/
export function TaskFxWorkspace({
taskBase,
business,
lite,
}: {
taskBase: string;
business: TaskBusiness;
lite?: boolean;
}) {
const { taskId, agentId, actionId, chatsBase } = useChatsBase(taskBase);
return (
{/* rwfx 专属:进入时预检 3q 是否完成,未完成则弹框引导去 3qfx。 */}
{business === "rwfx" &&
}
);
}
/** chats 索引:有历史对话 → 跳最新;否则 → 跳 new(rwfx 走闸门 / 3qfx/xdfx 直接带开场信息)。 */
export function TaskFxIndexResolver({ taskBase }: { taskBase: string }) {
const { taskId, agentId, actionId, chatsBase } = useChatsBase(taskBase);
const navigate = useNavigate();
const { isLoading, hasThreads, latestThreadId } = useTaskThreads(taskId, agentId, actionId);
useEffect(() => {
if (isLoading) return;
if (hasThreads && latestThreadId) {
navigate(`${chatsBase}/${latestThreadId}`, { replace: true });
} else {
// 首次进入、该任务尚无聊天对话 → /new 并打 taskAutoStart 标记:3qfx/xdfx 直接自动发开场词,
// rwfx 目的树为空时自动开始(用户主动「新建对话」走 TaskConversationsList 的 navigate,不带此标记)。
navigate(`${chatsBase}/new`, { replace: true, state: { taskAutoStart: true } });
}
}, [isLoading, hasThreads, latestThreadId, chatsBase, navigate]);
return (
);
}
/**
* 右侧对话宿主(rwfx / xdfx):AgentChatPage taskGate → 新建对话顶部展示目的树闸门面板 + 老对话停止。
* ``autoStartWhenPurposeEmpty``(rwfx):查询任务目的为空数据时自动开始第一次问答。
*/
export function TaskFxChat({
taskBase,
autoStartWhenPurposeEmpty,
}: {
taskBase: string;
autoStartWhenPurposeEmpty?: boolean;
}) {
const { agent_id } = useParams<{ agent_id: string }>();
return (
);
}
/** 右侧对话宿主(3qfx/xdfx):AgentChatPage taskBind → 仅 1:N 绑定,新建对话进问答页,右上角「开始回答」按钮手动发出开场信息。 */
export function TaskBindChat({ taskBase }: { taskBase: string }) {
const { agent_id } = useParams<{ agent_id: string }>();
return (
);
}