feat(summarization): 非破坏性上下文压缩——保留原始对话仅在请求时瞬态压缩

重写 DeerFlowSummarizationMiddleware:将 stock before_model 的
RemoveMessage(REMOVE_ALL_MESSAGES) 改为 no-op,改在 wrap_model_call 中
瞬态压缩为 [summary, *recent],完整原始对话仍保留在 state 中,前端继续
渲染用户真实问答。summary 消息标记 hide_from_ui 且永不持久化,按 thread
缓存以避免每次模型调用都触发 LLM。前端 isHiddenFromUIMessage 额外隐藏旧
版本持久化的 name="summary" 消息。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-11 08:34:28 +08:00
commit 22222308a1
47 files changed
+2856 -1063

No files matched your search

@@ -22,6 +22,7 @@ import {
Grid3X3,
HelpCircle,
History,
Layers,
Layout,
Library,
List,
@@ -61,8 +62,13 @@ export const sidebarMenuItems: MenuItem[] = [
path: "/page/strategy/qa/multi-agent-roundtable",
},
// { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
{ id: "qa-zzjc", label: "zzjc支持", icon: Bot },
{ id: "qa-zzxd", label: "zzXD应用", icon: Layout },
{
id: "qa-zzjc",
label: "zzjc支持",
icon: Bot,
path: "/page/strategy/agents/6bf/chats/new",
},
{ id: "qa-zzxd", label: "zzXD应用", icon: Layout, path: "/page/strategy/agents/10d69686718349688e58a8790cb61024/chats/new", },
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
],
},
@@ -209,10 +215,16 @@ export const sidebarMenuItems: MenuItem[] = [
{ id: "mgmt-agents", label: "智能体管理", icon: Bot, path: "/page/strategy/agents" },
{
id: "mgmt-business-chain-mapping",
label: "业务链条管理",
label: "配置管理",
icon: Network,
path: "/page/strategy/business-chain-mapping",
},
{
id: "mgmt-business-chains",
label: "业务链条配置",
icon: Layers,
path: "/page/strategy/business-chains",
},
{ id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" },
{ id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" },
{
@@ -1,5 +1,5 @@
/**
* 业务链条管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。
* 配置管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。
*
* 表格三列:业务(只读)/ 智能体 / 业务链。编辑模式下后两列为可搜索下拉:
* - 智能体来自 `/api/agents`
@@ -129,7 +129,7 @@ export default function BusinessChainMappingPage() {
<div className="mx-auto flex min-h-full max-w-[1280px] flex-col gap-5 px-6 py-6 lg:px-8">
<div className="space-y-1">
<h1 className={`text-[26px] font-semibold tracking-[0.02em] ${isDark ? "text-slate-50" : "text-[#222222]"}`}>
业务链条管理
配置管理
</h1>
<p className={`text-sm ${isDark ? "text-slate-300/80" : "text-slate-500"}`}>
为每个业务绑定智能体与业务链。多智能体会商从对应业务自动取用此处配置的业务链。业务链仅可选择「公共」链条。
@@ -17,6 +17,8 @@ import { ActionAnalyzes } from "@/strategy-components/components/task-components
import KnowledgeBaseResourceManagement from "@/strategy-components/components/resource-management/KnowledgeBaseResourceManagement";
import NetworkActorResourceManagement from "@/strategy-components/components/resource-management/NetworkActorResourceManagement";
import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage";
import BusinessChainListPage from "@/roundtable-planning/pages/BusinessChainListPage";
import BusinessChainEditorPage from "@/roundtable-planning/pages/BusinessChainEditorPage";
import "@/roundtable-planning/styles/roundtable-planning.css";
import AgentChatPage from "./AgentChatPage";
import AgentsPage from "./AgentsPage";
@@ -174,6 +176,8 @@ export default function StrategyRoutes() {
<Route path="skills/evolution" element={<SkillEvolutionPage />} />
<Route path="memory" element={<MemoryPage />} />
<Route path="business-chain-mapping" element={<BusinessChainMappingPage />} />
<Route path="business-chains" element={<BusinessChainListPage />} />
<Route path="roundtable/chains" element={<BusinessChainEditorPage />} />
<Route
path="scheduled"
@@ -44,6 +44,7 @@ import StudioNotebooksPage from './StudioNotebooksPage';
import StudioNotebookDetailPage from './StudioNotebookDetailPage';
import RoundtablePlanningPage from '@/roundtable-planning/pages/RoundtablePlanningPage';
import BusinessChainEditorPage from '@/roundtable-planning/pages/BusinessChainEditorPage';
import BusinessChainListPage from '@/roundtable-planning/pages/BusinessChainListPage';
import DispatchChainPreviewPage from '@/roundtable-planning/pages/DispatchChainPreviewPage';
import '@/roundtable-planning/styles/roundtable-planning.css';
import { cn } from '@/lib/utils';
@@ -328,6 +329,14 @@ export default function WorkspaceRoutes({
</WorkspaceLayout>
}
/>
<Route
path="business-chains"
element={
<WorkspaceLayout {...layoutProps}>
<BusinessChainListPage />
</WorkspaceLayout>
}
/>
{/* Phase 7 临时预览入口:派活链路流程图弹窗(假数据自测,接通后台后可删) */}
<Route
path="roundtable/chain-preview"
@@ -40,11 +40,30 @@ export interface BusinessChainListDialogProps {
onConfigure: (chainId?: string) => void;
}
export function BusinessChainListDialog({
open,
onOpenChange,
export interface BusinessChainListBodyProps {
/**
* 是否激活(控制何时拉取列表):弹窗里传 `open`(打开才拉),
* 独立页面里恒为 `true`。
*/
active: boolean;
/** 进入配置页。带 chainId = 打开并切到该链条;不带 = 空白新建。 */
onConfigure: (chainId?: string) => void;
/** 列表为空时的「去创建」引导文案差异(弹窗说「点下方配置」,页面说「点右上配置」)。 */
emptyHint?: string;
}
/**
* 业务链条列表主体(筛选 tab + 滚动卡片列表)。
*
* 从 `BusinessChainListDialog` 抽出,供弹窗与独立配置页(`BusinessChainListPage`)
* 复用,保证两处的加载 / 发布 / 卡片渲染逻辑完全一致。外层负责标题栏与「配置」入口、
* 以及撑出 `flex flex-col` 高度(本组件用 `flex-1` 占满剩余空间)。
*/
export function BusinessChainListBody({
active,
onConfigure,
}: BusinessChainListDialogProps) {
emptyHint,
}: BusinessChainListBodyProps) {
const [chains, setChains] = useState<BusinessChain[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -65,9 +84,9 @@ export function BusinessChainListDialog({
}
}, []);
// 每次打开 / 切换筛选都重新拉,保证配置页改动后回来看到的是最新列表。
// 每次激活 / 切换筛选都重新拉,保证配置页改动后回来看到的是最新列表。
useEffect(() => {
if (!open) return;
if (!active) return;
let cancelled = false;
void (async () => {
setLoading(true);
@@ -84,7 +103,7 @@ export function BusinessChainListDialog({
return () => {
cancelled = true;
};
}, [open, scope]);
}, [active, scope]);
const handleTogglePublic = useCallback(
async (chain: BusinessChain) => {
@@ -101,6 +120,73 @@ export function BusinessChainListDialog({
[refresh, scope],
);
return (
<>
{/* 筛选:我的 / 公共 */}
<div className="shrink-0 flex items-center gap-1 rounded-lg border border-border bg-muted/40 p-0.5 self-start">
{(["mine", "public"] as const).map((s) => (
<button
key={s}
type="button"
onClick={() => setScope(s)}
className={cn(
"rounded-md px-3 py-1 text-xs font-medium transition",
scope === s
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
{s === "mine" ? "我的" : "公共"}
</button>
))}
</div>
<div className="custom-scrollbar -mr-1 min-h-0 flex-1 overflow-y-auto py-3 pr-1">
{loading ? (
<div className="flex items-center gap-2 px-1 py-10 text-sm text-muted-foreground">
<Loader2Icon className="size-4 animate-spin" />
正在加载业务链条…
</div>
) : error ? (
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
加载业务链条失败:{error}
</div>
) : chains.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-3 px-6 py-12 text-center">
<LayersIcon className="size-10 text-primary/40" />
<div className="text-sm font-semibold">
{scope === "public" ? "还没有公共链条" : "还没有业务链条"}
</div>
<p className="max-w-sm text-xs leading-relaxed text-muted-foreground">
{scope === "public"
? "公共链条是其他账号(或你自己)发布出来、所有人可见的链条。去「我的」里发布一条试试。"
: emptyHint ??
"业务链条是可复用的有序智能体编排。点击下方「配置」去创建第一条链条。"}
</p>
</div>
) : (
<div className="space-y-3.5">
{chains.map((chain) => (
<ChainRow
key={chain.id}
chain={chain}
publishing={publishingId === chain.id}
onSettings={() => onConfigure(chain.id)}
onTogglePublic={() => handleTogglePublic(chain)}
/>
))}
</div>
)}
</div>
</>
);
}
export function BusinessChainListDialog({
open,
onOpenChange,
onConfigure,
}: BusinessChainListDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[85vh] flex-col gap-0 sm:max-w-2xl">
@@ -114,61 +200,7 @@ export function BusinessChainListDialog({
</DialogDescription>
</DialogHeader>
{/* 筛选:我的 / 公共 */}
<div className="shrink-0 flex items-center gap-1 rounded-lg border border-border bg-muted/40 p-0.5 self-start">
{(["mine", "public"] as const).map((s) => (
<button
key={s}
type="button"
onClick={() => setScope(s)}
className={cn(
"rounded-md px-3 py-1 text-xs font-medium transition",
scope === s
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
{s === "mine" ? "我的" : "公共"}
</button>
))}
</div>
<div className="custom-scrollbar -mr-1 min-h-0 flex-1 overflow-y-auto py-3 pr-1">
{loading ? (
<div className="flex items-center gap-2 px-1 py-10 text-sm text-muted-foreground">
<Loader2Icon className="size-4 animate-spin" />
正在加载业务链条…
</div>
) : error ? (
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
加载业务链条失败:{error}
</div>
) : chains.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-3 px-6 py-12 text-center">
<LayersIcon className="size-10 text-primary/40" />
<div className="text-sm font-semibold">
{scope === "public" ? "还没有公共链条" : "还没有业务链条"}
</div>
<p className="max-w-sm text-xs leading-relaxed text-muted-foreground">
{scope === "public"
? "公共链条是其他账号(或你自己)发布出来、所有人可见的链条。去「我的」里发布一条试试。"
: "业务链条是可复用的有序智能体编排。点击下方「配置」去创建第一条链条。"}
</p>
</div>
) : (
<div className="space-y-3.5">
{chains.map((chain) => (
<ChainRow
key={chain.id}
chain={chain}
publishing={publishingId === chain.id}
onSettings={() => onConfigure(chain.id)}
onTogglePublic={() => handleTogglePublic(chain)}
/>
))}
</div>
)}
</div>
<BusinessChainListBody active={open} onConfigure={onConfigure} />
<DialogFooter className="shrink-0 border-t pt-3 sm:justify-end">
<Button onClick={() => onConfigure()} className="gap-1.5">
@@ -246,22 +278,70 @@ function ChainRow({
<div className="mt-1.5 line-clamp-2 text-sm text-muted-foreground">{chain.description}</div>
)}
{/* 席位名 tag(按顺序,→ 箭头连接发言次序) */}
<div className="mt-3.5 flex flex-wrap items-center gap-1.5">
{chain.seats.map((seat, i) => (
<Fragment key={seat.agent_id}>
{i > 0 && <ArrowRightIcon className="size-3.5 shrink-0 text-muted-foreground/50" />}
<span
className={cn(
"rounded-md border px-2 py-1 text-xs font-medium",
chainTagClasses(getAvatarTypeFor(seat.agent_id, i)),
)}
>
{seat.name || seat.agent_id}
</span>
</Fragment>
))}
</div>
{/* 席位编排:按 stage 分组——并行阶段竖向并列在虚线框里,阶段之间用 → 串联。 */}
<ChainSeatsFlow chain={chain} />
</div>
);
}
// ── 链条席位编排流(体现「同层并行 · 层间串行」)─────────────────────────────
function ChainSeatsFlow({ chain }: { chain: BusinessChain }) {
// 名称回退表:用落库快照(seats)解析 stage 里的 agent_id。
const seatById = new Map(chain.seats.map((s) => [s.agent_id, s] as const));
// 分层来源:带 stages(分层编排)就按 stages 渲染(过滤空阶段);否则每个席位各成
// 一段、顺序串行(线性链,与旧版展示一致)。
const stages =
chain.stages && chain.stages.length > 0
? chain.stages.filter((s) => s.length > 0)
: chain.seats.map((s) => [s.agent_id]);
// 跨阶段累加的全局序号 —— 决定头像配色,与编辑器 / 展平 seats 顺序对齐。
const orderById = new Map<string, number>();
let counter = 0;
for (const stageIds of stages) {
for (const id of stageIds) {
orderById.set(id, counter);
counter += 1;
}
}
const seatTag = (agentId: string) => {
const seat = seatById.get(agentId);
return (
<span
key={agentId}
className={cn(
"rounded-md border px-2 py-1 text-xs font-medium",
chainTagClasses(getAvatarTypeFor(agentId, orderById.get(agentId) ?? 0)),
)}
>
{seat?.name || agentId}
</span>
);
};
return (
<div className="mt-3.5 flex flex-wrap items-center gap-1.5">
{stages.map((stageIds, stageIdx) => (
<Fragment key={stageIdx}>
{stageIdx > 0 && (
<ArrowRightIcon className="size-3.5 shrink-0 text-muted-foreground/50" />
)}
{stageIds.length > 1 ? (
// 并行阶段:席位竖向并列在虚线框里,左上角标「并行 N」。
<div className="relative flex flex-col gap-1 rounded-lg border border-dashed border-primary/40 bg-primary/[0.04] px-1.5 pb-1.5 pt-2.5">
<span className="pointer-events-none absolute -top-2 left-1.5 rounded-full bg-card px-1.5 text-[10px] font-semibold leading-tight text-primary">
并行 {stageIds.length}
</span>
{stageIds.map((id) => seatTag(id))}
</div>
) : (
seatTag(stageIds[0])
)}
</Fragment>
))}
</div>
);
}
@@ -289,6 +289,43 @@ function ChainCard({
const deletedCount = chain.seats.length - survivingCount;
const usable = survivingCount >= CHAIN_MIN_SEATS;
// ── 席位编排(体现「同层并行 · 层间串行」)─────────────────────────────
// 名称回退表:实时 name(nameById)→ 落库快照(seat.name)→ agent_id。
const seatById = new Map(chain.seats.map((s) => [s.agent_id, s] as const));
// 带 stages(分层编排)按 stages 渲染(过滤空阶段);否则每席位各成一段、顺序串行。
const stages =
chain.stages && chain.stages.length > 0
? chain.stages.filter((s) => s.length > 0)
: chain.seats.map((s) => [s.agent_id]);
// 跨阶段累加的全局序号 —— 决定头像配色,与展平 seats 顺序对齐。
const orderById = new Map<string, number>();
let seatOrder = 0;
for (const stageIds of stages) {
for (const id of stageIds) {
orderById.set(id, seatOrder);
seatOrder += 1;
}
}
// 单个席位 tag:存活按配色,已删除置灰删除线(保留旧行为)。
const seatTag = (agentId: string) => {
const alive = isExisting(agentId);
const label = nameById.get(agentId) ?? seatById.get(agentId)?.name ?? agentId;
return (
<span
key={`tag-${agentId}`}
className={cn(
"rounded border px-1.5 py-0.5 text-[11px] font-medium",
alive
? chainTagClasses(getAvatarTypeFor(agentId, orderById.get(agentId) ?? 0))
: "border-dashed border-border bg-muted text-muted-foreground/60 line-through",
)}
title={alive ? undefined : "该智能体已不存在"}
>
{label}
</span>
);
};
return (
<button
type="button"
@@ -322,36 +359,36 @@ function ChainCard({
<div className="mt-1 line-clamp-1 text-xs text-muted-foreground">{chain.description}</div>
)}
{/* 智能体名 tag(按顺序,← 箭头连接) */}
{/* 席位编排:按 stage 分组——并行阶段竖向并列在虚线框里,阶段之间用 → 串联。
不用 <Fragment>:dev 期 tagger 会往 Fragment 注入 data-lov-id 触发 React 警告,
这里用带 key 的扁平数组(箭头 + 阶段),注入落到真实元素上即可。 */}
<div className="mt-2 flex flex-wrap items-center gap-1">
{chain.seats.flatMap((seat, i) => {
const alive = isExisting(seat.agent_id);
const label = nameById.get(seat.agent_id) ?? seat.name ?? seat.agent_id;
// 不用 <Fragment>:dev 期 tagger 会往 Fragment 注入 data-lov-id 触发 React 警告,
// 这里改成带 key 的扁平数组(箭头 + tag),注入落到真实元素上即可。
{stages.flatMap((stageIds, stageIdx) => {
const nodes: ReactNode[] = [];
if (i > 0) {
if (stageIdx > 0) {
nodes.push(
<ArrowRightIcon
key={`arrow-${seat.agent_id}`}
key={`arrow-${stageIdx}`}
className="size-3 shrink-0 text-muted-foreground/50"
/>,
);
}
nodes.push(
<span
key={`tag-${seat.agent_id}`}
className={cn(
"rounded border px-1.5 py-0.5 text-[11px] font-medium",
alive
? chainTagClasses(getAvatarTypeFor(seat.agent_id, i))
: "border-dashed border-border bg-muted text-muted-foreground/60 line-through",
)}
title={alive ? undefined : "该智能体已不存在"}
>
{label}
</span>,
);
if (stageIds.length > 1) {
// 并行阶段:席位竖向并列在虚线框里,左上角标「并行 N」。
nodes.push(
<div
key={`stage-${stageIdx}`}
className="relative flex flex-col gap-1 rounded-lg border border-dashed border-primary/40 bg-primary/[0.04] px-1.5 pb-1.5 pt-2.5"
>
<span className="pointer-events-none absolute -top-2 left-1.5 rounded-full bg-card px-1.5 text-[10px] font-semibold leading-tight text-primary">
并行 {stageIds.length}
</span>
{stageIds.map((id) => seatTag(id))}
</div>,
);
} else {
nodes.push(seatTag(stageIds[0]));
}
return nodes;
})}
</div>
@@ -123,6 +123,12 @@ export interface UseStep1IntentReturn {
clarifications: ClarificationPayload[],
overrideByKey?: Record<string, IntentClarificationSelection>,
) => void;
/**
* 用户在「当前意图摘要」卡里手动编辑后保存:直接覆盖 `intentReady`。下游(Step 2 seed
* `buildInitialIntent`、左右侧栏摘要卡、step2Locked 判定)都读 `intentReady`,改这里即全局生效。
* 持久化由调用方(页面)在更新后触发草稿保存。
*/
updateIntentReady: (next: IntentSummary) => void;
/** Abort 当前 intent 流。气泡尾部追加「(已暂停)」并 toast。 */
stopIntentStream: () => void;
/** 从草稿加载:批量 setState + 标记 init 已完成,跳过自动 runStep1Init。 */
@@ -568,6 +574,12 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
setInteractiveMessages((prev) => [...prev, msg]);
}, []);
const updateIntentReady = useCallback((next: IntentSummary) => {
// 用户手动改完意图 → 覆盖为权威值,并清掉「待澄清」态(已确认意图,无需再答问)。
setIntentReady(next);
setPendingIntentClarificationMsgId(null);
}, []);
return {
intentThreadId,
intentReady,
@@ -582,6 +594,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
runStep1Init,
sendIntentMessage,
submitIntentClarifications,
updateIntentReady,
stopIntentStream,
hydrateFromDraft,
appendLocalMessage,
@@ -0,0 +1,64 @@
/**
* 业务链条配置页(独立页面版)。
*
* 把原本挂在圆桌页顶栏「业务链条」按钮上的 `BusinessChainListDialog` 弹窗内容,
* 单独画成一个全屏页面:综合管理 → 业务链条配置 入口进来即是本页。
*
* - 列表主体复用 `BusinessChainListBody`(与弹窗同一份加载 / 发布 / 卡片逻辑)。
* - 顶部「配置」按钮 = 进入编辑器空白新建;卡片「设置」= 进入编辑器并切到该链条。
*
* 路由 base 感知:用 `useRouteNavigate` 跳转,strategy / workspace 两套路由树下
* 都能落到当前 base 的编辑器(`roundtable/chains`)。
*/
import { LayersIcon, SettingsIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useRouteNavigate } from "@/core/route-base";
import { BusinessChainListBody } from "@/roundtable-planning/components/BusinessChainListDialog";
export default function BusinessChainListPage() {
const navigate = useRouteNavigate();
// 进入编辑器:带 chainId = 打开并切到该链条;不带 = 空白新建。
// 路径写 workspace base,`useRouteNavigate` 会按当前路由 base 改写。
const goConfigure = (chainId?: string) => {
navigate(
chainId
? `/page/workspace/roundtable/chains?chainId=${encodeURIComponent(chainId)}`
: "/page/workspace/roundtable/chains",
);
};
return (
<div className="flex h-full min-h-0 flex-col bg-muted/40 text-foreground">
{/* 顶栏:标题 + 描述 + 配置入口 */}
<header className="shrink-0 border-b border-border bg-card px-6 py-4">
<div className="mx-auto flex w-full max-w-3xl items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2 text-base font-bold text-foreground">
<LayersIcon className="size-5 text-primary" />
业务链条配置
</div>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
可复用的有序智能体编排。点击卡片「设置」可进入编辑器修改该链条;「发布」后所有账号都能在「公共」里查看 / 选用。
</p>
</div>
<Button onClick={() => goConfigure()} className="shrink-0 gap-1.5">
<SettingsIcon className="size-4" />
配置
</Button>
</div>
</header>
{/* 列表主体(与弹窗共用 BusinessChainListBody) */}
<div className="mx-auto flex w-full min-h-0 max-w-3xl flex-1 flex-col px-6 py-4">
<BusinessChainListBody
active
onConfigure={goConfigure}
emptyHint="业务链条是可复用的有序智能体编排。点击右上角「配置」去创建第一条链条。"
/>
</div>
</div>
);
}
@@ -41,12 +41,14 @@ import {
ListOrdered,
Loader2,
Pencil,
Plus,
Save,
Settings,
Sparkles,
Trash2,
Upload,
Users,
X,
} from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
@@ -89,6 +91,7 @@ import {
type SeatSelectionResult,
} from "@/roundtable-planning/lib/business-chain";
import type { OrchestrationPlan } from "@/roundtable-planning/lib/dag-types";
import type { IntentSummary } from "@/roundtable-planning/api/intent";
import { getJobByDraft, startJob } from "@/roundtable-planning/api/roundtable-jobs";
import { getChain } from "@/roundtable-planning/api/chains";
import { filterRecommendCandidates } from "@/roundtable-planning/api/recommend";
@@ -499,6 +502,26 @@ export default function RoundtablePlanningPage() {
loadedFromDraftRefHolder.current = drafts.loadedFromDraftRef.current;
});
// ─── 用户手动编辑「当前意图摘要」并保存 ───────────────────────
// 先 updateIntentReady 覆盖意图,再 bump 计数触发一次草稿保存。**必须用计数 + effect**
// 而非直接调 handleManualSave:enqueueSave 闭包捕获的 getStep1Snapshot 读的是本次渲染的
// step1.intentReady,同步调用会在 setState flush 前存进旧意图(踩坑);放到 effect 里,等
// 重渲染后再保存,getStep1Snapshot 才读到新值(与 step2/step3 的计数触发器同构)。
const [intentEditTick, setIntentEditTick] = useState(0);
const handleSaveIntent = useCallback(
(next: IntentSummary) => {
step1.updateIntentReady(next);
setIntentEditTick((t) => t + 1);
},
[step1],
);
useEffect(() => {
if (intentEditTick === 0) return;
drafts.handleManualSave();
// 仅在 tick 递增时保存;handleManualSave 取最新闭包(effect 跑在重渲染之后)。
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [intentEditTick]);
// ─── 从侧边栏「多智能体会商」记录进入 ───────────────────────
// 侧边栏点击某条草稿时:不在本页 → 把 id 存进 sessionStorage 再跳转,挂载时消费;
// 已在本页 → 直接派 `roundtable:open-draft` 事件。两条路径都走 handleLoadDraft。
@@ -1106,6 +1129,7 @@ export default function RoundtablePlanningPage() {
intentReady={step1.intentReady}
timelineEntries={step1.timelineEntries}
triggerToast={triggerToast}
onSaveIntent={handleSaveIntent}
/>
</main>
</div>
@@ -1763,6 +1787,148 @@ function ModelSelectorCard({
);
}
/**
* 「当前意图摘要」编辑态:目标(单行/多行文本)、约束、关键假设(均为可增删的列表)。
* 受控本地草稿,保存时过滤空行、要求目标非空,再上抛 {@link IntentSummary}。
*/
function IntentSummaryEditor({
initial,
title,
onSave,
onCancel,
}: {
initial: IntentSummary;
title: string;
onSave: (next: IntentSummary) => void;
onCancel: () => void;
}) {
const [objective, setObjective] = useState(initial.objective ?? "");
const [constraints, setConstraints] = useState<string[]>(
initial.constraints?.length ? [...initial.constraints] : [""],
);
const [assumptions, setAssumptions] = useState<string[]>(
initial.assumptions?.length ? [...initial.assumptions] : [],
);
const objectiveInvalid = objective.trim().length === 0;
const updateRow = (
setter: React.Dispatch<React.SetStateAction<string[]>>,
idx: number,
value: string,
) => setter((prev) => prev.map((v, i) => (i === idx ? value : v)));
const removeRow = (setter: React.Dispatch<React.SetStateAction<string[]>>, idx: number) =>
setter((prev) => prev.filter((_, i) => i !== idx));
const addRow = (setter: React.Dispatch<React.SetStateAction<string[]>>) =>
setter((prev) => [...prev, ""]);
const handleSave = () => {
if (objectiveInvalid) return;
onSave({
objective: objective.trim(),
constraints: constraints.map((c) => c.trim()).filter(Boolean),
assumptions: assumptions.map((a) => a.trim()).filter(Boolean),
});
};
const rowList = (
label: string,
rows: string[],
setter: React.Dispatch<React.SetStateAction<string[]>>,
placeholder: string,
) => (
<div className="space-y-1.5">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">{label}</span>
<div className="space-y-1.5">
{rows.map((row, i) => (
<div key={i} className="flex items-center gap-1.5">
<input
value={row}
onChange={(e) => updateRow(setter, i, e.target.value)}
placeholder={placeholder}
maxLength={200}
className="min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-[11.5px] text-slate-700 focus:border-primary focus:outline-none focus:ring-1 focus:ring-[var(--brand-200)]/50"
/>
<button
type="button"
onClick={() => removeRow(setter, i)}
title="删除该项"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-slate-400 hover:bg-rose-50 hover:text-rose-500 transition-colors"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
))}
<button
type="button"
onClick={() => addRow(setter)}
className="inline-flex items-center gap-1 text-[11px] font-bold text-slate-500 hover:text-primary transition-colors"
>
<Plus className="h-3.5 w-3.5" />
添加{label}
</button>
</div>
</div>
);
return (
<div className="bg-white border border-primary/30 rounded-2xl p-4.5 shadow-[0_2px_8px_rgba(0,0,0,0.01)]">
<div className="flex items-center justify-between gap-2 border-b border-slate-100 pb-2.5 mb-3">
<div className="flex items-center gap-2 min-w-0">
<IntentSectionIcon className="w-4 h-4" />
<h2 className="text-xs font-bold text-slate-900 uppercase tracking-widest truncate">
编辑{title}
</h2>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
type="button"
onClick={handleSave}
disabled={objectiveInvalid}
title="保存"
className="flex h-7 items-center gap-1 rounded-lg bg-primary px-2.5 text-[11px] font-bold text-white hover:bg-[var(--brand-900)] disabled:bg-slate-300 disabled:cursor-not-allowed transition-colors"
>
<Check className="h-3.5 w-3.5" />
保存
</button>
<button
type="button"
onClick={onCancel}
title="取消"
className="flex h-7 w-7 items-center justify-center rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-700 transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
<div className="space-y-3">
<div className="space-y-1.5">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">核心目标</span>
<textarea
value={objective}
onChange={(e) => setObjective(e.target.value)}
rows={3}
maxLength={500}
placeholder="一句话说明本次要达成什么"
className={cn(
"w-full resize-y rounded-lg border bg-white px-2.5 py-1.5 text-[11.5px] leading-relaxed text-slate-700 focus:outline-none focus:ring-1",
objectiveInvalid
? "border-rose-300 focus:border-rose-400 focus:ring-rose-200/50"
: "border-slate-200 focus:border-primary focus:ring-[var(--brand-200)]/50",
)}
/>
{objectiveInvalid && (
<p className="text-[10px] text-rose-500">核心目标不能为空</p>
)}
</div>
{rowList("约束", constraints, setConstraints, "如:预算 / 时间 / 合规 / 范围")}
{rowList("假设", assumptions, setAssumptions, "智能体为你做的假设(可选)")}
</div>
</div>
);
}
interface IntentSummaryCardProps {
intentReady: ReturnType<typeof useStep1Intent>["intentReady"];
title?: string;
@@ -1773,6 +1939,11 @@ interface IntentSummaryCardProps {
expanded: boolean;
onToggle: () => void;
};
/**
* 提供则显示「编辑」按钮,允许用户手动修改识别出的意图(目标 / 约束 / 假设)并保存。
* 保存后回调上抛新意图,由页面覆盖 `intentReady` 并持久化草稿。不传 = 只读。
*/
onSave?: (next: IntentSummary) => void;
}
function IntentSummaryCard({
@@ -1780,9 +1951,25 @@ function IntentSummaryCard({
title = "当前意图摘要",
collapsible = false,
sidebarToggle,
onSave,
}: IntentSummaryCardProps) {
const [expanded, setExpanded] = useState(!collapsible);
const showFullDetail = !collapsible || expanded;
const [editing, setEditing] = useState(false);
if (editing && intentReady) {
return (
<IntentSummaryEditor
initial={intentReady}
title={title}
onCancel={() => setEditing(false)}
onSave={(next) => {
onSave?.(next);
setEditing(false);
}}
/>
);
}
return (
<div className="bg-white border border-slate-200/50 rounded-2xl p-4.5 shadow-[0_2px_8px_rgba(0,0,0,0.01)]">
@@ -1791,21 +1978,33 @@ function IntentSummaryCard({
<IntentSectionIcon className="w-4 h-4" />
<h2 className="text-xs font-bold text-slate-900 uppercase tracking-widest truncate">{title}</h2>
</div>
{sidebarToggle && (
<button
type="button"
onClick={sidebarToggle.onToggle}
title={sidebarToggle.expanded ? "收起侧栏" : "展开侧栏"}
aria-expanded={sidebarToggle.expanded}
className="shrink-0 flex h-7 w-7 items-center justify-center rounded-lg text-slate-500 hover:text-primary hover:bg-slate-50 transition-colors"
>
{sidebarToggle.expanded ? (
<PanelLeftClose className="h-4 w-4" />
) : (
<PanelLeftOpen className="h-4 w-4" />
)}
</button>
)}
<div className="flex items-center gap-1 shrink-0">
{onSave && intentReady && (
<button
type="button"
onClick={() => setEditing(true)}
title="编辑意图"
className="flex h-7 w-7 items-center justify-center rounded-lg text-slate-500 hover:text-primary hover:bg-slate-50 transition-colors"
>
<Pencil className="h-3.5 w-3.5" />
</button>
)}
{sidebarToggle && (
<button
type="button"
onClick={sidebarToggle.onToggle}
title={sidebarToggle.expanded ? "收起侧栏" : "展开侧栏"}
aria-expanded={sidebarToggle.expanded}
className="flex h-7 w-7 items-center justify-center rounded-lg text-slate-500 hover:text-primary hover:bg-slate-50 transition-colors"
>
{sidebarToggle.expanded ? (
<PanelLeftClose className="h-4 w-4" />
) : (
<PanelLeftOpen className="h-4 w-4" />
)}
</button>
)}
</div>
</div>
<div className="p-3 bg-slate-50/50 border border-slate-100 rounded-xl space-y-3 mr-0.5">
@@ -2052,6 +2251,8 @@ interface RightSidebarProps {
intentReady: ReturnType<typeof useStep1Intent>["intentReady"];
timelineEntries: ReturnType<typeof useStep1Intent>["timelineEntries"];
triggerToast: (msg: string) => void;
/** 用户编辑并保存意图的回调(透传给 IntentSummaryCard)。 */
onSaveIntent: (next: IntentSummary) => void;
}
function RightSidebar({
@@ -2063,6 +2264,7 @@ function RightSidebar({
intentReady,
timelineEntries,
triggerToast,
onSaveIntent,
}: RightSidebarProps) {
// Step 2 侧栏内容已迁至左侧;Step 3 沿用原逻辑不展示右侧栏。
if (currentStep !== 1) return null;
@@ -2078,7 +2280,7 @@ function RightSidebar({
modelsLoading={modelsLoading}
/>
<IntentSummaryCard intentReady={intentReady} />
<IntentSummaryCard intentReady={intentReady} onSave={onSaveIntent} />
{/* 澄清记录 Timeline */}
<div className="bg-white border border-slate-200/50 rounded-2xl p-4.5 shadow-[0_2px_8px_rgba(0,0,0,0.01)] flex-1 flex flex-col min-h-[300px]">
@@ -37,10 +37,10 @@ export const KanbanColumn: React.FC<KanbanColumnProps> = ({
const columnClass = isDark
? 'border border-[rgba(67,89,124,0.46)] bg-[linear-gradient(180deg,rgba(18,36,67,0.96),rgba(12,28,54,0.96))] shadow-[0_18px_40px_rgba(3,10,22,0.18)]'
: 'border border-[#E8E8E8] bg-[linear-gradient(180deg,#FFFFFF_0%,#FFF9F9_100%)] shadow-[0_14px_28px_rgba(245,34,45,0.06)]';
: 'border border-[#E8E8E8] bg-[linear-gradient(180deg,#FFFFFF_0%,#FFF9F9_100%)] shadow-[0_2px_8px_rgba(34,34,34,0.08)]';
return (
<section className={`flex h-full flex-col rounded-[28px] p-4 ${columnClass}`}>
<section className={`flex h-full flex-col rounded p-4 ${columnClass}`}>
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-2">
<div className="flex min-w-0 items-center gap-2">
@@ -53,7 +53,7 @@ export const KanbanColumn: React.FC<KanbanColumnProps> = ({
{title}
</h2>
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs ${
className={`inline-flex rounded-sm px-2 py-0.5 text-xs ${
isDark ? 'bg-[rgba(77,141,255,0.14)] text-[#9CCBFF]' : 'bg-[#FFF1F0] text-[#CF1322]'
}`}
>
@@ -76,14 +76,14 @@ export const KanbanColumn: React.FC<KanbanColumnProps> = ({
/>
</div>
<p className={`mt-2 text-xs leading-6 ${isDark ? 'text-slate-400' : 'text-[#8C8C8C]'}`}>
<p className={`mt-1.5 text-xs leading-5 ${isDark ? 'text-slate-400' : 'text-[#A6A6A6]'}`}>
按时间倒序展示,支持分页切换。
</p>
<div className="mt-4 flex-1 space-y-4">
<div className="mt-3 flex-1 space-y-3">
{tasks.length === 0 ? (
<div
className={`flex h-full min-h-[220px] flex-col items-center justify-center rounded-[22px] border border-dashed ${
className={`flex h-full min-h-[220px] flex-col items-center justify-center rounded border border-dashed ${
isDark
? 'border-[rgba(87,111,149,0.42)] bg-[rgba(12,24,44,0.46)] text-slate-400'
: 'border-[#F3C4C8] bg-[#FFF7F7] text-[#A6A6A6]'
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ArrowRight, Bot, Layers, Plus, Search, Users, X } from 'lucide-react';
import { ArrowRight, Bot, CircleCheck, Inbox, Layers, Loader, Plus, Search, Users, X } from 'lucide-react';
import dayjs from 'dayjs';
import { DateRangePicker, DateRangePickerProps, DateRangeValue, Loading } from 'tdesign-react';
import { useNavigate } from 'react-router-dom';
@@ -170,7 +170,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
setShowAnalysisModal(false);
};
// 单智能体分析:用「业务链条管理」里为本业务配置的智能体,跳转到其问答页(新会话),
// 单智能体分析:用「配置管理」里为本业务配置的智能体,跳转到其问答页(新会话),
// 把任务名称/简介作为开场消息暂存,由 AgentChatPage 读取 sessionStorage.pendingAgentMessage
// 自动发出并开始问答。未配置智能体则提示去配置。
const handleSingleAgentAnalysis = async () => {
@@ -184,7 +184,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
/* ignore → 下面按未配置处理 */
}
if (!agentId) {
toast.error('该业务尚未在「业务链条管理」中配置单智能体');
toast.error('该业务尚未在「配置管理」中配置单智能体');
return;
}
const text = [
@@ -226,7 +226,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
const toolbarClass = isDark
? 'border border-[rgba(67,89,124,0.46)] bg-[rgba(10,23,42,0.94)] text-white shadow-[0_16px_36px_rgba(3,10,22,0.2)]'
: 'border border-[#E8E8E8] bg-white text-[#222222] shadow-[0_14px_30px_rgba(245,34,45,0.06)]';
: 'border border-[#E8E8E8] bg-white text-[#222222] shadow-[0_2px_8px_rgba(34,34,34,0.08)]';
const metricClass = isDark
? 'border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)]'
@@ -237,31 +237,31 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
: 'border-[#E8E8E8] bg-white text-[#222222] placeholder:text-[#A6A6A6]';
const metrics = [
{ label: '全部任务', value: unanalyzedTotal + runningTotal + completedTotal },
{ label: '未分析', value: unanalyzedTotal },
{ label: '分析中', value: runningTotal },
{ label: '已完成', value: completedTotal },
{ label: '全部任务', value: unanalyzedTotal + runningTotal + completedTotal, icon: Layers, fg: '#222222', chipBg: '#F5F5F5', chipFg: '#7F7F7F' },
{ label: '未分析', value: unanalyzedTotal, icon: Inbox, fg: '#F5222D', chipBg: '#FEEDED', chipFg: '#F5222D' },
{ label: '分析中', value: runningTotal, icon: Loader, fg: '#1677FF', chipBg: '#EAF4FF', chipFg: '#1677FF' },
{ label: '已完成', value: completedTotal, icon: CircleCheck, fg: '#52C41A', chipBg: '#EEFCF7', chipFg: '#189D72' },
];
return (
<div className={shellClass}>
<div className="mx-auto flex min-h-full max-w-[1680px] flex-col gap-4 px-6 py-6 lg:px-8">
<section className={`rounded-[28px] p-5 ${toolbarClass}`}>
<section className={`rounded px-6 py-5 ${toolbarClass}`}>
<div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div className="space-y-3">
<div className="flex items-center gap-2 text-xs">
<span className={`rounded-full px-3 py-1 ${isDark ? 'bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : 'bg-[#FFF1F0] text-[#CF1322]'}`}>
<div className="space-y-2.5">
<div className="flex items-center gap-1.5 text-xs">
<span className={`rounded-sm border px-2 py-0.5 font-medium ${isDark ? 'border-[rgba(108,161,255,0.24)] bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : 'border-[#FFD6D6] bg-[#FFF1F0] text-[#CF1322]'}`}>
{category}
</span>
<ArrowRight className={`h-3.5 w-3.5 ${isDark ? 'text-slate-500' : 'text-[#A6A6A6]'}`} />
<span className={`rounded-full px-3 py-1 ${isDark ? 'bg-[rgba(34,197,94,0.14)] text-[#89E8B8]' : 'bg-[#FFF6E8] text-[#D46B08]'}`}>
<ArrowRight className={`h-3 w-3 ${isDark ? 'text-slate-500' : 'text-[#A6A6A6]'}`} />
<span className={`rounded-sm border px-2 py-0.5 font-medium ${isDark ? 'border-[rgba(103,232,165,0.2)] bg-[rgba(34,197,94,0.14)] text-[#89E8B8]' : 'border-[#FFE2B8] bg-[#FFF6E8] text-[#D46B08]'}`}>
{subCategory}
</span>
</div>
<div>
<h1 className={`text-[28px] font-semibold ${isDark ? 'text-white' : 'text-[#222222]'}`}>{title}</h1>
<p className={`mt-2 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
<h1 className={`text-[24px] font-semibold leading-8 ${isDark ? 'text-white' : 'text-[#222222]'}`}>{title}</h1>
<p className={`mt-1.5 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
面向规划任务的情绪与倾向分析看板,支持快速检索、会商启动与结果回看。
</p>
</div>
@@ -269,7 +269,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
<div className="flex flex-col gap-3 xl:items-end">
<div className="flex flex-wrap items-center gap-3">
<label className={`flex h-11 min-w-[320px] items-center gap-3 rounded-xl border px-4 ${inputClass}`}>
<label className={`flex h-10 min-w-[300px] items-center gap-2 rounded-sm border px-3 ${inputClass}`}>
<Search className={`h-4 w-4 ${isDark ? 'text-slate-400' : 'text-[#A6A6A6]'}`} />
<input
value={searchQuery}
@@ -281,6 +281,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
</label>
<DateRangePicker
className="sentiment-date-range"
size="large"
value={range}
presets={presets}
onChange={setRangeChange}
@@ -292,7 +293,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
<button
type="button"
className={`inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm ${isDark ? 'bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : 'bg-[#FFF1F0] text-[#CF1322]'}`}
className={`inline-flex items-center gap-2 rounded-sm px-4 py-2 text-sm ${isDark ? 'bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : 'bg-[#F5222D] text-white hover:bg-[#CF1322]'}`}
>
<Plus className="h-4 w-4" />
新建任务
@@ -302,21 +303,30 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
</section>
<section className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
{metrics.map((item, index) => (
<div key={item.label} className={`rounded-[24px] p-5 ${metricClass}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-[#8C8C8C]'}`}>{item.label}</p>
<p className={`mt-3 text-[36px] font-semibold ${isDark ? 'text-white' : index === 0 ? 'text-[#222222]' : 'text-[#F5222D]'}`}>
{item.value}
</p>
</div>
<div className={`flex h-12 w-12 items-center justify-center rounded-2xl ${isDark ? 'bg-[rgba(77,141,255,0.18)] text-[#9CCBFF]' : 'bg-[#FFF1F0] text-[#CF1322]'}`}>
<Layers className="h-5 w-5" />
{metrics.map((item) => {
const Icon = item.icon;
return (
<div key={item.label} className={`rounded p-4 ${metricClass}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-[#7F7F7F]'}`}>{item.label}</p>
<p
className={`mt-2 text-[30px] font-semibold leading-none ${isDark ? 'text-white' : ''}`}
style={isDark ? undefined : { color: item.fg }}
>
{item.value}
</p>
</div>
<div
className={`flex h-11 w-11 items-center justify-center rounded ${isDark ? 'bg-[rgba(77,141,255,0.18)] text-[#9CCBFF]' : ''}`}
style={isDark ? undefined : { backgroundColor: item.chipBg, color: item.chipFg }}
>
<Icon className="h-5 w-5" />
</div>
</div>
</div>
</div>
))}
);
})}
</section>
<section className="grid grid-cols-1 items-stretch gap-4 xl:grid-cols-3">
@@ -375,7 +385,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
{showAnalysisModal && selectedTask ? (
<div className="fixed inset-0 z-[2000] flex items-center justify-center bg-black/45 px-4">
<div className={`w-full max-w-[560px] rounded-[28px] border p-6 ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(10,23,42,0.98)] text-white' : 'border-[#E8E8E8] bg-white text-[#222222] shadow-[0_18px_36px_rgba(0,0,0,0.12)]'}`}>
<div className={`w-full max-w-[560px] rounded border p-6 ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(10,23,42,0.98)] text-white' : 'border-[#E8E8E8] bg-white text-[#222222] shadow-[0_8px_32px_rgba(34,34,34,0.16)]'}`}>
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-xl font-semibold">选择分析模式</h3>
@@ -386,7 +396,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
<button
type="button"
onClick={() => setShowAnalysisModal(false)}
className={`rounded-xl p-2 ${isDark ? 'hover:bg-[rgba(21,42,72,0.9)]' : 'hover:bg-[#FFF1F0]'}`}
className={`rounded-sm p-2 ${isDark ? 'hover:bg-[rgba(21,42,72,0.9)]' : 'hover:bg-[#FFF1F0]'}`}
>
<X className="h-4 w-4" />
</button>
@@ -396,7 +406,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
<button
type="button"
onClick={handleOpenTaskDetail}
className={`flex w-full items-center justify-between rounded-[22px] border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(15,33,60,0.92)] hover:border-[rgba(110,168,255,0.46)]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
className={`flex w-full items-center justify-between rounded border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(15,33,60,0.92)] hover:border-[rgba(110,168,255,0.46)]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
>
<div>
<div className="font-medium">查看任务详情</div>
@@ -410,7 +420,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
<button
type="button"
onClick={handleOpenConsultation}
className={`flex w-full items-center justify-between rounded-[22px] border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(15,33,60,0.92)] hover:border-[rgba(103,232,165,0.42)]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
className={`flex w-full items-center justify-between rounded border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(15,33,60,0.92)] hover:border-[rgba(103,232,165,0.42)]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
>
<div className="flex items-start gap-3">
<Users className={`mt-0.5 h-5 w-5 ${isDark ? 'text-[#89E8B8]' : 'text-[#CF1322]'}`} />
@@ -427,7 +437,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
<button
type="button"
onClick={handleSingleAgentAnalysis}
className={`flex w-full items-center justify-between rounded-[22px] border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(15,33,60,0.92)] hover:border-[rgba(167,139,250,0.46)]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
className={`flex w-full items-center justify-between rounded border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(15,33,60,0.92)] hover:border-[rgba(167,139,250,0.46)]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
>
<div className="flex items-start gap-3">
<Bot className={`mt-0.5 h-5 w-5 ${isDark ? 'text-[#C4B5FD]' : 'text-[#7C3AED]'}`} />
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ArrowRight, Bot, Layers, Plus, Search, Users, X } from 'lucide-react';
import { ArrowRight, Bot, CircleCheck, Inbox, Layers, Loader, Plus, Search, Users, X } from 'lucide-react';
import { DateRangePicker, DateRangePickerProps, DateRangeValue, Loading } from 'tdesign-react';
import dayjs from 'dayjs';
import { useNavigate } from 'react-router-dom';
@@ -170,7 +170,7 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
setShowAnalysisModal(false);
};
// 单智能体分析:用「业务链条管理」里为本业务配置的智能体,跳转到其问答页(新会话),
// 单智能体分析:用「配置管理」里为本业务配置的智能体,跳转到其问答页(新会话),
// 把任务名称/简介作为开场消息暂存,由 AgentChatPage 读取 sessionStorage.pendingAgentMessage
// 自动发出并开始问答。未配置智能体则提示去配置。
const handleSingleAgentAnalysis = async () => {
@@ -184,7 +184,7 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
/* ignore → 下面按未配置处理 */
}
if (!agentId) {
toast.error('该业务尚未在「业务链条管理」中配置单智能体');
toast.error('该业务尚未在「配置管理」中配置单智能体');
return;
}
const text = [
@@ -224,115 +224,105 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
? 'strategy-data h-full overflow-y-auto bg-[radial-gradient(circle_at_top_left,rgba(27,94,168,0.28),transparent_32%),linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)] text-slate-100'
: 'strategy-data h-full overflow-y-auto bg-[radial-gradient(circle_at_top_left,rgba(245,34,45,0.08),transparent_26%),linear-gradient(180deg,#fffdfd_0%,#fff7f7_42%,#fafafa_100%)] text-[#222222]';
const toolbarClass = isDark
? 'border border-[rgba(110,136,176,0.24)] bg-[rgba(10,23,42,0.94)] text-slate-100 shadow-[0_16px_36px_rgba(3,10,22,0.2)]'
: 'border border-[#E8E8E8] bg-white text-[#222222] shadow-[0_2px_8px_rgba(34,34,34,0.08)]';
const metricCardClass = isDark
? 'border border-[rgba(110,136,176,0.24)] bg-[linear-gradient(180deg,rgba(13,31,55,0.98),rgba(8,19,34,0.98))] shadow-[0_14px_42px_rgba(2,8,20,0.30)]'
: 'border border-[#E8E8E8] bg-white shadow-[0_12px_30px_rgba(245,34,45,0.06)]';
: 'border border-[#E8E8E8] bg-white';
const inputClass = isDark
? 'border-[rgba(110,136,176,0.3)] bg-[rgba(8,23,42,0.92)] text-slate-100 placeholder:text-slate-500'
: 'border-[#E8E8E8] bg-white text-[#222222] placeholder:text-[#A6A6A6] shadow-[0_4px_14px_rgba(245,34,45,0.04)]';
: 'border-[#E8E8E8] bg-white text-[#222222] placeholder:text-[#A6A6A6]';
const panelGlowClass = isDark
? 'before:pointer-events-none before:absolute before:inset-x-8 before:top-0 before:h-px before:bg-[linear-gradient(90deg,transparent,rgba(96,165,250,0.45),transparent)]'
: '';
const metrics = [
{
label: '全部任务',
value: unanalyzedTotal + runningTotal + completedTotal,
icon: Layers,
accent: 'from-[#4D8DFF] to-[#7CC9FF]',
},
{
label: '未分析',
value: unanalyzedTotal,
icon: Bot,
accent: 'from-[#7C9BFF] to-[#5EEAD4]',
},
{
label: '分析中',
value: runningTotal,
icon: Layers,
accent: 'from-[#22C55E] to-[#67E8F9]',
},
{
label: '已完成',
value: completedTotal,
icon: Bot,
accent: 'from-[#38BDF8] to-[#A78BFA]',
},
{ label: '全部任务', value: unanalyzedTotal + runningTotal + completedTotal, icon: Layers, fg: '#222222', chipBg: '#F5F5F5', chipFg: '#7F7F7F' },
{ label: '未分析', value: unanalyzedTotal, icon: Inbox, fg: '#F5222D', chipBg: '#FEEDED', chipFg: '#F5222D' },
{ label: '分析中', value: runningTotal, icon: Loader, fg: '#1677FF', chipBg: '#EAF4FF', chipFg: '#1677FF' },
{ label: '已完成', value: completedTotal, icon: CircleCheck, fg: '#52C41A', chipBg: '#EEFCF7', chipFg: '#189D72' },
];
return (
<div className={shellClass}>
<div className={`relative mx-auto flex min-h-full max-w-[1680px] flex-col gap-5 px-6 py-6 lg:px-8 ${panelGlowClass}`}>
<div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
<div className="space-y-3">
<div className="flex items-center gap-3">
<div>
<h1 className={`text-[26px] font-semibold tracking-[0.02em] ${isDark ? 'text-slate-50' : 'text-[#222222]'}`}>
{title}
</h1>
<p className={`mt-1 text-sm ${isDark ? 'text-slate-300/80' : 'text-slate-500'}`}>
面向规划任务的任务分析看板,聚焦目标拆解、受众判断和策略阻力识别,支持快速检索、启动分析和查看会商结果。
</p>
</div>
</div>
<div className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs ${isDark ? 'border-[rgba(110,136,176,0.28)] bg-[rgba(9,25,46,0.72)] text-slate-300' : 'border-slate-200 bg-white text-slate-600'}`}>
<span>{category}</span>
<ArrowRight className="h-3.5 w-3.5 opacity-70" />
<span>{subCategory}</span>
<div className={`relative mx-auto flex min-h-full max-w-[1680px] flex-col gap-4 px-6 py-6 lg:px-8 ${panelGlowClass}`}>
<section className={`rounded px-6 py-5 ${toolbarClass}`}>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<h1 className={`text-[24px] font-semibold leading-8 ${isDark ? 'text-slate-50' : 'text-[#222222]'}`}>{title}</h1>
<div className="flex items-center gap-1.5 text-xs">
<span className={`rounded-sm border px-2 py-0.5 font-medium ${isDark ? 'border-[rgba(108,161,255,0.24)] bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : 'border-[#FFD6D6] bg-[#FFF1F0] text-[#CF1322]'}`}>
{category}
</span>
<ArrowRight className={`h-3 w-3 ${isDark ? 'text-slate-500' : 'text-[#A6A6A6]'}`} />
<span className={`rounded-sm border px-2 py-0.5 font-medium ${isDark ? 'border-[rgba(103,232,165,0.2)] bg-[rgba(34,197,94,0.14)] text-[#89E8B8]' : 'border-[#FFE2B8] bg-[#FFF6E8] text-[#D46B08]'}`}>
{subCategory}
</span>
</div>
</div>
<p className={`mt-1.5 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
面向规划任务的任务分析看板,聚焦目标拆解、受众判断和策略阻力识别,支持快速检索、启动分析和查看会商结果。
</p>
<div className="flex flex-col gap-3 xl:items-end">
<div className="flex flex-wrap items-center gap-3">
<label className={`flex h-11 min-w-[320px] items-center gap-3 rounded-2xl border px-4 ${inputClass}`}>
<Search className="h-4 w-4 text-[#7CC9FF]" />
<input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
onKeyDown={handleKeyDown}
className="w-full bg-transparent text-sm outline-none"
placeholder="搜索任务名称、摘要或负责人"
/>
</label>
<DateRangePicker
className="sentiment-date-range"
value={dateRange}
presets={presets}
enableTimePicker
clearable
popupProps={{ overlayClassName: 'sentiment-date-range-popup' }}
onChange={setRangeChange}
<div className="mt-4 flex flex-wrap items-center gap-3">
<label className={`flex h-10 min-w-[240px] flex-1 items-center gap-2 rounded-sm border px-3 ${inputClass}`}>
<Search className={`h-4 w-4 ${isDark ? 'text-slate-400' : 'text-[#A6A6A6]'}`} />
<input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
onKeyDown={handleKeyDown}
className="w-full bg-transparent text-sm outline-none"
placeholder="搜索任务名称、摘要或负责人"
/>
</div>
</label>
<DateRangePicker
className="sentiment-date-range"
size="large"
value={dateRange}
presets={presets}
enableTimePicker
clearable
popupProps={{ overlayClassName: 'sentiment-date-range-popup' }}
onChange={setRangeChange}
/>
<button
type="button"
className={`inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm ${isDark ? 'bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : 'bg-[#FFF1F0] text-[#CF1322]'}`}
className={`inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-sm px-4 py-2 text-sm ${isDark ? 'bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : 'bg-[#F5222D] text-white hover:bg-[#CF1322]'}`}
>
<Plus className="h-4 w-4" />
新建任务
</button>
</div>
</div>
</section>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
{metrics.map(({ label, value, icon: Icon, accent }) => (
<div key={label} className={`rounded-3xl p-5 ${metricCardClass}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-300/78' : 'text-slate-500'}`}>{label}</p>
<p className={`mt-3 text-3xl font-semibold tracking-[0.03em] ${isDark ? 'text-white' : 'text-[#222222]'}`}>
{value}
</p>
</div>
<div className={`flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br ${accent} shadow-[inset_0_1px_0_rgba(255,255,255,0.18)]`}>
<Icon className="h-5 w-5 text-white" />
{metrics.map((item) => {
const Icon = item.icon;
return (
<div key={item.label} className={`rounded p-4 ${metricCardClass}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-[#7F7F7F]'}`}>{item.label}</p>
<p
className={`mt-2 text-[30px] font-semibold leading-none ${isDark ? 'text-white' : ''}`}
style={isDark ? undefined : { color: item.fg }}
>
{item.value}
</p>
</div>
<div
className={`flex h-11 w-11 items-center justify-center rounded ${isDark ? 'bg-[rgba(77,141,255,0.18)] text-[#9CCBFF]' : ''}`}
style={isDark ? undefined : { backgroundColor: item.chipBg, color: item.chipFg }}
>
<Icon className="h-5 w-5" />
</div>
</div>
</div>
</div>
))}
);
})}
</div>
<div className="grid auto-rows-auto grid-cols-1 items-stretch gap-4 xl:grid-cols-3">
@@ -342,7 +332,7 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
<KanbanColumn
title="未分析"
count={unanalyzedTotal}
color="bg-[#7C9BFF]"
color={isDark ? 'bg-[#7CC9FF]' : 'bg-[#F5222D]'}
tasks={unanalyzedList}
taskType="unanalyzed"
pageIndexSearch={pageIndexChange}
@@ -358,7 +348,7 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
<KanbanColumn
title="分析中"
count={runningTotal}
color="bg-[#22C55E]"
color={isDark ? 'bg-[#5EEAD4]' : 'bg-[#1677FF]'}
tasks={runningList}
taskType="running"
pageIndexSearch={pageIndexChange}
@@ -374,7 +364,7 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
<KanbanColumn
title="已完成"
count={completedTotal}
color="bg-[#38BDF8]"
color={isDark ? 'bg-[#67E8A5]' : 'bg-[#52C41A]'}
tasks={completedList}
taskType="completed"
pageIndexSearch={pageIndexChange}
@@ -387,19 +377,19 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
</div>
{showAnalysisModal && selectedTask ? (
<div className="fixed inset-0 z-[2000] flex items-center justify-center bg-[rgba(3,10,22,0.62)] px-4 backdrop-blur-sm">
<div className={`w-full max-w-[560px] rounded-[28px] border p-6 ${isDark ? 'border-[rgba(110,136,176,0.26)] bg-[linear-gradient(180deg,rgba(10,29,54,0.98),rgba(8,20,37,0.98))] text-slate-100 shadow-[0_28px_80px_rgba(2,8,20,0.42)]' : 'border-slate-200 bg-white text-slate-900 shadow-xl'}`}>
<div className="fixed inset-0 z-[2000] flex items-center justify-center bg-black/45 px-4">
<div className={`w-full max-w-[560px] rounded border p-6 ${isDark ? 'border-[rgba(110,136,176,0.26)] bg-[linear-gradient(180deg,rgba(10,29,54,0.98),rgba(8,20,37,0.98))] text-slate-100 shadow-[0_28px_80px_rgba(2,8,20,0.42)]' : 'border-[#E8E8E8] bg-white text-[#222222] shadow-[0_8px_32px_rgba(34,34,34,0.16)]'}`}>
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-xl font-semibold">选择分析模式</h3>
<p className={`mt-2 text-sm ${isDark ? 'text-slate-300/78' : 'text-slate-500'}`}>
<p className={`mt-2 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
当前任务:{selectedTask.taskName}
</p>
</div>
<button
type="button"
onClick={() => setShowAnalysisModal(false)}
className={`rounded-xl p-2 transition ${isDark ? 'hover:bg-[rgba(15,41,74,0.88)]' : 'hover:bg-slate-100'}`}
className={`rounded-sm p-2 transition ${isDark ? 'hover:bg-[rgba(15,41,74,0.88)]' : 'hover:bg-[#FFF1F0]'}`}
>
<X className="h-4 w-4" />
</button>
@@ -409,15 +399,18 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
<button
type="button"
onClick={handleOpenTaskDetail}
className={`group flex items-center justify-between rounded-2xl border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(110,136,176,0.24)] bg-[rgba(10,30,54,0.9)] hover:border-[rgba(96,165,250,0.48)] hover:bg-[rgba(12,38,69,0.96)]' : 'border-slate-200 bg-slate-50 hover:border-sky-200 hover:bg-sky-50'}`}
className={`group flex items-center justify-between rounded border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(110,136,176,0.24)] bg-[rgba(10,30,54,0.9)] hover:border-[rgba(96,165,250,0.48)] hover:bg-[rgba(12,38,69,0.96)]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
>
<div className="flex items-center gap-4">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[linear-gradient(180deg,#1677FF,#0EA5E9)]">
<Layers className="h-5 w-5 text-white" />
<div
className={`flex h-10 w-10 items-center justify-center rounded-sm ${isDark ? 'bg-[linear-gradient(180deg,#1677FF,#0EA5E9)] text-white' : ''}`}
style={isDark ? undefined : { backgroundColor: '#EAF4FF', color: '#0079FE' }}
>
<Layers className="h-5 w-5" />
</div>
<div>
<div className="font-medium">查看任务详情</div>
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-slate-500'}`}>
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-[#575757]'}`}>
进入 B1 系统查看任务详情。
</div>
</div>
@@ -428,15 +421,18 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
<button
type="button"
onClick={handleOpenConsultation}
className={`group flex items-center justify-between rounded-2xl border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(110,136,176,0.24)] bg-[rgba(10,30,54,0.9)] hover:border-[rgba(45,212,191,0.48)] hover:bg-[rgba(10,42,58,0.96)]' : 'border-slate-200 bg-slate-50 hover:border-emerald-200 hover:bg-emerald-50'}`}
className={`group flex items-center justify-between rounded border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(110,136,176,0.24)] bg-[rgba(10,30,54,0.9)] hover:border-[rgba(45,212,191,0.48)] hover:bg-[rgba(10,42,58,0.96)]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
>
<div className="flex items-center gap-4">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[linear-gradient(180deg,#14B8A6,#22C55E)]">
<Users className="h-5 w-5 text-white" />
<div
className={`flex h-10 w-10 items-center justify-center rounded-sm ${isDark ? 'bg-[linear-gradient(180deg,#14B8A6,#22C55E)] text-white' : ''}`}
style={isDark ? undefined : { backgroundColor: '#FEEDED', color: '#CF1322' }}
>
<Users className="h-5 w-5" />
</div>
<div>
<div className="font-medium">多智能体会商</div>
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-slate-500'}`}>
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-[#575757]'}`}>
多个 AI 智能体从不同视角开展讨论并形成共识。
</div>
</div>
@@ -447,15 +443,18 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
<button
type="button"
onClick={handleSingleAgentAnalysis}
className={`group flex items-center justify-between rounded-2xl border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(110,136,176,0.24)] bg-[rgba(10,30,54,0.9)] hover:border-[rgba(167,139,250,0.48)] hover:bg-[rgba(28,24,58,0.96)]' : 'border-slate-200 bg-slate-50 hover:border-violet-200 hover:bg-violet-50'}`}
className={`group flex items-center justify-between rounded border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(110,136,176,0.24)] bg-[rgba(10,30,54,0.9)] hover:border-[rgba(167,139,250,0.48)] hover:bg-[rgba(28,24,58,0.96)]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
>
<div className="flex items-center gap-4">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[linear-gradient(180deg,#7C3AED,#A78BFA)]">
<Bot className="h-5 w-5 text-white" />
<div
className={`flex h-10 w-10 items-center justify-center rounded-sm ${isDark ? 'bg-[linear-gradient(180deg,#7C3AED,#A78BFA)] text-white' : ''}`}
style={isDark ? undefined : { backgroundColor: '#F3EDFF', color: '#7C3AED' }}
>
<Bot className="h-5 w-5" />
</div>
<div>
<div className="font-medium">单智能体分析</div>
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-slate-500'}`}>
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-[#575757]'}`}>
由单个 AI 智能体直接对该任务进行分析问答。
</div>
</div>
@@ -118,7 +118,7 @@ export const TaskCard: React.FC<TaskCardProps> = ({
const panelClass = isDark
? 'border border-[rgba(67,89,124,0.46)] bg-[rgba(19,39,72,0.9)] shadow-[0_14px_34px_rgba(3,10,22,0.2)] hover:border-[rgba(110,168,255,0.52)]'
: 'border border-[#E8E8E8] bg-white shadow-[0_10px_24px_rgba(245,34,45,0.06)] hover:border-[#F6B0B4] hover:shadow-[0_16px_32px_rgba(245,34,45,0.12)]';
: 'border border-[#E8E8E8] bg-white shadow-[0_2px_8px_rgba(34,34,34,0.08)] hover:border-[#F6B0B4] hover:shadow-[0_4px_16px_rgba(34,34,34,0.12)]';
const typeBadgeClass = isDark
? 'bg-[rgba(77,141,255,0.14)] text-[#A9D2FF] border border-[rgba(108,161,255,0.24)]'
@@ -151,25 +151,25 @@ export const TaskCard: React.FC<TaskCardProps> = ({
return (
<article
className={`group rounded-[24px] p-4 transition-all duration-200 ${panelClass}`}
className={`group rounded p-4 transition-all duration-200 ${panelClass}`}
onClick={handleCardClick}
>
<div className="flex items-start justify-between gap-3">
<span
className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium ${typeBadgeClass}`}
className={`inline-flex items-center gap-1.5 rounded-sm px-2 py-1 text-xs font-medium ${typeBadgeClass}`}
>
<TypeIcon className="h-3.5 w-3.5" />
{typeConfig.label}
</span>
<span
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium ${statusClass}`}
className={`inline-flex items-center rounded-sm px-2 py-1 text-xs font-medium ${statusClass}`}
>
{getColumnLabel(task.column)}
</span>
</div>
<h3
className={`mt-5 text-[16px] font-semibold leading-7 ${
className={`mt-4 text-[16px] font-semibold leading-6 ${
isDark ? 'text-white' : 'text-[#222222]'
}`}
>
@@ -177,9 +177,10 @@ export const TaskCard: React.FC<TaskCardProps> = ({
</h3>
<p
className={`mt-3 min-h-[56px] text-sm leading-7 ${
className={`mt-3 line-clamp-3 min-h-[63px] text-sm leading-[21px] ${
isDark ? 'text-slate-300' : 'text-[#575757]'
}`}
title={task.overview || undefined}
>
{task.overview || '暂无任务摘要'}
</p>
@@ -204,9 +205,9 @@ export const TaskCard: React.FC<TaskCardProps> = ({
<button
type="button"
onClick={handleActionClick}
className={`mt-5 flex h-12 w-full items-center justify-center gap-2 rounded-full text-sm font-medium transition ${actionClass}`}
className={`mt-4 flex h-9 w-full items-center justify-center gap-1.5 rounded-sm text-sm font-medium transition ${actionClass}`}
>
{isCompleted ? <Eye className="h-4 w-4" /> : <Play className="h-4 w-4" />}
{isCompleted ? <Eye className="h-3.5 w-3.5" /> : <Play className="h-3.5 w-3.5" />}
{isCompleted ? '查看结果' : '开始分析'}
</button>
</article>
@@ -0,0 +1,437 @@
import { TaskListItem } from './TaskManagement';
/**
* 任务看板假数据 —— 用于在没有后端 `/task/cop-tasks` / `/three/cop-three-tasks`
* 接口时,给「任务分析」「三情分析」两个页面提供演示数据。
*
* 用法:把组件里的 `await service.post(url, payload)` 换成
* `await mockCopTasks('task' | 'sentiment', taskType, payload)`
* 返回结构与真实接口保持一致:`{ data: { records, total } }`。
*/
export type MockColumnType = 'unanalyzed' | 'running' | 'completed';
export type MockDataset = 'task' | 'sentiment';
interface MockTaskSeed {
taskName: string;
overview: string;
taskDirection: string;
taskLevel: number; // 1 一般 / 2 紧急 / 3 加急
taskType: number; // 1 单任务 / 2 场景 / 3 会商 / 4 多智能体 / 5 检索
}
/** 列 → 状态码,与组件内 taskStatusMap 对齐。 */
const COLUMN_STATUS: Record<MockColumnType, number> = {
unanalyzed: 0,
running: 20,
completed: 30,
};
/** 生成一条完整的 TaskListItem,仅覆盖有意义的字段,其余给安全默认值。 */
function makeTask(
id: number,
column: MockColumnType,
seed: MockTaskSeed,
createTime: string,
updateTime: string,
): TaskListItem {
return {
createBy: 1001,
createTime,
deptId: null,
id,
isTop: 0,
issuingDept: '战略规划处',
keywords: null,
overview: seed.overview,
planTaskTop: 0,
platform: null,
ptaskId: null,
recommendedBasis: null,
scene: '',
sourceType: 1,
step: null,
taskContent: seed.overview,
taskDirection: seed.taskDirection,
taskEndDate: '2026-06-30 18:00:00',
taskLevel: seed.taskLevel,
taskName: seed.taskName,
taskNo: `T-${String(id).padStart(6, '0')}`,
taskStartDate: '2026-06-01 09:00:00',
taskStatus: COLUMN_STATUS[column],
taskSuperiorFlag: 0,
taskType: seed.taskType,
column,
updateBy: 1001,
updateTime,
endCount: column === 'completed' ? 1 : 0,
sumAll: 1,
progress: column === 'completed' ? 100 : column === 'running' ? 60 : 0,
taskStatusData: null,
} as TaskListItem;
}
/** 「任务分析」页(业务码 6BF)数据种子。 */
const TASK_SEEDS: Record<MockColumnType, MockTaskSeed[]> = {
unanalyzed: [
{
taskName: '夏季文旅消费季传播任务分析',
overview:
'围绕"夏季文旅消费季"主题,梳理传播目标、核心受众与潜在策略阻力,输出可执行的任务拆解建议。',
taskDirection: '王磊',
taskLevel: 2,
taskType: 1,
},
{
taskName: '新能源补贴政策解读专题',
overview:
'针对新一轮新能源汽车补贴政策,分析公众关注焦点与误读风险点,形成政策解读传播要点清单。',
taskDirection: '李婷',
taskLevel: 1,
taskType: 5,
},
{
taskName: '城市更新片区舆论引导任务',
overview:
'对老城区改造涉及的拆迁、安置等敏感议题进行任务目标拆解,识别需要重点回应的群体诉求。',
taskDirection: '张伟',
taskLevel: 3,
taskType: 2,
},
{
taskName: '高校毕业季就业服务宣传',
overview:
'梳理面向应届毕业生的就业服务政策传播任务,明确分阶段触达节奏与渠道组合。',
taskDirection: '赵敏',
taskLevel: 1,
taskType: 1,
},
{
taskName: '防汛应急科普传播任务',
overview:
'汛期来临前,分析防汛减灾科普内容的传播目标与受众分层,制定预警信息发布预案。',
taskDirection: '陈昊',
taskLevel: 2,
taskType: 1,
},
{
taskName: '乡村振兴成果展示任务',
overview:
'围绕本地乡村振兴典型案例,拆解成果展示传播任务,确定故事化表达与素材采集方向。',
taskDirection: '周琳',
taskLevel: 1,
taskType: 2,
},
],
running: [
{
taskName: '社区养老服务满意度专题分析',
overview:
'正在对社区居家养老服务的群众反馈进行任务分析,重点判断受众诉求与服务短板。',
taskDirection: '孙强',
taskLevel: 2,
taskType: 4,
},
{
taskName: '中小企业纾困政策落地评估',
overview:
'多智能体协同分析纾困政策落地过程中的执行阻力与企业关切,形成阶段性研判。',
taskDirection: '吴静',
taskLevel: 3,
taskType: 4,
},
{
taskName: '城市轨道交通延误舆情研判',
overview:
'针对近期地铁延误事件,正在拆解舆论传播路径与关键节点,评估引导策略有效性。',
taskDirection: '郑凯',
taskLevel: 2,
taskType: 3,
},
{
taskName: '食品安全抽检结果发布任务',
overview:
'正在分析食品安全抽检通报的发布口径与受众反应,优化信息透明度与安抚策略。',
taskDirection: '冯雪',
taskLevel: 1,
taskType: 1,
},
],
completed: [
{
taskName: '春节文化活动传播复盘',
overview:
'已完成春节系列文化活动的传播任务分析,沉淀受众画像与高互动内容规律。',
taskDirection: '黄涛',
taskLevel: 1,
taskType: 3,
},
{
taskName: '医保改革政策宣传任务',
overview:
'已完成医保改革政策的传播目标拆解与受众分层,输出分群体沟通话术。',
taskDirection: '宋佳',
taskLevel: 2,
taskType: 4,
},
{
taskName: '城市营商环境优化宣传',
overview:
'已完成营商环境优化举措的任务分析,明确企业端与公众端的差异化传播重点。',
taskDirection: '高鹏',
taskLevel: 1,
taskType: 2,
},
{
taskName: '老旧小区加装电梯议题分析',
overview:
'已完成加装电梯涉及的邻里诉求与阻力识别,形成多方协商沟通建议。',
taskDirection: '何丽',
taskLevel: 2,
taskType: 3,
},
{
taskName: '生态环保主题宣传周复盘',
overview:
'已完成环保宣传周的传播效果分析,总结渠道贡献度与可复用素材。',
taskDirection: '罗勇',
taskLevel: 1,
taskType: 1,
},
{
taskName: '青年人才引进政策解读',
overview:
'已完成人才引进政策的受众判断与策略阻力分析,输出落地传播节奏表。',
taskDirection: '谭芳',
taskLevel: 1,
taskType: 5,
},
],
};
/** 「三情分析」页(业务码 3Q)数据种子。 */
const SENTIMENT_SEEDS: Record<MockColumnType, MockTaskSeed[]> = {
unanalyzed: [
{
taskName: '暑期旅游热点情绪监测',
overview:
'对暑期热门景区相关讨论进行情绪与倾向分析,识别游客吐槽集中点与正向口碑话题。',
taskDirection: '王磊',
taskLevel: 2,
taskType: 1,
},
{
taskName: '电价调整公众情绪研判',
overview:
'围绕居民阶梯电价调整,分析公众情绪走向与主要担忧,判断需要重点疏导的群体。',
taskDirection: '李婷',
taskLevel: 3,
taskType: 5,
},
{
taskName: '校园周边交通安全舆情',
overview:
'对校园周边交通安全话题进行三情分析,梳理家长群体情绪与诉求强度。',
taskDirection: '张伟',
taskLevel: 2,
taskType: 1,
},
{
taskName: '景区门票预约制讨论分析',
overview:
'分析门票预约制相关讨论的情绪分布,识别便利性与公平性两类核心关切。',
taskDirection: '赵敏',
taskLevel: 1,
taskType: 2,
},
{
taskName: '夜间经济噪音投诉情绪',
overview:
'对夜市、夜间演出引发的噪音投诉进行情绪分析,区分居民与商户的不同立场。',
taskDirection: '陈昊',
taskLevel: 1,
taskType: 1,
},
],
running: [
{
taskName: '台风预警信息传播情绪追踪',
overview:
'正在分析台风预警发布后的公众情绪变化,评估恐慌情绪与配合度的动态走势。',
taskDirection: '孙强',
taskLevel: 3,
taskType: 4,
},
{
taskName: '学区划分调整舆论情绪',
overview:
'多智能体协同追踪学区调整话题的情绪热度与对立观点,研判潜在升级风险。',
taskDirection: '吴静',
taskLevel: 2,
taskType: 4,
},
{
taskName: '网络热点事件情绪态势',
overview:
'正在对近期网络热点事件进行三情分析,识别情绪峰值节点与关键意见领袖。',
taskDirection: '郑凯',
taskLevel: 2,
taskType: 3,
},
],
completed: [
{
taskName: '春运出行体验情绪复盘',
overview:
'已完成春运期间出行体验的情绪与倾向分析,沉淀满意度变化与高频抱怨主题。',
taskDirection: '黄涛',
taskLevel: 1,
taskType: 3,
},
{
taskName: '惠民政策落地情绪评估',
overview:
'已完成惠民政策落地后的公众情绪评估,输出正向反馈与改进诉求清单。',
taskDirection: '宋佳',
taskLevel: 2,
taskType: 4,
},
{
taskName: '城市内涝应对情绪分析',
overview:
'已完成强降雨内涝事件的三情分析,总结情绪演化曲线与有效安抚措施。',
taskDirection: '高鹏',
taskLevel: 2,
taskType: 1,
},
{
taskName: '景区限流措施满意度分析',
overview:
'已完成节假日景区限流措施的情绪分析,明确游客接受度与体验改进方向。',
taskDirection: '何丽',
taskLevel: 1,
taskType: 2,
},
{
taskName: '公共交通票价听证情绪',
overview:
'已完成票价听证会相关讨论的情绪与立场分析,输出分群体沟通建议。',
taskDirection: '罗勇',
taskLevel: 1,
taskType: 5,
},
],
};
const SEEDS: Record<MockDataset, Record<MockColumnType, MockTaskSeed[]>> = {
task: TASK_SEEDS,
sentiment: SENTIMENT_SEEDS,
};
/** 给同一列里的卡片生成错落有致的时间,便于「按时间倒序」展示。 */
function buildTimes(index: number): { createTime: string; updateTime: string } {
const day = String(28 - index).padStart(2, '0');
const hour = String(9 + (index % 8)).padStart(2, '0');
return {
createTime: `2026-05-${day} ${hour}:15:00`,
updateTime: `2026-06-0${(index % 9) + 1} ${hour}:40:00`,
};
}
/** 数据集 → 列 → 完整任务列表(按 id 区分,避免两页 key 冲突)。 */
function buildPool(dataset: MockDataset, column: MockColumnType): TaskListItem[] {
const base = dataset === 'task' ? 10000 : 20000;
const columnOffset = column === 'unanalyzed' ? 0 : column === 'running' ? 300 : 600;
return SEEDS[dataset][column].map((seed, index) => {
const { createTime, updateTime } = buildTimes(index);
return makeTask(base + columnOffset + index, column, seed, createTime, updateTime);
});
}
interface MockCopTasksPayload {
content?: string;
pageNum?: number;
pageSize?: number;
taskStatus?: number[];
}
/** 根据请求里的 taskStatus 数组反推当前查询的是哪一列。 */
function resolveColumn(taskStatus?: number[]): MockColumnType {
const list = Array.isArray(taskStatus) ? taskStatus : [];
if (list.includes(20)) return 'running';
if (list.includes(0) || list.includes(10)) return 'unanalyzed';
return 'completed';
}
/**
* 模拟 `/task/cop-tasks` / `/three/cop-three-tasks` 接口的 body:
* 按列返回分页后的假数据,支持按 `content` 对任务名/摘要做模糊过滤。
*/
function buildCopTasksBody(dataset: MockDataset, payload: MockCopTasksPayload) {
const { content = '', pageNum = 1, pageSize = 10, taskStatus } = payload;
const column = resolveColumn(taskStatus);
let pool = buildPool(dataset, column);
const keyword = content.trim();
if (keyword) {
pool = pool.filter(
(item) =>
item.taskName.includes(keyword) || item.overview.includes(keyword),
);
}
const total = pool.length;
const start = (pageNum - 1) * pageSize;
const records = pool.slice(start, start + pageSize);
return { code: 0, message: 'ok', data: { records, total } };
}
/** 命中本 mock 的接口路径 → 对应数据集。 */
const MOCK_ROUTES: Array<{ match: string; dataset: MockDataset }> = [
{ match: '/three/cop-three-tasks', dataset: 'sentiment' },
{ match: '/task/cop-tasks', dataset: 'task' },
];
/** 判断某个请求 url 是否应走假数据。 */
export function matchTaskBoardMock(url?: string): MockDataset | null {
if (!url) return null;
const hit = MOCK_ROUTES.find((route) => url.includes(route.match));
return hit ? hit.dataset : null;
}
/**
* 临时假数据 axios adapter:命中任务看板接口时直接返回 mock body,不发真实请求。
* 仅用于演示,撤掉假数据时删除 request.ts 中对它的引用即可。
*/
export function taskBoardMockAdapter(config: {
url?: string;
data?: unknown;
}): Promise<{ data: unknown; status: number; statusText: string; headers: Record<string, unknown>; config: unknown }> {
const dataset = matchTaskBoardMock(config.url) ?? 'task';
let payload: MockCopTasksPayload = {};
try {
payload =
typeof config.data === 'string'
? JSON.parse(config.data)
: ((config.data as MockCopTasksPayload) ?? {});
} catch {
payload = {};
}
const body = buildCopTasksBody(dataset, payload);
return new Promise((resolve) => {
// 模拟网络延迟,让 Loading 态可见。
setTimeout(() => {
resolve({
data: body,
status: 200,
statusText: 'OK',
headers: {},
config,
});
}, 280);
});
}
+6
View File
@@ -1,4 +1,6 @@
import axios from 'axios'
// === 临时假数据(任务分析 / 三情分析看板):撤掉时删除本 import 及下方 request 拦截器中的对应块即可 ===
import { matchTaskBoardMock, taskBoardMockAdapter } from '@/strategy-components/components/task-components/task-mock-data'
// axios.interceptors
const service = axios.create({
// baseURL: '', // url = base url + request url
@@ -31,6 +33,10 @@ service.interceptors.response.use(
)
service.interceptors.request.use(
config => {
// === 临时假数据:任务看板两个接口直接走本地 mock,不发真实请求 ===
if (matchTaskBoardMock(config.url)) {
config.adapter = taskBoardMockAdapter as any;
}
// let token: any = localStorage.getItem('token');
config.headers['Authorization'] = `Bearer admin`;
// if (token) {