Merge remote-tracking branch 'origin/master' into xuhd
This commit is contained in:
commit
06508cfb7c
27 files changed
+1054
-72
No files matched your search
@@ -119,7 +119,8 @@
|
||||
border: 1px solid;
|
||||
border-color: rgba(43, 143, 254, 0.6);
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
position: absolute;
|
||||
left: 108px;
|
||||
top: 0;
|
||||
|
||||
@@ -207,6 +207,12 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
icon: Settings,
|
||||
children: [
|
||||
{ id: "mgmt-agents", label: "智能体管理", icon: Bot, path: "/page/strategy/agents" },
|
||||
{
|
||||
id: "mgmt-business-chain-mapping",
|
||||
label: "业务链条管理",
|
||||
icon: Network,
|
||||
path: "/page/strategy/business-chain-mapping",
|
||||
},
|
||||
{ id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" },
|
||||
{ id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" },
|
||||
{
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 业务链条管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。
|
||||
*
|
||||
* 表格三列:业务(只读)/ 智能体 / 业务链。编辑模式下后两列为可搜索下拉:
|
||||
* - 智能体来自 `/api/agents`
|
||||
* - 业务链来自 `/api/roundtable-chains?scope=public`(只列公共链,保证全局共享时各账号都能解析)
|
||||
* 保存调 `PUT /api/business-mapping/{code}`。映射全局共享、无用户隔离。
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Loading, Select } from "tdesign-react";
|
||||
import { Link2, Pencil, Save, Users, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { listAgents } from "@/core/agents/api";
|
||||
import type { Agent } from "@/core/agents/types";
|
||||
import { listChains } from "@/roundtable-planning/api/chains";
|
||||
import type { BusinessChain } from "@/roundtable-planning/lib/business-chain";
|
||||
import {
|
||||
listBusinessMappings,
|
||||
updateBusinessMapping,
|
||||
type BusinessMapping,
|
||||
} from "@/strategy-components/api/business-mapping";
|
||||
|
||||
interface DraftSelection {
|
||||
agentId: string | null;
|
||||
chainId: string | null;
|
||||
}
|
||||
|
||||
export default function BusinessChainMappingPage() {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
|
||||
const [rows, setRows] = useState<BusinessMapping[]>([]);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [chains, setChains] = useState<BusinessChain[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingCode, setEditingCode] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<DraftSelection>({ agentId: null, chainId: null });
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [mappingRes, agentRes, chainRes] = await Promise.allSettled([
|
||||
listBusinessMappings(),
|
||||
listAgents(),
|
||||
listChains("public"),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
if (mappingRes.status === "fulfilled") setRows(mappingRes.value);
|
||||
else toast.error(`加载业务映射失败:${mappingRes.reason}`);
|
||||
if (agentRes.status === "fulfilled") setAgents(agentRes.value);
|
||||
if (chainRes.status === "fulfilled") setChains(chainRes.value);
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const agentOptions = useMemo(
|
||||
() => agents.map((a) => ({ label: a.name || a.id, value: a.id })),
|
||||
[agents],
|
||||
);
|
||||
const chainOptions = useMemo(
|
||||
() => chains.map((c) => ({ label: c.title || c.id, value: c.id })),
|
||||
[chains],
|
||||
);
|
||||
|
||||
const beginEdit = (row: BusinessMapping) => {
|
||||
setEditingCode(row.businessCode);
|
||||
setDraft({ agentId: row.agentId, chainId: row.chainId });
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingCode(null);
|
||||
setDraft({ agentId: null, chainId: null });
|
||||
};
|
||||
|
||||
const handleSave = async (row: BusinessMapping) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const agentName = draft.agentId
|
||||
? agents.find((a) => a.id === draft.agentId)?.name ?? row.agentName ?? null
|
||||
: null;
|
||||
const chainTitle = draft.chainId
|
||||
? chains.find((c) => c.id === draft.chainId)?.title ?? row.chainTitle ?? null
|
||||
: null;
|
||||
const updated = await updateBusinessMapping(row.businessCode, {
|
||||
agentId: draft.agentId,
|
||||
agentName,
|
||||
chainId: draft.chainId,
|
||||
chainTitle,
|
||||
});
|
||||
if (!updated) {
|
||||
toast.error(`业务「${row.label}」不存在,保存失败`);
|
||||
return;
|
||||
}
|
||||
setRows((prev) => prev.map((r) => (r.businessCode === row.businessCode ? updated : r)));
|
||||
toast.success(`已保存「${row.label}」的配置`);
|
||||
cancelEdit();
|
||||
} catch (err) {
|
||||
toast.error(`保存失败:${(err as Error).message}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const shellClass = isDark
|
||||
? "h-full overflow-y-auto bg-[linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)] text-slate-100"
|
||||
: "h-full overflow-y-auto bg-[linear-gradient(180deg,#fffdfd_0%,#fff7f7_42%,#fafafa_100%)] text-[#222222]";
|
||||
const cardClass = isDark
|
||||
? "border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)] shadow-[0_16px_36px_rgba(3,10,22,0.2)]"
|
||||
: "border border-[#E8E8E8] bg-white shadow-[0_14px_30px_rgba(245,34,45,0.06)]";
|
||||
const headRowClass = isDark ? "text-slate-400" : "text-[#8C8C8C]";
|
||||
const rowBorder = isDark ? "border-[rgba(67,89,124,0.32)]" : "border-[#F0F0F0]";
|
||||
const mutedText = isDark ? "text-slate-500" : "text-[#A6A6A6]";
|
||||
const codeBadge = isDark
|
||||
? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]"
|
||||
: "bg-[#FFF1F0] text-[#CF1322]";
|
||||
|
||||
return (
|
||||
<div className={shellClass}>
|
||||
<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"}`}>
|
||||
为每个业务绑定智能体与业务链。多智能体会商从对应业务自动取用此处配置的业务链。业务链仅可选择「公共」链条。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Loading loading={loading} showOverlay={false}>
|
||||
<div className={`overflow-hidden rounded-3xl ${cardClass}`}>
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className={`text-left text-xs uppercase tracking-widest ${headRowClass}`}>
|
||||
<th className="px-6 py-4 font-semibold">业务</th>
|
||||
<th className="px-6 py-4 font-semibold">智能体</th>
|
||||
<th className="px-6 py-4 font-semibold">业务链</th>
|
||||
<th className="px-6 py-4 text-right font-semibold">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 && !loading ? (
|
||||
<tr>
|
||||
<td colSpan={4} className={`px-6 py-12 text-center ${mutedText}`}>
|
||||
暂无业务数据
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
rows.map((row) => {
|
||||
const editing = editingCode === row.businessCode;
|
||||
return (
|
||||
<tr key={row.businessCode} className={`border-t ${rowBorder}`}>
|
||||
{/* 业务(只读) */}
|
||||
<td className="px-6 py-4 align-middle">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[15px] font-semibold ${isDark ? "text-white" : "text-[#222222]"}`}>
|
||||
{row.label}
|
||||
</span>
|
||||
<span className={`rounded-full px-2 py-0.5 text-[11px] font-mono ${codeBadge}`}>
|
||||
{row.businessCode}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* 智能体 */}
|
||||
<td className="px-6 py-4 align-middle">
|
||||
{editing ? (
|
||||
<Select
|
||||
className="min-w-[200px]"
|
||||
value={draft.agentId ?? undefined}
|
||||
options={agentOptions}
|
||||
filterable
|
||||
clearable
|
||||
placeholder="搜索并选择智能体"
|
||||
onChange={(v) => setDraft((d) => ({ ...d, agentId: (v as string) ?? null }))}
|
||||
/>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Users className={`h-3.5 w-3.5 ${mutedText}`} />
|
||||
{row.agentName || row.agentId ? (
|
||||
<span>{row.agentName || row.agentId}</span>
|
||||
) : (
|
||||
<span className={mutedText}>未配置</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* 业务链 */}
|
||||
<td className="px-6 py-4 align-middle">
|
||||
{editing ? (
|
||||
<Select
|
||||
className="min-w-[220px]"
|
||||
value={draft.chainId ?? undefined}
|
||||
options={chainOptions}
|
||||
filterable
|
||||
clearable
|
||||
placeholder="搜索并选择业务链(公共)"
|
||||
onChange={(v) => setDraft((d) => ({ ...d, chainId: (v as string) ?? null }))}
|
||||
/>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Link2 className={`h-3.5 w-3.5 ${mutedText}`} />
|
||||
{row.chainTitle || row.chainId ? (
|
||||
<span>{row.chainTitle || row.chainId}</span>
|
||||
) : (
|
||||
<span className={mutedText}>未配置</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* 操作 */}
|
||||
<td className="px-6 py-4 text-right align-middle">
|
||||
{editing ? (
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => handleSave(row)}
|
||||
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium disabled:opacity-50 ${
|
||||
isDark ? "bg-[rgba(34,197,94,0.18)] text-[#86EFAC]" : "bg-[#EEFCF7] text-[#0F9D6C]"
|
||||
}`}
|
||||
>
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={cancelEdit}
|
||||
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium disabled:opacity-50 ${
|
||||
isDark ? "bg-[rgba(110,136,176,0.18)] text-slate-300" : "bg-[#F5F5F5] text-[#575757]"
|
||||
}`}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => beginEdit(row)}
|
||||
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium ${
|
||||
isDark ? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]" : "bg-[#FFF1F0] text-[#CF1322]"
|
||||
}`}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
编辑
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Loading>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanni
|
||||
import "@/roundtable-planning/styles/roundtable-planning.css";
|
||||
import AgentChatPage from "./AgentChatPage";
|
||||
import AgentsPage from "./AgentsPage";
|
||||
import BusinessChainMappingPage from "./BusinessChainMappingPage";
|
||||
import ChatPage from "./ChatPage";
|
||||
import ChatsPage from "./ChatsPage";
|
||||
import AdminUserLeaderboardPage from "./AdminUserLeaderboardPage";
|
||||
@@ -172,6 +173,7 @@ export default function StrategyRoutes() {
|
||||
<Route path="skills" element={<SkillsPage />} />
|
||||
<Route path="skills/evolution" element={<SkillEvolutionPage />} />
|
||||
<Route path="memory" element={<MemoryPage />} />
|
||||
<Route path="business-chain-mapping" element={<BusinessChainMappingPage />} />
|
||||
|
||||
<Route
|
||||
path="scheduled"
|
||||
|
||||
@@ -211,6 +211,13 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
||||
[artifactsBySeat],
|
||||
);
|
||||
|
||||
// 只保留「有产出文件」的席位;加载中 / 出错的席位仍保留以展示其状态。
|
||||
// 全部席位都没有文件(且都已加载完成、无错误)时,整块「生成的报告文件」隐藏。
|
||||
const seatsWithFiles = useMemo(
|
||||
() => artifactsBySeat.filter((s) => s.loading || s.error || s.files.length > 0),
|
||||
[artifactsBySeat],
|
||||
);
|
||||
|
||||
const toggleSeat = useCallback((key: string) => {
|
||||
setExpandedSeats((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -424,21 +431,20 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Section 3: Generated files per seat —— 点击「查看」进入沙箱分栏预览 */}
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<Download className="w-4 h-4 text-primary" />
|
||||
生成的报告文件
|
||||
<span className="text-[10px] font-mono font-semibold text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">
|
||||
{totalFileCount}
|
||||
</span>
|
||||
</h4>
|
||||
{/* Section 3: Generated files per seat —— 点击「查看」进入沙箱分栏预览。
|
||||
仅当存在「有产出文件 / 加载中 / 出错」的席位时才渲染;全部席位无文件则整块隐藏。 */}
|
||||
{seatsWithFiles.length > 0 && (
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<Download className="w-4 h-4 text-primary" />
|
||||
生成的报告文件
|
||||
<span className="text-[10px] font-mono font-semibold text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">
|
||||
{totalFileCount}
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
{artifactsBySeat.length === 0 ? (
|
||||
<p className="text-[11px] text-slate-400 italic">尚未初始化席位线程,无法列出文件。</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{artifactsBySeat.map((seat) => (
|
||||
{seatsWithFiles.map((seat) => (
|
||||
<div key={seat.agentName} className="border border-slate-150 rounded-xl">
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-2 bg-slate-50 border-b border-slate-150 rounded-t-xl">
|
||||
<span className="text-[11.5px] font-bold text-slate-800 truncate">{seat.displayName}</span>
|
||||
@@ -501,8 +507,8 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-2 border-t border-slate-100 flex items-center justify-between flex-wrap gap-4">
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
* (`/api/roundtable-chains`,按登录用户隔离)。在 Step 2 圆桌研讨里,总控会**按
|
||||
* 链条顺序逐个派活**(见 `docs/multi-agent-business-chain-dev.md` §6)。
|
||||
*
|
||||
* 这里只放纯类型 / 常量,不含 React 状态或请求逻辑(CRUD 在 `api/chains.ts`)。
|
||||
* 这里放纯类型 / 常量,以及不依赖 React 的纯函数(如 `buildChainSeatSelection`);
|
||||
* 不含状态或请求逻辑(CRUD 在 `api/chains.ts`)。
|
||||
*/
|
||||
|
||||
import { buildPlanFromStages } from "./dag-types";
|
||||
|
||||
/** 链条里的一个有序席位。`seats[0]` 是第一个发言者。 */
|
||||
export interface ChainSeat {
|
||||
agent_id: string;
|
||||
@@ -86,6 +89,55 @@ export interface SeatSelectionResult {
|
||||
export const CHAIN_MIN_SEATS = 2;
|
||||
export const CHAIN_MAX_SEATS = 8;
|
||||
|
||||
/**
|
||||
* 由一条业务链条 + 当前候选智能体,构造 `SeatSelectionResult`。
|
||||
*
|
||||
* 与 `BusinessChainPicker.handleUse` 同源逻辑,抽成纯函数供「自动选链」复用
|
||||
* (会商自动化串联:从任务页带 chainId 进圆桌时跳过选链弹窗,直接据此进 Step 2):
|
||||
* - 仅保留候选池里仍存在的存活席位;存活不足 `CHAIN_MIN_SEATS` 返回 null(调用方回退弹窗);
|
||||
* - 链条带分层、过滤存活后仍含并行 stage / 任一阶段目标 / 总控提示 → 走 `dag`(默认 fast);
|
||||
* - 否则走线性 `chain`,席位顺序 = 链条顺序。
|
||||
*
|
||||
* `candidates` 为 `{ agent_id, name }` 列表(一般来自 `filterRecommendCandidates(listAgents())`)。
|
||||
*/
|
||||
export function buildChainSeatSelection(
|
||||
chain: BusinessChain,
|
||||
candidates: ReadonlyArray<{ agent_id: string; name: string }>,
|
||||
): SeatSelectionResult | null {
|
||||
const existingIds = new Set(candidates.map((c) => c.agent_id));
|
||||
const nameById = new Map(candidates.map((c) => [c.agent_id, c.name]));
|
||||
const seatNameById = new Map(chain.seats.map((s) => [s.agent_id, s.name]));
|
||||
const nameOf = (id: string) => nameById.get(id) ?? seatNameById.get(id) ?? id;
|
||||
|
||||
const survivingSeats = chain.seats.filter((s) => existingIds.has(s.agent_id));
|
||||
if (survivingSeats.length < CHAIN_MIN_SEATS) return null;
|
||||
|
||||
const chainMeta = { id: chain.id, title: chain.title };
|
||||
|
||||
if (chain.stages && chain.stages.length > 0) {
|
||||
const surviveIds = survivingSeats.map((s) => s.agent_id);
|
||||
const plan = buildPlanFromStages(
|
||||
chain.stages,
|
||||
surviveIds,
|
||||
true,
|
||||
chain.stageGoals,
|
||||
chain.coordinatorPrompt,
|
||||
);
|
||||
const hasParallel = plan.stages.some((st) => st.agentIds.length > 1);
|
||||
const hasGoal = plan.stages.some((st) => (st.goal?.trim() ?? "") !== "");
|
||||
const hasCoord = (plan.coordinatorPrompt?.trim() ?? "") !== "";
|
||||
if (hasParallel || hasGoal || hasCoord) {
|
||||
const agents = plan.stages
|
||||
.flatMap((st) => st.agentIds)
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id) }));
|
||||
return { agents, mode: "dag", chain: chainMeta, plan, dagExecMode: "fast" };
|
||||
}
|
||||
}
|
||||
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id) }));
|
||||
return { agents, mode: "chain", chain: chainMeta };
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 `getAvatarTypeFor()` 产出的头像配色(blue/purple/emerald/amber/pink/human)
|
||||
* 映射成 tag chip 的 tailwind 类(背景 + 文字 + 边框,含深色变体)。
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
* 6) 模型选择全局默认 `availableModels[0].name`,Persona 覆盖更高优先级
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import {
|
||||
Activity,
|
||||
Check,
|
||||
@@ -84,14 +84,44 @@ import {
|
||||
type RoleEntry,
|
||||
type SelectedAgent,
|
||||
} from "@/roundtable-planning/lib/roundtable-constants";
|
||||
import type { SeatSelectionResult } from "@/roundtable-planning/lib/business-chain";
|
||||
import {
|
||||
buildChainSeatSelection,
|
||||
type SeatSelectionResult,
|
||||
} from "@/roundtable-planning/lib/business-chain";
|
||||
import type { OrchestrationPlan } from "@/roundtable-planning/lib/dag-types";
|
||||
import { getJobByDraft, startJob } from "@/roundtable-planning/api/roundtable-jobs";
|
||||
import { getChain } from "@/roundtable-planning/api/chains";
|
||||
import { filterRecommendCandidates } from "@/roundtable-planning/api/recommend";
|
||||
import { listAgents } from "@/core/agents";
|
||||
import { listBusinessMappings } from "@/strategy-components/api/business-mapping";
|
||||
|
||||
/** 从任务页带进来的预填(经 navigation state 传递)。见会商自动化串联(模块 C)。 */
|
||||
interface RoundtablePrefill {
|
||||
/** 任务名称(注入 Step 1 首条用户消息)。 */
|
||||
taskTitle?: string;
|
||||
/** 任务描述(注入 Step 1 首条用户消息)。 */
|
||||
taskGoal?: string;
|
||||
/** 来源业务码(如 "6BF"):据此查业务映射拿到要自动选用的业务链。 */
|
||||
businessCode?: string;
|
||||
/** 是否进入即自动开跑 Step 1 意图分析。 */
|
||||
autoStart?: boolean;
|
||||
}
|
||||
|
||||
export default function RoundtablePlanningPage() {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// ─── 会商自动化串联(模块 C):任务页带进来的预填 ─────────────────
|
||||
// 只在首次渲染读一次 navigation state;随后清掉 history state,避免刷新/后退重放。
|
||||
const prefillRef = useRef<RoundtablePrefill | null>(
|
||||
((location.state as { roundtablePrefill?: RoundtablePrefill } | null)?.roundtablePrefill) ?? null,
|
||||
);
|
||||
// 由 prefill.businessCode 解析出的「自动选用业务链」结果;有值时「进入研讨」跳过选链弹窗直接进 Step 2。
|
||||
const [autoChainResult, setAutoChainResult] = useState<SeatSelectionResult | null>(null);
|
||||
const autoStartedRef = useRef(false);
|
||||
const prefillClearedRef = useRef(false);
|
||||
|
||||
const { models: availableModels, isLoading: modelsLoading } = useModels();
|
||||
|
||||
@@ -604,6 +634,69 @@ export default function RoundtablePlanningPage() {
|
||||
[buildCurrentStep2Snapshot, mergeActiveRun, step2, step3Report, triggerToast],
|
||||
);
|
||||
|
||||
// ─── 会商自动化串联(模块 C) ──────────────────────────────────
|
||||
// 消费 prefill 后清掉 history state,避免刷新 / 后退重放自动开跑与自动选链。
|
||||
useEffect(() => {
|
||||
if (prefillClearedRef.current || !prefillRef.current) return;
|
||||
prefillClearedRef.current = true;
|
||||
try {
|
||||
window.history.replaceState({ ...window.history.state, usr: null }, "");
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}, []);
|
||||
|
||||
// C2:带 autoStart 时,等 Step 1 意图线程就绪后自动把「任务名称/描述」作为首条用户消息发出,
|
||||
// 触发任务理解智能体自动分析(只发一次)。
|
||||
useEffect(() => {
|
||||
const prefill = prefillRef.current;
|
||||
if (!prefill?.autoStart || autoStartedRef.current) return;
|
||||
if (currentStep !== 1) return;
|
||||
if (!step1.intentThreadId || step1.intentReady) return;
|
||||
const title = prefill.taskTitle?.trim();
|
||||
const goal = prefill.taskGoal?.trim();
|
||||
const text = [title ? `任务名称:${title}` : "", goal ? `任务描述:${goal}` : ""]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
if (!text) return;
|
||||
autoStartedRef.current = true;
|
||||
void step1.sendIntentMessage(text);
|
||||
}, [currentStep, step1.intentThreadId, step1.intentReady, step1.sendIntentMessage]);
|
||||
|
||||
// C3:据 prefill.businessCode 查业务映射 → 取 chainId → 拉公共链 + 候选智能体 → 构造自动选链结果。
|
||||
// 解析成功后「进入研讨」会跳过选链弹窗直接据此进 Step 2;解析失败(未配置 / 链不存在 / 席位不足)
|
||||
// 静默回退到弹窗手选。
|
||||
// 不加「已启动」永久闸门 —— StrictMode 的 mount→unmount→remount 会让首次 run 在 cleanup
|
||||
// 里被 cancelled,闸门又让 remount 跳过重启,导致 autoChainResult 永远设不上(与
|
||||
// useStep1Intent 同款坑)。改为:每次 setup 都重跑,靠 cancelled 丢弃过期的首次结果,
|
||||
// 末次 setup 正常落地(生产无 StrictMode 只跑一次)。
|
||||
useEffect(() => {
|
||||
const prefill = prefillRef.current;
|
||||
if (!prefill?.businessCode) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const mappings = await listBusinessMappings();
|
||||
const row = mappings.find((m) => m.businessCode === prefill.businessCode);
|
||||
const chainId = row?.chainId;
|
||||
if (!chainId) return;
|
||||
const [chain, agents] = await Promise.all([
|
||||
getChain(chainId),
|
||||
listAgents().catch(() => []),
|
||||
]);
|
||||
if (cancelled || !chain) return;
|
||||
const candidates = filterRecommendCandidates(agents);
|
||||
const result = buildChainSeatSelection(chain, candidates);
|
||||
if (!cancelled && result) setAutoChainResult(result);
|
||||
} catch {
|
||||
/* 解析失败 → 保持 null,回退弹窗手选 */
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 把页面重置回「第一步全新任务」——新建任务 + 后台挂起后都复用。
|
||||
// 注意:只清前端状态,不删后端已建的草稿 / 已挂起的后台作业(它们继续在后台跑,
|
||||
// 在历史记录里可见)。
|
||||
@@ -903,6 +996,13 @@ export default function RoundtablePlanningPage() {
|
||||
triggerToast("⚠️ 请先与任务理解智能体完成澄清,生成任务摘要后再进入");
|
||||
return;
|
||||
}
|
||||
// 会商自动化串联(模块 C):若任务页带来的业务链已解析成功,跳过选链弹窗,
|
||||
// 直接据该业务链进入 Step 2 研讨(用户无需再手选)。
|
||||
if (autoChainResult) {
|
||||
const result = autoChainResult;
|
||||
void drafts.ensureDraft(1).finally(() => handleConfirmAgents(result));
|
||||
return;
|
||||
}
|
||||
// 先把草稿落地拿到 draft_id,推荐历史才有归属;失败也照常打开弹窗
|
||||
// (弹窗在无 draftId 时退化为「直接推荐、不存历史」)。
|
||||
void drafts.ensureDraft(1).finally(() => setIsRecommendDialogOpen(true));
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 全局业务映射(业务码 → 智能体 / 业务链)API 客户端。
|
||||
*
|
||||
* 后端:`/api/business-mapping`(见 app/gateway/routers/business_mapping.py)。
|
||||
* 全局共享、无用户隔离;只读列表 + 更新 agent / chain 两列(业务行由后端 seed 固定)。
|
||||
* snake_case ⇄ camelCase 在此边界收敛,调用方保持干净。
|
||||
*/
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
export interface BusinessMapping {
|
||||
businessCode: string;
|
||||
label: string;
|
||||
agentId: string | null;
|
||||
agentName: string | null;
|
||||
chainId: string | null;
|
||||
chainTitle: string | null;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
/** 部分更新 —— 只发改动的字段;显式传 null 清空该字段。 */
|
||||
export interface BusinessMappingPatch {
|
||||
agentId?: string | null;
|
||||
agentName?: string | null;
|
||||
chainId?: string | null;
|
||||
chainTitle?: string | null;
|
||||
}
|
||||
|
||||
const BASE = () => `${getBackendBaseURL()}/api/business-mapping`;
|
||||
|
||||
interface ServerMapping {
|
||||
business_code: string;
|
||||
label: string;
|
||||
agent_id: string | null;
|
||||
agent_name: string | null;
|
||||
chain_id: string | null;
|
||||
chain_title: string | null;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
function toMapping(s: ServerMapping): BusinessMapping {
|
||||
return {
|
||||
businessCode: s.business_code,
|
||||
label: s.label ?? "",
|
||||
agentId: s.agent_id ?? null,
|
||||
agentName: s.agent_name ?? null,
|
||||
chainId: s.chain_id ?? null,
|
||||
chainTitle: s.chain_title ?? null,
|
||||
sortOrder: s.sort_order ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function readJson<T>(res: Response, ctx: string): Promise<T> {
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`${ctx} failed (${res.status}): ${text}`);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
/** 列出全部业务映射(按展示顺序)。 */
|
||||
export async function listBusinessMappings(): Promise<BusinessMapping[]> {
|
||||
const res = await apiFetch(BASE(), { method: "GET" });
|
||||
const data = await readJson<{ mappings: ServerMapping[] }>(res, "list business mappings");
|
||||
return (data.mappings ?? []).map(toMapping);
|
||||
}
|
||||
|
||||
/** 更新某业务行的智能体 / 业务链选择。404 返回 null。 */
|
||||
export async function updateBusinessMapping(
|
||||
businessCode: string,
|
||||
patch: BusinessMappingPatch,
|
||||
): Promise<BusinessMapping | null> {
|
||||
const body: Record<string, unknown> = {};
|
||||
if (patch.agentId !== undefined) body.agent_id = patch.agentId;
|
||||
if (patch.agentName !== undefined) body.agent_name = patch.agentName;
|
||||
if (patch.chainId !== undefined) body.chain_id = patch.chainId;
|
||||
if (patch.chainTitle !== undefined) body.chain_title = patch.chainTitle;
|
||||
const res = await apiFetch(`${BASE()}/${encodeURIComponent(businessCode)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (res.status === 404) return null;
|
||||
const data = await readJson<ServerMapping>(res, "update business mapping");
|
||||
return toMapping(data);
|
||||
}
|
||||
@@ -4,9 +4,10 @@ import { Bot, ChevronRight, Download, Edit, Eye, FileText, Layers, MoreHorizonta
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Col, Empty, Loading, Pagination, Progress, Row } from 'tdesign-react';
|
||||
import { Col, Loading, Pagination, Progress, Row } from 'tdesign-react';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { ActionColumn } from './ActionColumn';
|
||||
import { resolveStatusIcon } from './icons';
|
||||
|
||||
interface ActionAnalyzesProps {
|
||||
TaskType?: string;
|
||||
@@ -78,7 +79,6 @@ export const ActionAnalyzes: React.FC<ActionAnalyzesProps> = ({ title, category,
|
||||
setTaskLoading(true);
|
||||
const res = await service.post(`${zncmUrl}/action/cop-action-tasks`, param);
|
||||
setTaskLoading(false);
|
||||
console.log('任务搜索', res);
|
||||
const result = res.data;
|
||||
result.forEach((item: TaskListItem) => {
|
||||
item.progress = item.endCount / item.sumAll || 0; // 获取已完成和未完成的占比
|
||||
@@ -86,7 +86,6 @@ export const ActionAnalyzes: React.FC<ActionAnalyzesProps> = ({ title, category,
|
||||
})
|
||||
setTaskList(result || []); // 设置任务列表页
|
||||
const totalRes = await service.post(`${zncmUrl}/action/cop-action-tasks-count`, param); // 查询任务总数
|
||||
console.log('totalRes', totalRes)
|
||||
setTotal(totalRes.data || 0); // 设置总数
|
||||
if (result.length > 0) {
|
||||
setTaskId(result[0].id);
|
||||
@@ -101,7 +100,6 @@ export const ActionAnalyzes: React.FC<ActionAnalyzesProps> = ({ title, category,
|
||||
}
|
||||
setActionLoading(true);
|
||||
const res = await service.get(`${zncmUrl}/action/cop-task-actions`, param);
|
||||
console.log('行动查询', res);
|
||||
setActionLoading(false);
|
||||
const result = res.data;
|
||||
setActionColumnList([
|
||||
@@ -151,7 +149,7 @@ export const ActionAnalyzes: React.FC<ActionAnalyzesProps> = ({ title, category,
|
||||
<div className="text-[var(--text-color)] text-[17px] mb-[5px] font-[500]">{item.taskName}</div>
|
||||
<div className="text-[#7f7f7f] mb-[5px]">{format(new Date(item.taskStartDate), 'yyyy-MM-dd HH:mm:ss')}</div>
|
||||
<div className="flex items-center mb-[8px]">
|
||||
<img src={`/icon/${item?.taskStatusData?.icon}`} alt="" />
|
||||
<img src={resolveStatusIcon(item?.taskStatusData?.icon)} className="w-[20px]" alt="" />
|
||||
<span className="ml-[5px] leading-[16px]">{item?.taskStatusData?.label}</span>
|
||||
</div>
|
||||
<div className="pb-[10px] pt-[10px]">
|
||||
@@ -195,7 +193,11 @@ export const ActionAnalyzes: React.FC<ActionAnalyzesProps> = ({ title, category,
|
||||
))
|
||||
}
|
||||
{
|
||||
actionColumnList.length === 0 && <Empty/>
|
||||
actionColumnList.length === 0 && (
|
||||
<div className={`col-span-4 flex items-center justify-center py-[60px] text-sm ${isDark ? 'text-slate-500' : 'text-[#bfbfbf]'}`}>
|
||||
暂无数据
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Loading>
|
||||
|
||||
@@ -4,8 +4,9 @@ import { Bot, ChevronRight, Download, Edit, Eye, FileText, Layers, MoreHorizonta
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Col, Empty, Pagination, Progress, Row } from 'tdesign-react';
|
||||
import { Col, Pagination, Progress, Row } from 'tdesign-react';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { resolveStatusIcon } from './icons';
|
||||
|
||||
interface ActionColumnProps {
|
||||
TaskType?: string;
|
||||
@@ -66,7 +67,7 @@ export const ActionColumn: React.FC<ActionColumnProps> = ({ title, category, sub
|
||||
<div className={`h-[calc(100vh-100px)] p-[10px] flex-1 ${isDark? 'dark-action-bg' : `bg-white action-bg${columnIndex} shadow-[2px_0px_6px_rgba(0,0,0,0.12)]`} rounded-[4px]`}>
|
||||
<div className="mb-[10px]">
|
||||
<div className="flex items-center mb-[20px]">
|
||||
<img src={`/icon/${icon}`} className="w-[24px] mr-[10px]" alt="" />
|
||||
<img src={resolveStatusIcon(icon)} className="w-[24px] mr-[10px]" alt="" />
|
||||
<span className="text-[#222222] text-[18px] font-bold mr-[20px]">{columnName}</span>
|
||||
<span className="text-[#222222] text-[18px] font-bold mr-[20px]">{actionCount}</span>
|
||||
</div>
|
||||
@@ -99,13 +100,11 @@ export const ActionColumn: React.FC<ActionColumnProps> = ({ title, category, sub
|
||||
))
|
||||
}
|
||||
{
|
||||
actionList.length === 0 && <Empty image={
|
||||
<img
|
||||
src="/icon/empty-list.png"
|
||||
alt="empty"
|
||||
className="w-[135px]"
|
||||
/>
|
||||
} />
|
||||
actionList.length === 0 && (
|
||||
<div className={`flex items-center justify-center py-[48px] text-sm ${isDark ? 'text-slate-500' : 'text-[#bfbfbf]'}`}>
|
||||
暂无数据
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+67
-17
@@ -1,13 +1,14 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ArrowRight, Layers, Plus, Search, Users, X } from 'lucide-react';
|
||||
import { ArrowRight, Bot, Layers, 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';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { useChatContext } from '@/contexts/ChatContext';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { toast } from 'sonner';
|
||||
import { KanbanColumn } from './KanbanColumn';
|
||||
import { TaskListItem } from './TaskManagement';
|
||||
import { listBusinessMappings } from '@/strategy-components/api/business-mapping';
|
||||
|
||||
interface TaskListPageProps {
|
||||
title: string;
|
||||
@@ -25,10 +26,11 @@ type ColumnType = 'unanalyzed' | 'running' | 'completed';
|
||||
|
||||
const service = requestCrudService;
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
/** 本页对应的业务码(3QFX = 3Q);用于查业务映射拿到要自动选用的业务链 / 单智能体。 */
|
||||
const BUSINESS_CODE = '3Q';
|
||||
|
||||
const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategory }) => {
|
||||
const { theme } = useTheme();
|
||||
const { setStartConfig } = useChatContext();
|
||||
const navigate = useNavigate();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
@@ -153,23 +155,54 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
|
||||
const handleOpenConsultation = () => {
|
||||
const task = stateRef.current.task;
|
||||
if (!task) return;
|
||||
// 会商自动化串联(模块 C):跳转圆桌规划页,带上任务名称/描述 + 本页业务码(3Q=三情分析)。
|
||||
// 圆桌据此自动开跑 Step 1,并在「进入研讨」时自动选用 3Q 配置的业务链(跳过选链弹窗)。
|
||||
navigate('/page/strategy/qa/multi-agent-roundtable', {
|
||||
state: {
|
||||
roundtablePrefill: {
|
||||
taskTitle: task.taskName,
|
||||
taskGoal: task.overview,
|
||||
businessCode: BUSINESS_CODE,
|
||||
autoStart: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
setShowAnalysisModal(false);
|
||||
};
|
||||
|
||||
// 单智能体分析:用「业务链条管理」里为本业务配置的智能体,跳转到其问答页(新会话),
|
||||
// 把任务名称/简介作为开场消息暂存,由 AgentChatPage 读取 sessionStorage.pendingAgentMessage
|
||||
// 自动发出并开始问答。未配置智能体则提示去配置。
|
||||
const handleSingleAgentAnalysis = async () => {
|
||||
const task = stateRef.current.task;
|
||||
if (!task) return;
|
||||
let agentId: string | null = null;
|
||||
try {
|
||||
const sceneItem = JSON.parse(task.scene || '{}');
|
||||
if (sceneItem.taskJson) {
|
||||
const taskJson = JSON.parse(sceneItem.taskJson);
|
||||
setStartConfig({
|
||||
...taskJson,
|
||||
llm_preset_id: 'BAILING',
|
||||
consistency_mode: 'soft',
|
||||
phase_config: { diverge_rounds: 3 },
|
||||
task_context: task.overview,
|
||||
});
|
||||
navigate('/qa-center/mope-chat');
|
||||
}
|
||||
} finally {
|
||||
setShowAnalysisModal(false);
|
||||
const mappings = await listBusinessMappings();
|
||||
agentId = mappings.find((m) => m.businessCode === BUSINESS_CODE)?.agentId ?? null;
|
||||
} catch {
|
||||
/* ignore → 下面按未配置处理 */
|
||||
}
|
||||
if (!agentId) {
|
||||
toast.error('该业务尚未在「业务链条管理」中配置单智能体');
|
||||
return;
|
||||
}
|
||||
const text = [
|
||||
task.taskName ? `任务名称:${task.taskName}` : '',
|
||||
task.overview ? `任务简介:${task.overview}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
'pendingAgentMessage',
|
||||
JSON.stringify({ agentId, text }),
|
||||
);
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
navigate(`/page/strategy/agents/${agentId}/chats/new`);
|
||||
setShowAnalysisModal(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -390,6 +423,23 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 opacity-70" />
|
||||
</button>
|
||||
|
||||
<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]'}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Bot className={`mt-0.5 h-5 w-5 ${isDark ? 'text-[#C4B5FD]' : 'text-[#7C3AED]'}`} />
|
||||
<div>
|
||||
<div className="font-medium">单智能体分析</div>
|
||||
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
|
||||
由单个 AI 智能体直接对该任务进行分析问答。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 opacity-70" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,11 @@ import { DateRangePicker, DateRangePickerProps, DateRangeValue, Loading } from '
|
||||
import dayjs from 'dayjs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { useChatContext } from '@/contexts/ChatContext';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { toast } from 'sonner';
|
||||
import { KanbanColumn } from './KanbanColumn';
|
||||
import { TaskListItem } from './TaskManagement';
|
||||
import { listBusinessMappings } from '@/strategy-components/api/business-mapping';
|
||||
|
||||
interface TaskListPageProps {
|
||||
title: string;
|
||||
@@ -25,10 +26,11 @@ type ColumnType = 'unanalyzed' | 'running' | 'completed';
|
||||
|
||||
const service = requestCrudService;
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
/** 本页对应的业务码(任务FX = 6BF);用于查业务映射拿到要自动选用的业务链 / 单智能体。 */
|
||||
const BUSINESS_CODE = '6BF';
|
||||
|
||||
const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategory }) => {
|
||||
const { theme } = useTheme();
|
||||
const { setStartConfig } = useChatContext();
|
||||
const navigate = useNavigate();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
@@ -153,23 +155,54 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
|
||||
const handleOpenConsultation = () => {
|
||||
const task = stateRef.current.task;
|
||||
if (!task) return;
|
||||
// 会商自动化串联(模块 C):跳转圆桌规划页,带上任务名称/描述 + 本页业务码(6BF=任务FX)。
|
||||
// 圆桌据此自动开跑 Step 1,并在「进入研讨」时自动选用 6BF 配置的业务链(跳过选链弹窗)。
|
||||
navigate('/page/strategy/qa/multi-agent-roundtable', {
|
||||
state: {
|
||||
roundtablePrefill: {
|
||||
taskTitle: task.taskName,
|
||||
taskGoal: task.overview,
|
||||
businessCode: BUSINESS_CODE,
|
||||
autoStart: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
setShowAnalysisModal(false);
|
||||
};
|
||||
|
||||
// 单智能体分析:用「业务链条管理」里为本业务配置的智能体,跳转到其问答页(新会话),
|
||||
// 把任务名称/简介作为开场消息暂存,由 AgentChatPage 读取 sessionStorage.pendingAgentMessage
|
||||
// 自动发出并开始问答。未配置智能体则提示去配置。
|
||||
const handleSingleAgentAnalysis = async () => {
|
||||
const task = stateRef.current.task;
|
||||
if (!task) return;
|
||||
let agentId: string | null = null;
|
||||
try {
|
||||
const sceneItem = JSON.parse(task.scene || '{}');
|
||||
if (sceneItem.taskJson) {
|
||||
const taskJson = JSON.parse(sceneItem.taskJson);
|
||||
setStartConfig({
|
||||
...taskJson,
|
||||
llm_preset_id: 'BAILING',
|
||||
consistency_mode: 'soft',
|
||||
phase_config: { diverge_rounds: 3 },
|
||||
task_context: task.overview,
|
||||
});
|
||||
navigate('/qa-center/mope-chat');
|
||||
}
|
||||
} finally {
|
||||
setShowAnalysisModal(false);
|
||||
const mappings = await listBusinessMappings();
|
||||
agentId = mappings.find((m) => m.businessCode === BUSINESS_CODE)?.agentId ?? null;
|
||||
} catch {
|
||||
/* ignore → 下面按未配置处理 */
|
||||
}
|
||||
if (!agentId) {
|
||||
toast.error('该业务尚未在「业务链条管理」中配置单智能体');
|
||||
return;
|
||||
}
|
||||
const text = [
|
||||
task.taskName ? `任务名称:${task.taskName}` : '',
|
||||
task.overview ? `任务简介:${task.overview}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
'pendingAgentMessage',
|
||||
JSON.stringify({ agentId, text }),
|
||||
);
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
navigate(`/page/strategy/agents/${agentId}/chats/new`);
|
||||
setShowAnalysisModal(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -236,9 +269,6 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
|
||||
<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 className="flex h-11 w-11 items-center justify-center rounded-2xl border border-[rgba(96,165,250,0.28)] bg-[linear-gradient(180deg,rgba(22,119,255,0.28),rgba(14,63,135,0.18))] shadow-[0_10px_30px_rgba(14,63,135,0.24)]">
|
||||
<Bot className="h-5 w-5 text-[#7CC9FF]" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className={`text-[26px] font-semibold tracking-[0.02em] ${isDark ? 'text-slate-50' : 'text-[#222222]'}`}>
|
||||
{title}
|
||||
@@ -413,6 +443,25 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 opacity-70 transition group-hover:translate-x-0.5" />
|
||||
</button>
|
||||
|
||||
<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'}`}
|
||||
>
|
||||
<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>
|
||||
<div>
|
||||
<div className="font-medium">单智能体分析</div>
|
||||
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300/72' : 'text-slate-500'}`}>
|
||||
由单个 AI 智能体直接对该任务进行分析问答。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 opacity-70 transition group-hover:translate-x-0.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 行动 / 任务状态图标。
|
||||
*
|
||||
* 用 `import` 引入 SVG,让 Vite 把它们打进产物并生成带 hash 的最终 URL,
|
||||
* 这样无论部署到子路径(base)还是带指纹的静态资源服务都能正确访问 ——
|
||||
* 不能再用 `/icon/xxx.svg` 这种运行时绝对路径(部署后会 404)。
|
||||
*
|
||||
* key 沿用后端 / taskStatusList 里给的中文文件名,方便按名取用。
|
||||
*/
|
||||
import daiQianShou from "./待签收.svg";
|
||||
import daiCEH from "./待CEH.svg";
|
||||
import jinXingZhong from "./进行中.svg";
|
||||
import wanCheng from "./完成.svg";
|
||||
import weiTongGuo from "./未通过.svg";
|
||||
import yiZhongZhi from "./已终止.svg";
|
||||
|
||||
export const statusIconMap: Record<string, string> = {
|
||||
"待签收.svg": daiQianShou,
|
||||
"待CEH.svg": daiCEH,
|
||||
"进行中.svg": jinXingZhong,
|
||||
"完成.svg": wanCheng,
|
||||
"未通过.svg": weiTongGuo,
|
||||
"已终止.svg": yiZhongZhi,
|
||||
};
|
||||
|
||||
/** 按文件名取打包后的图标 URL;取不到返回 undefined(<img> 不渲染 broken icon)。 */
|
||||
export function resolveStatusIcon(name?: string | null): string | undefined {
|
||||
if (!name) return undefined;
|
||||
return statusIconMap[name];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="11" fill="#1eb07f"/>
|
||||
<path d="M7.5 12.3 10.5 15.3 16.5 9" stroke="#fff" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="11" fill="#8c8c8c"/>
|
||||
<rect x="8.7" y="8.7" width="6.6" height="6.6" rx="1.4" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="11" fill="#2f9acc"/>
|
||||
<circle cx="11" cy="11" r="3.2" stroke="#fff" stroke-width="1.8" fill="none"/>
|
||||
<path d="M13.4 13.4 16.2 16.2" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="11" fill="#0062d9"/>
|
||||
<rect x="7.6" y="7.8" width="8.8" height="9.6" rx="1.6" stroke="#fff" stroke-width="1.5" fill="none"/>
|
||||
<path d="M9.8 7.8v-.5a1 1 0 0 1 1-1h2.4a1 1 0 0 1 1 1v.5" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.8 11h4.4M9.8 13.6h2.8" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="11" fill="#cf1322"/>
|
||||
<path d="M8.8 8.8 15.2 15.2M15.2 8.8 8.8 15.2" stroke="#fff" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 258 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="11" fill="#d97b00"/>
|
||||
<path d="M10.2 8.4 15.4 12l-5.2 3.6z" fill="#fff" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 264 B |
Reference in new issue
Block a user