'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(); }} > {agent.name} {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 (
人设描述