337 lines
12 KiB
TypeScript
337 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 (
|
|
// 仅将 8BF 会商页锁为深蓝色:全局主题状态和切换逻辑保持原样,
|
|
// 后续去掉本容器与 forceDarkAppearance 即可恢复随主题切换。
|
|
<div className="dark flex h-full min-h-0 flex-col bg-[#061b3b]">
|
|
<RoundtablePlanningPage
|
|
compactChrome={compactChrome}
|
|
forceDarkAppearance
|
|
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}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|