Merge branch 'dhk-1' into 'master'
样式修改 See merge request knowledge-base-bj/deerflow-server!89
This commit is contained in:
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" />
|
||||
|
||||
Reference in new issue
Block a user