样式修改
This commit is contained in:
1 parent
f87f6031de
commit
c0e3c5d692
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。
|
* 详见 `docs/multi-agent-business-chain-dev.md` §7 阶段 1。
|
||||||
*/
|
*/
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import {
|
import {
|
||||||
@@ -101,6 +101,7 @@ type ActiveDrag =
|
|||||||
|
|
||||||
export default function BusinessChainEditorPage() {
|
export default function BusinessChainEditorPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
// ─── 数据 ────────────────────────────────────────────────────────────
|
// ─── 数据 ────────────────────────────────────────────────────────────
|
||||||
const [pool, setPool] = useState<RecommendCandidate[]>([]);
|
const [pool, setPool] = useState<RecommendCandidate[]>([]);
|
||||||
@@ -184,6 +185,19 @@ export default function BusinessChainEditorPage() {
|
|||||||
setSeats([]);
|
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(
|
const addAgentToSeats = useCallback(
|
||||||
(agentId: string, atIndex?: number) => {
|
(agentId: string, atIndex?: number) => {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import { useModels } from "@/core/models/hooks";
|
|||||||
|
|
||||||
import { getAvatar, Man1Avatar, RobotAvatar } from "@/roundtable-planning/components/Avatars";
|
import { getAvatar, Man1Avatar, RobotAvatar } from "@/roundtable-planning/components/Avatars";
|
||||||
import { RecommendAgentsDialog } from "@/roundtable-planning/components/RecommendAgentsDialog";
|
import { RecommendAgentsDialog } from "@/roundtable-planning/components/RecommendAgentsDialog";
|
||||||
|
import { BusinessChainListDialog } from "@/roundtable-planning/components/BusinessChainListDialog";
|
||||||
import { Step1Panel } from "@/roundtable-planning/components/Step1Panel";
|
import { Step1Panel } from "@/roundtable-planning/components/Step1Panel";
|
||||||
import { Step2Panel } from "@/roundtable-planning/components/Step2Panel";
|
import { Step2Panel } from "@/roundtable-planning/components/Step2Panel";
|
||||||
import { Step3Panel } from "@/roundtable-planning/components/Step3Panel";
|
import { Step3Panel } from "@/roundtable-planning/components/Step3Panel";
|
||||||
@@ -85,6 +86,8 @@ export default function RoundtablePlanningPage() {
|
|||||||
const [currentStep, setCurrentStep] = useState<number>(1);
|
const [currentStep, setCurrentStep] = useState<number>(1);
|
||||||
const [showToast, setShowToast] = useState<string | null>(null);
|
const [showToast, setShowToast] = useState<string | null>(null);
|
||||||
const [isRecommendDialogOpen, setIsRecommendDialogOpen] = useState<boolean>(false);
|
const [isRecommendDialogOpen, setIsRecommendDialogOpen] = useState<boolean>(false);
|
||||||
|
// 「业务链条」按钮 → 打开链条列表弹窗(弹窗里再决定进配置页 / 切到某条链条)。
|
||||||
|
const [isChainListOpen, setIsChainListOpen] = useState<boolean>(false);
|
||||||
const [selectedModel, setSelectedModel] = useState<string>("");
|
const [selectedModel, setSelectedModel] = useState<string>("");
|
||||||
// Step 2 编排模式:recommend = 总控自由派活;chain = 总控按业务链条顺序派活。
|
// Step 2 编排模式:recommend = 总控自由派活;chain = 总控按业务链条顺序派活。
|
||||||
// 由推荐弹窗确认时的 SeatSelectionResult.mode 决定(见 handleConfirmAgents)。
|
// 由推荐弹窗确认时的 SeatSelectionResult.mode 决定(见 handleConfirmAgents)。
|
||||||
@@ -374,6 +377,19 @@ export default function RoundtablePlanningPage() {
|
|||||||
onConfirm={handleConfirmAgents}
|
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 && (
|
{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">
|
<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" />
|
<Sparkles className="h-4 w-4 shrink-0" />
|
||||||
@@ -394,8 +410,8 @@ export default function RoundtablePlanningPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
id="business-chain-btn"
|
id="business-chain-btn"
|
||||||
onClick={() => navigate("/page/workspace/roundtable/chains")}
|
onClick={() => setIsChainListOpen(true)}
|
||||||
title="配置可复用的业务链条(有序智能体编排)"
|
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"
|
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" />
|
<ListOrdered className="w-3.5 h-3.5 text-primary" />
|
||||||
|
|||||||
Reference in new issue
Block a user