Files
deerflow-code/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx
T

332 lines
12 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { Loader2, Save, X } from "lucide-react";
import { getAccountDisplayName, getStoredAuth } from "@/core/auth";
import { EightBfTaskCard } from "@/eightbf-roundtable/components/EightBfTaskCard";
import {
EightBfStep1Panel,
EightBfStep1Sidebar,
EightBfStep2SidebarHeader,
} from "@/eightbf-roundtable/components/EightBfStep1Workspace";
import {
buildEightBfTaskContext,
createEightBfFallbackTask,
fetchEightBfTask,
fetchEightBfTaskDictionaries,
normalizeEightBfTask,
createEightBfTaskProcessReport,
updateEightBfTaskProcessReport,
type EightBfTask,
type EightBfTaskDictionaries,
} from "@/eightbf-roundtable/api/task-context";
import type { TaskArtifactSubmissionConfig } from "@/roundtable-planning/lib/task-artifact-submission";
import { PositionBusinessChainFlow } from "@/position-roundtable/components/PositionBusinessChainFlow";
import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage";
import { listChains } from "@/roundtable-planning/api/chains";
import type { BusinessChain } from "@/roundtable-planning/lib/business-chain";
import { useEightBfParentReturnBridge } from "@/eightbf-roundtable/lib/parent-return";
import {
listBusinessMappings,
updateBusinessMapping,
type BusinessMapping,
} from "@/strategy-components/api/business-mapping";
import "../styles/step1.css";
import "../styles/step2.css";
import "../styles/step3.css";
const BUSINESS_CODE = "8BF";
/**
* 8BF 独立多智能体会商页。
*
* 页面复用圆桌会商的三步能力,但业务链选择、草稿存储键和 CSS 根节点完全独立;
* 不引用岗位会商的页面、会话或接口。
*/
export default function EightBfRoundtablePage({ compactChrome = false }: { compactChrome?: boolean }) {
useEightBfParentReturnBridge();
const auth = getStoredAuth();
const [searchParams] = useSearchParams();
const taskId = useMemo(
() => searchParams.get("taskId")?.trim() || "",
[searchParams],
);
// 8BF 公共业务链由指定账号维护,不依赖外部透传的管理员角色。
// 与敏感词管理使用同一可信账户展示名规则(登录邮箱 @ 前缀)。
const canConfigureChain = getAccountDisplayName(auth).toLowerCase() === "lqq";
const storageTaskId = useMemo(
() => `8bf:${auth?.user_id || "anonymous"}:${taskId || "anonymous"}`,
[auth?.user_id, taskId],
);
const [task, setTask] = useState<EightBfTask | null>(() => (
taskId ? normalizeEightBfTask({}, taskId) : null
));
const [taskDictionaries, setTaskDictionaries] = useState<EightBfTaskDictionaries>({
priority: [],
direction: [],
status: [],
});
const [taskLoading, setTaskLoading] = useState(Boolean(taskId));
const [taskError, setTaskError] = useState<string | null>(null);
const [mapping, setMapping] = useState<BusinessMapping | null>(null);
const [chains, setChains] = useState<BusinessChain[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [selectedChainId, setSelectedChainId] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const configuredChain = useMemo(
() => chains.find((chain) => chain.id === mapping?.chainId) ?? null,
[chains, mapping?.chainId],
);
const taskContext = useMemo(
() => buildEightBfTaskContext(task, taskDictionaries),
[task, taskDictionaries],
);
const taskArtifactSubmissionConfig = useMemo<TaskArtifactSubmissionConfig | undefined>(
() => taskId ? {
taskId,
position: 0,
createProcessReport: createEightBfTaskProcessReport,
updateProcessReport: updateEightBfTaskProcessReport,
} : undefined,
[taskId],
);
useEffect(() => {
if (!taskId) {
setTask(null);
setTaskError(null);
setTaskLoading(false);
setTaskDictionaries({ priority: [], direction: [], status: [] });
return;
}
let cancelled = false;
setTaskLoading(true);
setTaskError(null);
setTask(normalizeEightBfTask({}, taskId));
let pendingRequests = 2;
const markRequestFinished = () => {
pendingRequests -= 1;
if (!cancelled && pendingRequests === 0) setTaskLoading(false);
};
// Keep detail and dictionary requests independent: a missing dictionary
// must not prevent the task card or task context from being available.
void fetchEightBfTask(taskId)
.then((nextTask) => {
if (!cancelled) setTask(nextTask);
})
.catch((error) => {
if (cancelled) return;
console.warn("[8bf] 任务详情请求失败,使用页面示例任务", error);
setTask(createEightBfFallbackTask(taskId));
setTaskError(null);
})
.finally(markRequestFinished);
void fetchEightBfTaskDictionaries()
.then((nextDictionaries) => {
if (!cancelled) setTaskDictionaries(nextDictionaries);
})
.catch(() => {
// fetchEightBfTaskDictionaries already degrades each dictionary to an
// empty list; this guard protects future adapter changes.
})
.finally(markRequestFinished);
return () => {
cancelled = true;
};
}, [taskId]);
const loadConfiguration = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const [mappings, availableChains] = await Promise.all([
listBusinessMappings(),
listChains("public"),
]);
const nextMapping = mappings.find((item) => item.businessCode === BUSINESS_CODE) ?? null;
// 8BF 只允许绑定已发布业务链;后端同样会进行二次校验。
const nextChains = availableChains.filter((chain) => chain.isPublic);
setMapping(nextMapping);
setChains(nextChains);
setSelectedChainId(nextMapping?.chainId ?? "");
} catch {
setError("业务链配置加载失败,请刷新后重试");
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
void loadConfiguration();
}, [loadConfiguration]);
const handleSave = useCallback(async () => {
if (!canConfigureChain) {
setError("仅 lqq 账号可以配置 8BF 业务链条");
return;
}
const selected = chains.find((chain) => chain.id === selectedChainId);
if (!selected) {
setError("请选择一条已发布的业务链");
return;
}
setIsSaving(true);
setError(null);
try {
const next = await updateBusinessMapping(BUSINESS_CODE, {
chainId: selected.id,
chainTitle: selected.title,
});
if (!next) throw new Error("8BF 映射不存在");
setMapping(next);
setIsSettingsOpen(false);
} catch (err) {
setError(err instanceof Error ? err.message : "保存业务链失败");
} finally {
setIsSaving(false);
}
}, [canConfigureChain, chains, selectedChainId]);
const openChainSettings = useCallback(() => {
if (canConfigureChain) setIsSettingsOpen(true);
}, [canConfigureChain]);
const toolbarSlot = (
<>
{canConfigureChain && isSettingsOpen && (
<div className="eightbf-roundtable__settings-mask" role="presentation">
<section
className="eightbf-roundtable__settings-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="eightbf-chain-settings-title"
>
<div className="eightbf-roundtable__settings-heading">
<div>
<p className="eightbf-roundtable__settings-kicker">管理员配置</p>
<h2 id="eightbf-chain-settings-title">设置 8BF 公共业务链</h2>
</div>
<button
type="button"
className="eightbf-roundtable__settings-close"
onClick={() => setIsSettingsOpen(false)}
aria-label="关闭"
>
<X className="h-4 w-4" />
</button>
</div>
<p className="eightbf-roundtable__settings-tip">
保存后,所有 8BF 用户都会使用这条已发布业务链;会商记录仍按用户独立保存。
</p>
<label className="eightbf-roundtable__settings-field">
<span>已发布业务链</span>
<select value={selectedChainId} onChange={(event) => setSelectedChainId(event.target.value)}>
<option value="">请选择业务链</option>
{chains.map((chain) => (
<option key={chain.id} value={chain.id}>
{chain.title}
</option>
))}
</select>
</label>
{error && <p className="eightbf-roundtable__settings-error" role="alert">{error}</p>}
<div className="eightbf-roundtable__settings-actions">
<button type="button" onClick={() => setIsSettingsOpen(false)}>取消</button>
<button type="button" onClick={() => void handleSave()} disabled={isSaving}>
{isSaving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Save className="h-3.5 w-3.5" />}
保存并应用
</button>
</div>
</section>
</div>
)}
</>
);
return (
<RoundtablePlanningPage
compactChrome={compactChrome}
lockedChainId={mapping?.chainId ?? null}
lockBusinessChain
storageTaskId={storageTaskId}
taskContext={taskContext as Record<string, unknown> | null}
entryRailTaskId={taskId || null}
entryRailTaskCardSlot={taskId ? (
<EightBfTaskCard
task={task}
dictionaries={taskDictionaries}
loading={taskLoading}
error={taskError}
/>
) : undefined}
toolbarSlot={toolbarSlot}
hideHeaderSaveDraft
hideHeaderSettings
newTaskLabel="新建会话"
hideDefaultStep1RightSidebar
step2HeaderTitle={`${configuredChain?.title || "8BF 业务链"} · 多智能体会商`}
step3HeaderTitle={`${configuredChain?.title || "8BF 业务链"} · 方案总结`}
renderStep1={(props) => (
<EightBfStep1Panel
{...props}
chain={configuredChain}
canConfigureChain={canConfigureChain}
onConfigureChain={openChainSettings}
/>
)}
renderStep1LeftSidebar={({ intentReady, onCollapse, submittedSeatKeys, onSeatClick }) => (
<EightBfStep1Sidebar
intentReady={intentReady}
onCollapse={onCollapse}
taskCard={taskId ? (
<EightBfTaskCard
task={task}
dictionaries={taskDictionaries}
loading={taskLoading}
error={taskError}
onCollapse={onCollapse}
/>
) : undefined}
businessChain={
<PositionBusinessChainFlow
chain={configuredChain}
loading={isLoading}
embedded
submittedSeatKeys={submittedSeatKeys}
onSeatClick={onSeatClick}
className="position-roundtable-intent-delivery-flow eightbf-roundtable__business-chain-flow"
/>
}
/>
)}
renderStep2LeftSidebarHeader={({ intentReady, onCollapse }) => (
<EightBfStep2SidebarHeader
intentReady={intentReady}
taskCard={taskId ? (
<EightBfTaskCard
task={task}
dictionaries={taskDictionaries}
loading={taskLoading}
error={taskError}
onCollapse={onCollapse}
/>
) : undefined}
/>
)}
entryRailBusinessChainSlot={
<PositionBusinessChainFlow
chain={configuredChain}
loading={isLoading}
className="eightbf-roundtable__business-chain-flow"
/>
}
rootClassName="eightbf-roundtable"
taskArtifactSubmissionConfig={taskArtifactSubmissionConfig}
/>
);
}