feat(task-deeplink): 3qfx/xdfx 新建对话改手动「开始回答」+ xdfx 行动详情卡片 + 独立问答智能体列表栏
- AgentChatPage: 纯绑定深链(3qfx/xdfx)不再自动发开场信息,改由右上角「开始回答」按钮手动触发,从根上消除新建对话竞态导致的重复发送;移除 autoSentArmedRef/streamThreadId 相关逻辑 - task-deeplink.ts: 抽出 fetchActionDetail 返回整包 ActionDetail,fetchTaskIdFromAction 复用之 - TaskFxWorkspace: xdfx 左栏「当前任务」下新增「行动详情」卡片(行动名称/叙事) - StrategyRoutes/WorkspaceRoutes: 独立智能体问答整页新增最左「智能体列表」栏(showAgentList);xdfx 右侧宿主改用 TaskBindChat(精简版) - 新增 agent-chat-sidebar.tsx Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
179a3f2b2f
commit
6aaa4c841e
6 files changed
+298
-49
No files matched your search
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
import { Bot, Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { agentColor } from "@/components/workspace/agent-icon-color";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useAgentsPaginated } from "@/core/agents";
|
||||
import { useRouteBase } from "@/core/route-base";
|
||||
import { usePublishSquares } from "@/core/system-settings/hooks";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** 「我的」作用域的下拉选项值;其余选项值为各发布广场(publish square)的 id。 */
|
||||
const MINE_SCOPE = "mine";
|
||||
|
||||
// 列表一次拉取的条数。超出时底部出现「查看全部」入口,跳到完整的智能体管理页,
|
||||
// 避免静默截断。侧边栏只做快速切换,不在此分页。
|
||||
const LIST_PAGE_SIZE = 50;
|
||||
|
||||
/**
|
||||
* 智能体问答页左侧「智能体列表」栏。
|
||||
*
|
||||
* - 顶部下拉:选「我的」(默认,= 我创建/收藏的)或任一「广场」(发布广场 publish square),
|
||||
* 与「智能体管理」页的主标签一致。
|
||||
* - 下方:所选作用域下的智能体列表;点击进入该智能体的新对话(保持当前路由命名空间)。
|
||||
* - 当前正在对话的智能体高亮。
|
||||
*
|
||||
* 仅在独立的智能体问答整页(/page/strategy|workspace/agents/:id/chats/:thread)展示;
|
||||
* 任务工作区 / zzJC·zzXD 模块 / 嵌入态不展示(它们各有自己的左栏)。
|
||||
*/
|
||||
export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string }) {
|
||||
const routeBase = useRouteBase();
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
|
||||
const { data: publishSquares } = usePublishSquares();
|
||||
// 每个发布广场对应一个下拉选项;后端至少返回一个内置默认广场。
|
||||
const squares = useMemo(
|
||||
() => publishSquares?.squares ?? [],
|
||||
[publishSquares?.squares],
|
||||
);
|
||||
|
||||
// 作用域:MINE_SCOPE 或某个广场 id。默认「我的」。
|
||||
const [scope, setScope] = useState<string>(MINE_SCOPE);
|
||||
|
||||
// 若所选广场被管理员删除,回退到「我的」,避免指向不存在的广场。
|
||||
useEffect(() => {
|
||||
if (
|
||||
scope !== MINE_SCOPE &&
|
||||
squares.length > 0 &&
|
||||
!squares.some((sq) => sq.id === scope)
|
||||
) {
|
||||
setScope(MINE_SCOPE);
|
||||
}
|
||||
}, [scope, squares]);
|
||||
|
||||
const isMine = scope === MINE_SCOPE;
|
||||
const { agents, total, isLoading, isFetching } = useAgentsPaginated({
|
||||
scope: isMine ? "mine" : "square",
|
||||
sort: "time_desc",
|
||||
page: 1,
|
||||
pageSize: LIST_PAGE_SIZE,
|
||||
square: isMine ? undefined : scope,
|
||||
});
|
||||
|
||||
return (
|
||||
// 配色与 AgentModuleSidebar 一致:浅色白底,深蓝主题半透明深蓝玻璃面板。
|
||||
<aside className="flex h-full w-(--sidebar-width) shrink-0 flex-col overflow-hidden border-r border-border/70 bg-white text-foreground backdrop-blur-md dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80">
|
||||
{/* 顶部:作用域下拉(我的 / 各广场) */}
|
||||
<div className="shrink-0 border-b border-border/60 px-3 py-2.5">
|
||||
<Select value={scope} onValueChange={setScope}>
|
||||
<SelectTrigger className="h-9 w-full text-sm" title="切换智能体来源">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||
<SelectItem value={MINE_SCOPE}>我的</SelectItem>
|
||||
{squares.map((sq) => (
|
||||
<SelectItem key={sq.id} value={sq.id}>
|
||||
{sq.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 智能体列表 */}
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-2 py-3">
|
||||
{isLoading && agents.length === 0 ? (
|
||||
<div className="text-muted-foreground flex h-24 items-center justify-center gap-2 text-xs">
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
加载中…
|
||||
</div>
|
||||
) : agents.length === 0 ? (
|
||||
<p className="px-3 py-1.5 text-xs text-muted-foreground/70">
|
||||
{isMine ? "暂无智能体" : "该广场暂无智能体"}
|
||||
</p>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-1 transition-opacity",
|
||||
isFetching && "opacity-60",
|
||||
)}
|
||||
>
|
||||
{agents.map((agent) => {
|
||||
const href = `${routeBase}/agents/${agent.id}/chats/new`;
|
||||
const isActive = agent.id === activeAgentId;
|
||||
const { from, to } = agentColor(agent.id, isDark);
|
||||
return (
|
||||
<Link
|
||||
key={agent.id}
|
||||
href={href}
|
||||
className={cn(
|
||||
"group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors",
|
||||
isActive
|
||||
? "border-primary/40 bg-primary/5"
|
||||
: "border-transparent hover:border-border hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg text-white shadow-sm"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(135deg, ${from}, ${to})`,
|
||||
}}
|
||||
>
|
||||
<Bot className="size-[18px]" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span
|
||||
className={cn(
|
||||
"block truncate text-[13px] font-semibold",
|
||||
isActive ? "text-primary" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{desensitize(agent.name)}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-[11px] leading-snug text-muted-foreground">
|
||||
{agent.description
|
||||
? desensitize(agent.description)
|
||||
: "智能体助手"}
|
||||
</span>
|
||||
</span>
|
||||
{isActive && (
|
||||
<span className="absolute top-1/2 right-1 h-6 w-1 -translate-y-1/2 rounded-full bg-primary" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部:超出一页时引导到完整的智能体管理页(不在侧边栏静默截断) */}
|
||||
{total > agents.length && (
|
||||
<Link
|
||||
href={`${routeBase}/agents`}
|
||||
className="text-muted-foreground hover:text-foreground shrink-0 border-t border-border/60 px-3 py-2 text-center text-xs transition-colors"
|
||||
>
|
||||
共 {total} 个,查看全部 →
|
||||
</Link>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -111,13 +111,29 @@ export function readCopTaskDetail(): CopTaskDetail | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** 行动详情接口 ``{action_detail_url}/{actionId}`` 返回的 ``data`` 字段(xdfx 行动分析用)。 */
|
||||
export interface ActionDetail {
|
||||
/** 行动id(深链 ?taskId= 传入的就是它)。 */
|
||||
id: string;
|
||||
/** 该行动归属的真正「任务id」。 */
|
||||
taskId: string;
|
||||
/** 所属目的id。 */
|
||||
purposeId: string;
|
||||
/** 行动名称。 */
|
||||
actionName: string;
|
||||
/** 行动状态码。 */
|
||||
actionStatus: number | string;
|
||||
/** 行动叙事 / 描述。 */
|
||||
narration: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* xdfx 深链:用「行动id」调行动详情接口 ``{action_detail_url}/{actionId}``,取出 ``data.taskId``
|
||||
* 作为真正的「任务id」。``action_test=true`` 时直接用后端配置里的假数据(接口未通时用)。
|
||||
* 缺地址 / 返回不合法 → 返回 ""(调用方据此回退)。
|
||||
* xdfx 深链:用「行动id」调行动详情接口 ``{action_detail_url}/{actionId}``,取出整包行动详情。
|
||||
* ``action_test=true`` 时直接用后端配置里的假数据(接口未通时用)。
|
||||
* 缺地址 / 返回不合法 → 返回 null。
|
||||
*/
|
||||
export async function fetchTaskIdFromAction(actionId: string): Promise<string> {
|
||||
if (!actionId) return "";
|
||||
export async function fetchActionDetail(actionId: string): Promise<ActionDetail | null> {
|
||||
if (!actionId) return null;
|
||||
const cfg = await fetchTaskDeeplinkConfig();
|
||||
let body: unknown;
|
||||
if (cfg.actionTest) {
|
||||
@@ -127,12 +143,28 @@ export async function fetchTaskIdFromAction(actionId: string): Promise<string> {
|
||||
// service 拦截器已统一注入 Authorization: Bearer admin 并返回整包 body;行动id 走路径段。
|
||||
body = await requestCrudService.get(`${base}/${encodeURIComponent(actionId)}`);
|
||||
} else {
|
||||
return "";
|
||||
return null;
|
||||
}
|
||||
const data = body && typeof body === "object" ? (body as { data?: unknown }).data : null;
|
||||
if (!data || typeof data !== "object") return "";
|
||||
const taskId = (data as Record<string, unknown>).taskId;
|
||||
return taskId != null && taskId !== "" ? String(taskId) : "";
|
||||
if (!data || typeof data !== "object") return null;
|
||||
const d = data as Record<string, unknown>;
|
||||
return {
|
||||
id: d.id != null ? String(d.id) : "",
|
||||
taskId: d.taskId != null ? String(d.taskId) : "",
|
||||
purposeId: d.purposeId != null ? String(d.purposeId) : "",
|
||||
actionName: typeof d.actionName === "string" ? d.actionName : "",
|
||||
actionStatus:
|
||||
typeof d.actionStatus === "number" || typeof d.actionStatus === "string" ? d.actionStatus : "",
|
||||
narration: typeof d.narration === "string" ? d.narration : "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* xdfx 深链:用「行动id」解析出真正的「任务id」(取行动详情的 ``data.taskId``)。
|
||||
* 缺地址 / 返回不合法 → 返回 ""(调用方据此回退)。
|
||||
*/
|
||||
export async function fetchTaskIdFromAction(actionId: string): Promise<string> {
|
||||
return (await fetchActionDetail(actionId))?.taskId ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AgentChatSidebar } from "@/components/workspace/agents/agent-chat-sidebar";
|
||||
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
||||
import { AgentQuestionChips } from "@/components/workspace/agents/agent-question-chips";
|
||||
import { AgentWelcome } from "@/components/workspace/agent-welcome";
|
||||
@@ -39,6 +40,7 @@ export default function AgentChatPage({
|
||||
taskGate,
|
||||
taskBind,
|
||||
autoStartWhenPurposeEmpty,
|
||||
showAgentList,
|
||||
}: {
|
||||
/** 当作为「通用问答右侧嵌入」渲染时由父组件传入的 agent id(替代路由参数)。
|
||||
* 传入即视为嵌入态:路由仍停留在 /chats/new,仅右侧问答区换成该智能体的对话。 */
|
||||
@@ -56,6 +58,9 @@ export default function AgentChatPage({
|
||||
taskBind?: boolean;
|
||||
/** rwfx:查询任务目的为空数据时,自动开始第一次问答(跳过手动点「开始问答」)。 */
|
||||
autoStartWhenPurposeEmpty?: boolean;
|
||||
/** 是否在最左侧展示「智能体列表」栏(顶部下拉切换 我的/广场,点击进入该智能体对话)。
|
||||
* 仅独立的智能体问答整页开启;任务工作区 / 模块页 / 嵌入态不传(各有自己的左栏)。 */
|
||||
showAgentList?: boolean;
|
||||
} = {}) {
|
||||
const routeBase = useRouteBase();
|
||||
const { t } = useI18n();
|
||||
@@ -80,16 +85,13 @@ export default function AgentChatPage({
|
||||
taskCtxOn && taskContext && taskContext.agentId === agent_id
|
||||
? taskContext.taskId
|
||||
: "";
|
||||
// 纯绑定(taskBind 且无闸门):新建对话自动发出的任务开场信息。rwfx 走闸门发出,不在此列。
|
||||
const taskAutoSendText =
|
||||
// 纯绑定(taskBind,无闸门,3qfx/xdfx):新建对话进问答页后,右上角「开始回答」按钮点击时
|
||||
// 发出的任务开场信息。不再自动发(避免新建对话竞态重复发送),改由用户手动点按钮触发。
|
||||
// rwfx 走目的树闸门「开始问答」,不在此列。
|
||||
const taskStartText =
|
||||
taskBind && !taskGate && taskContext && taskContext.agentId === agent_id
|
||||
? (taskContext.openingText?.trim() ?? "")
|
||||
: "";
|
||||
// 纯绑定每次进入「新建对话」自动发一次开场信息。用「武装」标志去重,而不是绑定到 threadId——
|
||||
// 新建态首帧 useThreadChat 会把 threadId 从 useState 初值换成新 uuid(抖动),若用 threadId 做
|
||||
// 去重,会被误判成「又一个新会话」而重复发送(表现为「发了两次、开始回答后又变回一个」)。
|
||||
// 「武装」只在会话真正创建完成(isNewThread 落回 false)后才重置,故对 threadId 抖动免疫。
|
||||
const autoSentArmedRef = useRef(true);
|
||||
|
||||
// If ChatPage stashed an opening message for this agent, skip the landing
|
||||
// screen on first render so the user doesn't see a flash of the welcome UI
|
||||
@@ -106,7 +108,7 @@ export default function AgentChatPage({
|
||||
}
|
||||
};
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(
|
||||
() => hasPendingForThisAgent() || !!taskAutoSendText,
|
||||
() => hasPendingForThisAgent(),
|
||||
);
|
||||
|
||||
const { agent, isLoading: isAgentLoading } = useAgent(agent_id);
|
||||
@@ -125,19 +127,13 @@ export default function AgentChatPage({
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewThread) {
|
||||
setHasStartedConversation(hasPendingForThisAgent() || !!taskAutoSendText);
|
||||
setHasStartedConversation(hasPendingForThisAgent());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isNewThread, threadId, agent_id]);
|
||||
|
||||
// 会话真正创建完成(isNewThread 落回 false)后重新「武装」,让下一次「新建对话」能再自动发一次。
|
||||
// 不依赖 threadId,故新建态首帧的 threadId 抖动不会误触发重复发送。
|
||||
useEffect(() => {
|
||||
if (!isNewThread) autoSentArmedRef.current = true;
|
||||
}, [isNewThread]);
|
||||
|
||||
const { showNotification } = useNotification();
|
||||
const [thread, sendMessage, , streamThreadId] = useThreadStream({
|
||||
const [thread, sendMessage] = useThreadStream({
|
||||
threadId: isNewThread ? undefined : threadId,
|
||||
assistantId: agent_id,
|
||||
context: { ...settings.context, agent_id },
|
||||
@@ -234,22 +230,11 @@ export default function AgentChatPage({
|
||||
handleSubmit({ text: pending.text, files: [] });
|
||||
}, [agent_id, threadId, isNewThread, handleSubmit]);
|
||||
|
||||
// 纯绑定任务深链(3qfx/xdfx,无闸门):进入新建对话即自动发出任务开场信息(每个新建会话恰好一次)。
|
||||
//
|
||||
// 两道守卫缺一不可:
|
||||
// 1) `streamThreadId == null`:等底层 stream 真正切到全新线程再发。从已有会话点「新建对话」时
|
||||
// isNewThread 先翻 true,但 onStreamThreadId 慢一帧才归零;若那一帧就 submit,hook 仍绑定上一个
|
||||
// 会话(submit 的 threadId 选项仅在 hook 线程为空时生效)→ 开场信息发进旧对话。
|
||||
// 2) `autoSentArmedRef`:本轮新建会话是否还「待发」。只在会话创建完成(isNewThread→false)后重置,
|
||||
// 不受 threadId 抖动影响,避免同一个新建会话里重复发出。
|
||||
useEffect(() => {
|
||||
if (!taskAutoSendText) return;
|
||||
if (!isNewThread) return;
|
||||
if (streamThreadId != null) return;
|
||||
if (!autoSentArmedRef.current) return;
|
||||
autoSentArmedRef.current = false;
|
||||
handleSubmit({ text: taskAutoSendText, files: [] });
|
||||
}, [taskAutoSendText, isNewThread, streamThreadId, handleSubmit]);
|
||||
// 纯绑定任务深链(3qfx/xdfx):新建对话进问答页 landing 后,由 header 右上角「开始回答」按钮
|
||||
// 手动触发发出开场信息(见下方 header)。不再自动发,从根上避免新建对话竞态导致的重复发送。
|
||||
const handleTaskStart = useCallback(() => {
|
||||
if (taskStartText) handleSubmit({ text: taskStartText, files: [] });
|
||||
}, [handleSubmit, taskStartText]);
|
||||
|
||||
const handleClarificationSubmit = useCallback(
|
||||
(answer: string) => {
|
||||
@@ -265,7 +250,7 @@ export default function AgentChatPage({
|
||||
|
||||
const isLandingState = isNewThread && !hasStartedConversation;
|
||||
// 任务闸门面板:仅 rwfx(taskGate)在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。
|
||||
// 3qfx/xdfx(taskBind)不展示闸门,靠 taskAutoSendText 直接发出开场信息。
|
||||
// 3qfx/xdfx(taskBind)不展示闸门,新建对话进问答页后由右上角「开始回答」按钮手动发出开场信息。
|
||||
const showTaskGate = !!taskGate && !!taskId && isNewThread && !hasStartedConversation;
|
||||
const isDemoMode = env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true";
|
||||
|
||||
@@ -283,6 +268,8 @@ export default function AgentChatPage({
|
||||
always opens between the chat and the detail panel — the detail
|
||||
panel stays rightmost. */}
|
||||
<div className="flex size-full min-h-0 overflow-hidden workspace-chat-bg">
|
||||
{/* 最左:智能体列表栏(仅独立整页问答开启)。顶部下拉切换 我的/广场。 */}
|
||||
{showAgentList && <AgentChatSidebar activeAgentId={agent_id} />}
|
||||
<div className="min-h-0 min-w-0 flex-1">
|
||||
<ChatBox threadId={threadId}>
|
||||
{/* Main conversation column. `relative` hosts the floating
|
||||
@@ -305,6 +292,17 @@ export default function AgentChatPage({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
{/* 3qfx/xdfx 任务深链:新建对话进问答页后,点此发出任务开场信息开始问答(不自动发)。 */}
|
||||
{taskStartText && isLandingState && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="mr-1 bg-[var(--color-main)] text-white shadow-sm hover:bg-[var(--brand-950)]"
|
||||
onClick={handleTaskStart}
|
||||
disabled={isDemoMode}
|
||||
>
|
||||
开始回答
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip content={t.agents.newChat}>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -224,7 +224,8 @@ export default function StrategyRoutes() {
|
||||
</ChatRuntime>
|
||||
}
|
||||
/>
|
||||
{/* 行动分析:任务工作区(左对话列表 1:N 精简版;右侧带目的树闸门,与 rwfx 同一查询/显示)。
|
||||
{/* 行动分析:任务工作区(左对话列表 1:N 精简版 + 左上行动详情卡片;直接问答,
|
||||
右上角「开始回答」按钮手动发出开场信息,不走目的树闸门/覆盖确认)。
|
||||
注意:xdfx 深链传入的是「行动id」,登录时已先经 resolveDeeplinkTaskId 换成任务id。 */}
|
||||
<Route
|
||||
path="plan-task/plan-task-action/agent/:agent_id/chats"
|
||||
@@ -241,7 +242,7 @@ export default function StrategyRoutes() {
|
||||
<Route
|
||||
path=":thread_id"
|
||||
element={
|
||||
<TaskFxChat taskBase="/page/strategy/task-management/plan-task/plan-task-action" />
|
||||
<TaskBindChat taskBase="/page/strategy/task-management/plan-task/plan-task-action" />
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
@@ -290,7 +291,7 @@ export default function StrategyRoutes() {
|
||||
path="agents/:agent_id/chats/:thread_id"
|
||||
element={
|
||||
<ChatRuntime>
|
||||
<AgentChatPage />
|
||||
<AgentChatPage showAgentList />
|
||||
</ChatRuntime>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -288,7 +288,7 @@ export default function WorkspaceRoutes({
|
||||
element={
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<ChatRuntime>
|
||||
<AgentChatPage />
|
||||
<AgentChatPage showAgentList />
|
||||
</ChatRuntime>
|
||||
</WorkspaceLayout>
|
||||
}
|
||||
|
||||
+49
-3
@@ -3,7 +3,8 @@
|
||||
* - rwfx(任务FX,完整版):左对话列表 + 任务操作(编辑/下一步/清除记录)+ 跳转三情分析按钮;
|
||||
* 右侧问答带「目的树闸门」(AgentChatPage taskGate)。
|
||||
* - 3qfx(三情分析)/ xdfx(行动分析)(``lite`` 精简版):只有左对话列表(按 taskId + agentId 隔离),
|
||||
* 无任务操作、无闸门;新建对话直接带任务开场信息自动发出(AgentChatPage taskBind)。
|
||||
* 无任务操作、无闸门;新建对话进问答页后,右上角「开始回答」按钮手动发出任务开场信息(AgentChatPage taskBind)。
|
||||
* xdfx 额外在左上「当前任务」卡片下展示「行动详情」(行动名称/叙事,来自行动详情接口)。
|
||||
*
|
||||
* 左侧顶部 = 当前任务卡片(任务名称 + 简介,来自登录深链暂存的 copTaskDetail)+「新建对话」;
|
||||
* 右侧 = <Outlet/> 渲染选中的对话。
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { deleteTaskPurpose, readTaskContext } from "@/core/auth/task-purpose";
|
||||
import { readCopTaskDetail } from "@/core/auth/task-deeplink";
|
||||
import { fetchActionDetail, readCopTaskDetail, type ActionDetail } from "@/core/auth/task-deeplink";
|
||||
import { TASK_BUSINESSES, type TaskBusiness } from "@/core/auth/task-buttons";
|
||||
import { useTaskThreads } from "@/core/threads/use-task-threads";
|
||||
import { titleOfThread } from "@/core/threads/utils";
|
||||
@@ -78,6 +79,49 @@ function TaskDetailHeader() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* xdfx 专用「行动详情」卡片:放在「当前任务」卡片下面。用任务上下文里的「行动id」
|
||||
* (xdfx 深链原始 ?taskId=)调行动详情接口,展示行动名称 + 叙事。无数据时不渲染。
|
||||
*/
|
||||
function ActionDetailHeader() {
|
||||
const actionId = useMemo(() => readTaskContext()?.actionId ?? "", []);
|
||||
const [detail, setDetail] = useState<ActionDetail | null>(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 (
|
||||
<div className="mb-3 rounded-md border border-[var(--card-border)] bg-background/70 p-2.5 shadow-sm dark:border-[#2b8ffe]/40 dark:bg-[#0b2546]/70">
|
||||
<div className="text-muted-foreground mb-1 text-[11px] font-medium">行动详情</div>
|
||||
{title && (
|
||||
<div className="text-foreground line-clamp-2 text-sm font-semibold leading-snug" title={title}>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{narration && (
|
||||
<div
|
||||
className="text-muted-foreground mt-1 line-clamp-3 text-xs leading-relaxed"
|
||||
title={narration}
|
||||
>
|
||||
{narration}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 从路由路径里解析当前打开的会话 id(布局组件读不到子路由的 :thread_id,故从 path 取)。 */
|
||||
function parseThreadId(pathname: string, chatsBase: string): string | null {
|
||||
if (!pathname.startsWith(chatsBase)) return null;
|
||||
@@ -117,6 +161,8 @@ function TaskConversationsList({
|
||||
<span className="text-foreground text-sm font-semibold tracking-wide">{businessName}</span>
|
||||
</div>
|
||||
<TaskDetailHeader />
|
||||
{/* xdfx:任务详情下面再展示行动详情 */}
|
||||
{business === "xdfx" && <ActionDetailHeader />}
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-foreground text-sm font-semibold">任务对话</div>
|
||||
@@ -308,7 +354,7 @@ export function TaskFxChat({
|
||||
);
|
||||
}
|
||||
|
||||
/** 右侧对话宿主(3qfx/xdfx):AgentChatPage taskBind → 仅 1:N 绑定,新建对话直接带开场信息自动发出。 */
|
||||
/** 右侧对话宿主(3qfx/xdfx):AgentChatPage taskBind → 仅 1:N 绑定,新建对话进问答页,右上角「开始回答」按钮手动发出开场信息。 */
|
||||
export function TaskBindChat({ taskBase }: { taskBase: string }) {
|
||||
const { agent_id } = useParams<{ agent_id: string }>();
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user