Merge branch 'dhk-1' into 'master'

样式修改

See merge request knowledge-base-bj/deerflow-server!89
This commit is contained in:
董宏凯 committed 2026-06-04 06:55:59 +00:00
commit 002e71f971
3 files changed
+211 -4

No files matched your search

@@ -0,0 +1,177 @@
/**
* 业务链条列表弹窗(圆桌页顶栏「业务链条」按钮入口)。
*
* 点击「业务链条」按钮后打开本弹窗:展示当前用户已配置的业务链条卡片列表。
* - 每张卡片右侧「设置」按钮 → 进入配置页并**切换到该链条**(带 `chainId`)。
* - footer「配置」按钮 → 进入配置页(空白新建,等价于旧按钮的直接跳转)。
*
* 纯导航 / 浏览用途,不参与研讨席位选择(席位选择走 `BusinessChainPicker`)。
* 卡片席位名直接用落库快照(`seat.name`),无需实时校验,点「设置」进编辑页后
* 会看到实时数据。
*/
import { Fragment, useEffect, useState } from "react";
import {
ArrowRightIcon,
LayersIcon,
Loader2Icon,
SettingsIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { listChains } from "../api/chains";
import { chainTagClasses, type BusinessChain } from "../lib/business-chain";
import { getAvatarTypeFor } from "../lib/roundtable-constants";
export interface BusinessChainListDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** 进入配置页。带 chainId = 打开并切到该链条;不带 = 空白新建。 */
onConfigure: (chainId?: string) => void;
}
export function BusinessChainListDialog({
open,
onOpenChange,
onConfigure,
}: BusinessChainListDialogProps) {
const [chains, setChains] = useState<BusinessChain[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 每次打开都重新拉一次,保证配置页改动后回来看到的是最新列表。
useEffect(() => {
if (!open) return;
let cancelled = false;
setLoading(true);
setError(null);
void (async () => {
try {
const list = await listChains();
if (!cancelled) setChains(list);
} catch (err) {
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [open]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[85vh] flex-col gap-0 sm:max-w-2xl">
<DialogHeader className="shrink-0">
<DialogTitle className="flex items-center gap-2">
<LayersIcon className="size-5 text-primary" />
业务链条
</DialogTitle>
<DialogDescription>
可复用的有序智能体编排。点击卡片「设置」可进入配置页编辑该链条。
</DialogDescription>
</DialogHeader>
<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">还没有业务链条</div>
<p className="max-w-sm text-xs leading-relaxed text-muted-foreground">
业务链条是可复用的有序智能体编排。点击下方「配置」去创建第一条链条。
</p>
</div>
) : (
<div className="space-y-3.5">
{chains.map((chain) => (
<ChainRow
key={chain.id}
chain={chain}
onSettings={() => onConfigure(chain.id)}
/>
))}
</div>
)}
</div>
<DialogFooter className="shrink-0 border-t pt-3 sm:justify-end">
<Button onClick={() => onConfigure()} className="gap-1.5">
<SettingsIcon className="size-4" />
配置
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
// ── 单张链条行卡片 ─────────────────────────────────────────────────────────
function ChainRow({
chain,
onSettings,
}: {
chain: BusinessChain;
onSettings: () => void;
}) {
return (
<div className="rounded-2xl border border-border bg-card p-5 transition-colors hover:border-primary/40">
{/* 标题行 + 设置按钮 */}
<div className="flex items-center gap-2.5">
<div className="truncate text-base font-semibold">{chain.title || "未命名链条"}</div>
<span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs font-medium tabular-nums text-muted-foreground">
{chain.seats.length} 席
</span>
<Button
variant="ghost"
size="sm"
onClick={onSettings}
className="ml-auto h-8 shrink-0 gap-1.5 px-3 text-sm"
>
<SettingsIcon className="size-4" />
设置
</Button>
</div>
{chain.description && (
<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>
</div>
);
}
@@ -21,8 +21,8 @@
*
* 详见 `docs/multi-agent-business-chain-dev.md` §7 阶段 1。
*/
import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import { nanoid } from "nanoid";
import {
@@ -101,6 +101,7 @@ type ActiveDrag =
export default function BusinessChainEditorPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
// ─── 数据 ────────────────────────────────────────────────────────────
const [pool, setPool] = useState<RecommendCandidate[]>([]);
@@ -184,6 +185,19 @@ export default function BusinessChainEditorPage() {
setSeats([]);
}, []);
// ─── URL 参数 chainId:从「业务链条」弹窗点「设置」进来时自动切到该链条 ──────
// 只在链条列表加载完后应用一次(用 ref 闸门避免覆盖用户后续的手动切换)。
const appliedChainParamRef = useRef(false);
useEffect(() => {
if (appliedChainParamRef.current) return;
const chainId = searchParams.get("chainId");
if (!chainId) return;
if (chains.length === 0) return; // 等 refreshChains 拉回列表
const chain = chains.find((c) => c.id === chainId);
if (chain) loadChain(chain);
appliedChainParamRef.current = true; // 命中或链条已不存在,都不再重试
}, [chains, searchParams, loadChain]);
// ─── 席位增删改 ───────────────────────────────────────────────────────
const addAgentToSeats = useCallback(
(agentId: string, atIndex?: number) => {
@@ -55,6 +55,7 @@ import { useModels } from "@/core/models/hooks";
import { getAvatar, Man1Avatar, RobotAvatar } from "@/roundtable-planning/components/Avatars";
import { RecommendAgentsDialog } from "@/roundtable-planning/components/RecommendAgentsDialog";
import { BusinessChainListDialog } from "@/roundtable-planning/components/BusinessChainListDialog";
import { Step1Panel } from "@/roundtable-planning/components/Step1Panel";
import { Step2Panel } from "@/roundtable-planning/components/Step2Panel";
import { Step3Panel } from "@/roundtable-planning/components/Step3Panel";
@@ -85,6 +86,8 @@ export default function RoundtablePlanningPage() {
const [currentStep, setCurrentStep] = useState<number>(1);
const [showToast, setShowToast] = useState<string | null>(null);
const [isRecommendDialogOpen, setIsRecommendDialogOpen] = useState<boolean>(false);
// 「业务链条」按钮 → 打开链条列表弹窗(弹窗里再决定进配置页 / 切到某条链条)。
const [isChainListOpen, setIsChainListOpen] = useState<boolean>(false);
const [selectedModel, setSelectedModel] = useState<string>("");
// Step 2 编排模式:recommend = 总控自由派活;chain = 总控按业务链条顺序派活。
// 由推荐弹窗确认时的 SeatSelectionResult.mode 决定(见 handleConfirmAgents)。
@@ -374,6 +377,19 @@ export default function RoundtablePlanningPage() {
onConfirm={handleConfirmAgents}
/>
<BusinessChainListDialog
open={isChainListOpen}
onOpenChange={setIsChainListOpen}
onConfigure={(chainId) => {
setIsChainListOpen(false);
navigate(
chainId
? `/page/workspace/roundtable/chains?chainId=${encodeURIComponent(chainId)}`
: "/page/workspace/roundtable/chains",
);
}}
/>
{showToast && (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 bg-primary text-white text-xs px-4 py-2.5 rounded-xl shadow-lg border border-primary/40 font-medium flex items-center gap-2 animate-bounce">
<Sparkles className="h-4 w-4 shrink-0" />
@@ -394,8 +410,8 @@ export default function RoundtablePlanningPage() {
<div className="flex items-center gap-2">
<button
id="business-chain-btn"
onClick={() => navigate("/page/workspace/roundtable/chains")}
title="配置可复用的业务链条(有序智能体编排)"
onClick={() => setIsChainListOpen(true)}
title="查看 / 配置可复用的业务链条(有序智能体编排)"
className="bg-white hover:bg-slate-50 border border-[#e2e8f0] text-[#334155] text-xs font-bold px-3 py-2 rounded-xl flex items-center gap-1.5 transition dark:border-border dark:bg-secondary dark:text-foreground dark:hover:bg-accent"
>
<ListOrdered className="w-3.5 h-3.5 text-primary" />