Files
deerflow-code/frontend-web/src/roundtable-planning/components/PersonaDrawer.tsx
T

191 lines
6.9 KiB
TypeScript

/**
* Persona 抽屉。
*
* Step 2 圆桌可视化里点击智能体头像后展开。允许覆写该席位的:
* - System Prompt(本地记录,后端覆写未开放)
* - 推理模型(下一轮派活时由 `runOrchestration` 读 `agentConfigs[id].model`)
* - Temperature(本地记录)
*
* 「参数覆盖并重启动算研讨」按钮 = `onSave`,不会立即重启编排,只更新本地配置;
* 真正生效在该席位下一次被 leader 派活时。
*/
import { X, Sparkles } from "lucide-react";
import { getAvatar } from "@/roundtable-planning/components/Avatars";
import type {
AgentConfig,
} from "@/roundtable-planning/hooks/useStep2Orchestration";
import type {
RoleEntry,
} from "@/roundtable-planning/lib/roundtable-constants";
interface ModelOption {
name: string;
display_name?: string;
}
export interface PersonaDrawerProps {
seatId: number;
roles: RoleEntry[];
agentConfigs: Record<number, AgentConfig>;
setAgentConfigs: React.Dispatch<
React.SetStateAction<Record<number, AgentConfig>>
>;
selectedModel: string;
availableModels: ModelOption[];
modelsLoading: boolean;
onClose: () => void;
onSave: (id: number) => void;
}
/** 席位 id → 头像配色(与圆桌可视化里的硬编码保持一致)。 */
function avatarTypeForSeat(seatId: number): string {
switch (seatId) {
case 2:
return "blue";
case 3:
return "purple";
case 4:
return "emerald";
case 5:
return "human";
case 6:
return "amber";
case 7:
return "pink";
default:
return "blue";
}
}
export function PersonaDrawer({
seatId,
roles,
agentConfigs,
setAgentConfigs,
selectedModel,
availableModels,
modelsLoading,
onClose,
onSave,
}: PersonaDrawerProps) {
const cfg = agentConfigs[seatId];
const roleName = roles.find((r) => r.id === seatId)?.name ?? "专家智能体";
return (
<div className="mx-6 mb-4 p-4.5 bg-gradient-to-r from-[var(--brand-200)]/50 via-[var(--brand-200)]/15 to-white border-2 border-primary rounded-2xl shadow-[0_8px_30px_rgba(245,34,45,0.06)] relative animate-fade-in">
<button
onClick={onClose}
className="absolute top-3.5 right-3.5 text-slate-400 hover:text-slate-650 hover:bg-slate-100 p-1.5 rounded-full transition cursor-pointer"
title="关闭 Override"
>
<X className="w-4 h-4" />
</button>
<div className="flex flex-col lg:flex-row gap-5 text-xs text-slate-600">
<div className="flex-1 space-y-3">
<div className="flex items-center gap-3">
{getAvatar(avatarTypeForSeat(seatId), "h-10 w-10")}
<div>
<div className="flex items-center gap-1.5">
<h4 className="text-sm font-extrabold text-slate-900">
{roleName} 人设专有策略调制
</h4>
<span className="text-[9px] font-black bg-[var(--brand-200)] text-primary px-2 py-0.5 rounded-full scale-90">
系统就绪
</span>
</div>
<p className="text-[10px] text-slate-400 font-bold">
配置此席位智能体的运行大语言模型及系统 System Prompt 指令
</p>
</div>
</div>
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-500 uppercase block tracking-wider">
该席位系统提示词指令 override (System Instruction)
</label>
<textarea
value={cfg?.prompt || ""}
onChange={(e) => {
const val = e.target.value;
setAgentConfigs((prev) => ({
...prev,
[seatId]: { ...prev[seatId], prompt: val },
}));
}}
className="w-full text-xs text-slate-700 bg-white border border-slate-200 focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 rounded-xl px-3 py-2.5 h-[65px] resize-none focus:outline-none font-medium leading-relaxed"
placeholder="请输入覆盖的专家提示词内容..."
/>
</div>
</div>
<div className="w-full lg:w-[260px] flex flex-col justify-between gap-3 border-t lg:border-t-0 lg:border-l border-slate-100 lg:pl-5 pt-3.5 lg:pt-0">
<div className="space-y-3">
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-500 uppercase tracking-wider block">
推理本源模型
</label>
<select
value={cfg?.model || selectedModel || ""}
onChange={(e) => {
const val = e.target.value;
setAgentConfigs((prev) => ({
...prev,
[seatId]: { ...prev[seatId], model: val },
}));
}}
className="w-full bg-white border border-slate-200 rounded-xl px-2.5 py-1.5 text-xs text-slate-700 font-bold focus:outline-none"
disabled={modelsLoading}
>
{availableModels.length === 0 && (
<option value="">
{modelsLoading ? "加载中…" : "未配置模型"}
</option>
)}
{availableModels.map((m) => (
<option key={m.name} value={m.name}>
{m.display_name && m.display_name !== m.name
? `${m.display_name} (${m.name})`
: m.name}
</option>
))}
</select>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between text-[9px] font-black text-slate-500">
<span>随机创造性倾向 (Temp)</span>
<span className="font-mono text-primary font-bold">{cfg?.temp || 0.2}</span>
</div>
<input
type="range"
min={0.1}
max={1.0}
step={0.1}
value={cfg?.temp || 0.2}
onChange={(e) => {
const val = parseFloat(e.target.value);
setAgentConfigs((prev) => ({
...prev,
[seatId]: { ...prev[seatId], temp: val },
}));
}}
className="w-full h-1 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-primary"
/>
</div>
</div>
<button
onClick={() => onSave(seatId)}
className="w-full bg-primary hover:bg-[var(--brand-900)] text-white text-[10.5px] font-black py-2 rounded-xl transition shadow active:scale-95 cursor-pointer flex items-center justify-center gap-1"
>
<Sparkles className="w-3.5 h-3.5 text-red-200" />
参数覆盖并重启动算研讨
</button>
</div>
</div>
</div>
);
}