diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index c94249a2f..a482b60f6 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -436,6 +436,17 @@ function reorderCanvasSkillTags( return nextTags; } +function applyCanvasSkillTagOrder(tags: CanvasSkillTag[], order: string[]) { + if (tags.length < 2 || order.length === 0) return tags; + + const tagById = new Map(tags.map((tag) => [tag.id, tag])); + const orderedTags = order + .map((id) => tagById.get(id)) + .filter((tag): tag is CanvasSkillTag => Boolean(tag)); + const orderedIds = new Set(orderedTags.map((tag) => tag.id)); + return [...orderedTags, ...tags.filter((tag) => !orderedIds.has(tag.id))]; +} + type ClawdComposerSubmissionSnapshot = { chatId: string; canvasId?: string; @@ -2185,8 +2196,13 @@ export function CanvasChatPanel({ const [selectedSkillTagId, setSelectedSkillTagId] = useState(null); const [areCanvasSkillTagsExpanded, setAreCanvasSkillTagsExpanded] = useState(false); const [draggingSkillTagId, setDraggingSkillTagId] = useState(null); + const [canvasSkillTagOrderOverride, setCanvasSkillTagOrderOverride] = useState( + null + ); const pendingSelectedClawdSkillTagRef = useRef(null); const draggingSkillTagIdRef = useRef(null); + const latestCanvasSkillTagsRef = useRef([]); + const pendingCanvasSkillTagOrderSaveRef = useRef(null); const suppressSkillTagClickRef = useRef(false); const [messageViewportKey, setMessageViewportKey] = useState(0); const messagesEndRef = useRef(null); @@ -4164,16 +4180,34 @@ export function CanvasChatPanel({ const canvasSkillTagsEnabled = canvasSkillTagsData?.enabled !== false; const canvasSkillTagsTitle = String(canvasSkillTagsData?.title || "").trim() || DEFAULT_CANVAS_SKILL_TAGS_TITLE; - const canvasSkillTags = useMemo( + const baseCanvasSkillTags = useMemo( () => (canvasSkillTagsEnabled ? normalizeCanvasSkillTags(canvasSkillTagsData?.tags) : []), [canvasSkillTagsData?.tags, canvasSkillTagsEnabled] ); - const handleReorderCanvasSkillTag = useCallback( - async (draggedId: string, targetId: string) => { - const nextTags = reorderCanvasSkillTags(canvasSkillTags, draggedId, targetId); - if (nextTags === canvasSkillTags) return; + const canvasSkillTags = useMemo( + () => + canvasSkillTagOrderOverride + ? applyCanvasSkillTagOrder(baseCanvasSkillTags, canvasSkillTagOrderOverride) + : baseCanvasSkillTags, + [baseCanvasSkillTags, canvasSkillTagOrderOverride] + ); + useEffect(() => { + latestCanvasSkillTagsRef.current = canvasSkillTags; + }, [canvasSkillTags]); - const userOrder = nextTags.map((tag) => tag.id); + const persistCanvasSkillTagOrder = useCallback( + async (nextOrder: string[]) => { + const knownIds = new Set(baseCanvasSkillTags.map((tag) => tag.id)); + const userOrder = Array.from( + new Set( + nextOrder + .map((id) => String(id || "").trim()) + .filter((id) => id && knownIds.has(id)) + ) + ); + if (userOrder.length < 2) return; + + const nextTags = applyCanvasSkillTagOrder(baseCanvasSkillTags, userOrder); const optimisticData = { ...(canvasSkillTagsData || {}), enabled: canvasSkillTagsData?.enabled, @@ -4198,11 +4232,12 @@ export function CanvasChatPanel({ toast.success("快捷标签顺序已保存"); } catch (error) { console.error("[CanvasChat] 保存快捷标签顺序失败:", error); + setCanvasSkillTagOrderOverride(null); toast.error("快捷标签顺序保存失败"); void mutate("/api/canvas-skill-tags"); } }, - [canvasSkillTags, canvasSkillTagsData, mutate] + [baseCanvasSkillTags, canvasSkillTagsData, mutate] ); const selectedSkillTag = useMemo( () => canvasSkillTags.find((tag) => tag.id === selectedSkillTagId) || null, @@ -5429,6 +5464,7 @@ export function CanvasChatPanel({ const handleCanvasSkillTagDragStart = useCallback( (event: ReactDragEvent, tagId: string) => { draggingSkillTagIdRef.current = tagId; + pendingCanvasSkillTagOrderSaveRef.current = null; setDraggingSkillTagId(tagId); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", tagId); @@ -5436,6 +5472,24 @@ export function CanvasChatPanel({ [] ); + const handleCanvasSkillTagDragEnter = useCallback( + (event: ReactDragEvent, targetId: string) => { + const draggedId = draggingSkillTagIdRef.current; + if (!draggedId || draggedId === targetId) return; + + event.preventDefault(); + const currentTags = latestCanvasSkillTagsRef.current; + const nextTags = reorderCanvasSkillTags(currentTags, draggedId, targetId); + if (nextTags === currentTags) return; + + const nextOrder = nextTags.map((tag) => tag.id); + pendingCanvasSkillTagOrderSaveRef.current = nextOrder; + latestCanvasSkillTagsRef.current = nextTags; + setCanvasSkillTagOrderOverride(nextOrder); + }, + [] + ); + const handleCanvasSkillTagDragOver = useCallback( (event: ReactDragEvent, targetId: string) => { const draggedId = draggingSkillTagIdRef.current; @@ -5451,23 +5505,41 @@ export function CanvasChatPanel({ event.preventDefault(); const draggedId = event.dataTransfer.getData("text/plain") || draggingSkillTagIdRef.current; - draggingSkillTagIdRef.current = null; - setDraggingSkillTagId(null); - if (!draggedId || draggedId === targetId) return; + if (draggedId && draggedId !== targetId && !pendingCanvasSkillTagOrderSaveRef.current) { + const nextTags = reorderCanvasSkillTags( + latestCanvasSkillTagsRef.current, + draggedId, + targetId + ); + if (nextTags !== latestCanvasSkillTagsRef.current) { + const nextOrder = nextTags.map((tag) => tag.id); + pendingCanvasSkillTagOrderSaveRef.current = nextOrder; + latestCanvasSkillTagsRef.current = nextTags; + setCanvasSkillTagOrderOverride(nextOrder); + } + } suppressSkillTagClickRef.current = true; window.setTimeout(() => { suppressSkillTagClickRef.current = false; }, 0); - void handleReorderCanvasSkillTag(draggedId, targetId); }, - [handleReorderCanvasSkillTag] + [] ); const handleCanvasSkillTagDragEnd = useCallback(() => { + const pendingOrder = pendingCanvasSkillTagOrderSaveRef.current; + pendingCanvasSkillTagOrderSaveRef.current = null; draggingSkillTagIdRef.current = null; setDraggingSkillTagId(null); - }, []); + if (pendingOrder?.length) { + suppressSkillTagClickRef.current = true; + window.setTimeout(() => { + suppressSkillTagClickRef.current = false; + }, 0); + void persistCanvasSkillTagOrder(pendingOrder); + } + }, [persistCanvasSkillTagOrder]); useEffect(() => { if (!effectiveAgentEnabled || typeof window === "undefined") return; @@ -12760,6 +12832,9 @@ export function CanvasChatPanel({ onDragStart={(event) => handleCanvasSkillTagDragStart(event, tag.id) } + onDragEnter={(event) => + handleCanvasSkillTagDragEnter(event, tag.id) + } onDragOver={(event) => handleCanvasSkillTagDragOver(event, tag.id) }