'use client';
import { useState, useRef, useCallback } from 'react';
import { Camera, ChevronDown, ChevronUp, Redo2, Undo2, UserMinus, UserPlus } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { GeneratedAgentConfig } from '@/lib/types/stage';
import { useAgentRoster } from './useAgentRoster';
import { AvatarPicker } from './AvatarPicker';
const PERSONA_MAX = 2000;
// ─── Avatar with camera overlay ──────────────────────────────────────────────
interface AvatarWithOverlayProps {
readonly agent: GeneratedAgentConfig;
readonly size: number;
readonly ringColor: string;
readonly onPickerOpen: () => void;
}
function AvatarWithOverlay({ agent, size, ringColor, onPickerOpen }: AvatarWithOverlayProps) {
const [hovering, setHovering] = useState(false);
return (
setHovering(true)}
onMouseLeave={() => setHovering(false)}
onClick={(e) => {
e.stopPropagation();
onPickerOpen();
}}
>

{hovering && (
)}
);
}
// ─── Inline editable name ────────────────────────────────────────────────────
interface EditableNameProps {
readonly value: string;
readonly onCommit: (v: string) => void;
readonly className?: string;
}
function EditableName({ value, onCommit, className }: EditableNameProps) {
const ref = useRef(null);
const handleBlur = useCallback(() => {
const text = ref.current?.textContent?.trim() ?? '';
if (text && text !== value) onCommit(text);
else if (ref.current) ref.current.textContent = value;
}, [value, onCommit]);
return (
e.stopPropagation()}
className={cn(
'outline-none cursor-text rounded-[3px]',
'hover:underline hover:decoration-dashed hover:decoration-[#b08ee6]',
'focus:shadow-[0_0_0_2px_rgba(114,46,209,.18)]',
className,
)}
style={{ minWidth: 10 }}
>
{value}
);
}
// ─── Persona textarea ─────────────────────────────────────────────────────────
interface PersonaEditorProps {
readonly agentId: string;
readonly value: string;
readonly borderColor: string;
readonly onUpdate: (id: string, persona: string) => void;
}
function PersonaEditor({ agentId, value, borderColor, onUpdate }: PersonaEditorProps) {
const [prevValue, setPrevValue] = useState(value);
const [draft, setDraft] = useState(value);
const [focused, setFocused] = useState(false);
// Sync draft when value changes externally (e.g. undo/redo), but only when
// not focused — avoids clobbering in-progress typing. Render-time state
// update: React re-renders immediately with the new draft before painting.
if (prevValue !== value && !focused) {
setPrevValue(value);
setDraft(value);
}
const handleChange = (e: React.ChangeEvent) => {
const v = e.target.value.slice(0, PERSONA_MAX);
setDraft(v);
};
const handleFocus = () => setFocused(true);
const handleBlur = () => {
setFocused(false);
if (draft !== value) onUpdate(agentId, draft);
};
return (
人设描述
{draft.length} / {PERSONA_MAX}
);
}
// ─── Teacher card ─────────────────────────────────────────────────────────────
interface TeacherCardProps {
readonly agent: GeneratedAgentConfig;
readonly open: boolean;
readonly onToggle: () => void;
readonly onUpdate: (id: string, patch: Partial) => void;
}
function TeacherCard({ agent, open, onToggle, onUpdate }: TeacherCardProps) {
const [showAvatarPicker, setShowAvatarPicker] = useState(false);
const personaPreview = agent.persona?.slice(0, 40) || '暂无人设';
return (
{/* Card head */}
(e.key === 'Enter' || e.key === ' ') && onToggle()}
className="flex cursor-pointer items-center gap-3 px-3 py-[11px] select-none"
>
{
if (!open) {
onToggle();
setShowAvatarPicker(true);
return;
}
setShowAvatarPicker((v) => !v);
}}
/>
onUpdate(agent.id, { name })}
className="text-[13.5px] font-semibold text-[#27272a]"
/>
{/* 主讲 badge */}
👑 主讲
{personaPreview}
{open ? (
) : (
)}
{/* Expanded editor */}
{open && (
{showAvatarPicker && (
{
onUpdate(agent.id, { avatar });
setShowAvatarPicker(false);
}}
/>
)}
onUpdate(id, { persona })}
/>
)}
);
}
// ─── Classmate card ────────────────────────────────────────────────────────────
interface ClassmateCardProps {
readonly agent: GeneratedAgentConfig;
readonly open: boolean;
readonly isFirst: boolean;
readonly isLast: boolean;
readonly onToggle: () => void;
readonly onUpdate: (id: string, patch: Partial) => void;
readonly onRemove: (id: string) => void;
readonly onMoveUp: () => void;
readonly onMoveDown: () => void;
}
function ClassmateCard({
agent,
open,
isFirst,
isLast,
onToggle,
onUpdate,
onRemove,
onMoveUp,
onMoveDown,
}: ClassmateCardProps) {
const [showAvatarPicker, setShowAvatarPicker] = useState(false);
const ringColor = agent.color || '#a1a1aa';
const personaPreview = agent.persona?.slice(0, 35) || '暂无人设';
return (
{/* Card head */}
(e.key === 'Enter' || e.key === ' ') && onToggle()}
className="flex cursor-pointer items-center gap-3 px-3 py-[11px] select-none"
>
{
if (!open) {
onToggle();
setShowAvatarPicker(true);
return;
}
setShowAvatarPicker((v) => !v);
}}
/>
onUpdate(agent.id, { name })}
className="block truncate text-[13.5px] font-semibold text-[#27272a]"
/>
{personaPreview}
{/* Reorder controls (stop propagation so they don't expand) */}
e.stopPropagation()}>
{open ? (
) : (
)}
{/* Expanded editor */}
{open && (
{showAvatarPicker && (
{
onUpdate(agent.id, { avatar });
setShowAvatarPicker(false);
}}
/>
)}
onUpdate(id, { persona })}
/>
{/* Footer: remove */}
)}
);
}
// ─── Main panel ───────────────────────────────────────────────────────────────
export function AgentRosterPanel() {
const { roster, selectedId, select, add, update, remove, reorder, history } = useAgentRoster();
const teachers = roster.filter((a) => a.role === 'teacher');
const classmates = roster.filter((a) => a.role !== 'teacher');
const handleUpdate = useCallback(
(id: string, patch: Partial) => {
update(id, patch as Parameters[1]);
},
[update],
);
const handleToggle = (id: string) => {
select(selectedId === id ? null : id);
};
const handleAdd = () => {
add('student');
// select() called inside add() already
};
// Reorder indices are within the full roster array
const classmateGlobalIndex = (localIdx: number) =>
roster.findIndex((a) => a.id === classmates[localIdx]?.id);
return (
{/* Sub-head */}
课堂阵容
{roster.length} 位
点击一位展开编辑
{/* Undo/redo */}
{/* Scrollable list */}
{/* Teacher cards */}
{teachers.map((agent) => (
handleToggle(agent.id)}
onUpdate={handleUpdate}
/>
))}
{/* Divider */}
{classmates.length > 0 && (
AI 同学 · {classmates.length}
)}
{/* Classmate cards */}
{classmates.map((agent, localIdx) => {
const globalIdx = classmateGlobalIndex(localIdx);
return (
handleToggle(agent.id)}
onUpdate={handleUpdate}
onRemove={remove}
onMoveUp={() => reorder(agent.id, globalIdx - 1)}
onMoveDown={() => reorder(agent.id, globalIdx + 1)}
/>
);
})}
{/* Add button */}
);
}