"use client"; import React, { useState, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; import { X, Loader2, Check, RefreshCw, Crosshair } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useCanvasStore, type PickedObject } from "@/store/canvas"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; export function ObjectPickerOverlay() { const isPickerMode = useCanvasStore((state) => state.isPickerMode); const pickerTargetNodeId = useCanvasStore((state) => state.pickerTargetNodeId); const isPickerLoading = useCanvasStore((state) => state.isPickerLoading); const pickedObject = useCanvasStore((state) => state.pickedObject); const setPickerMode = useCanvasStore((state) => state.setPickerMode); const setPickerLoading = useCanvasStore((state) => state.setPickerLoading); const setPickedObject = useCanvasStore((state) => state.setPickedObject); const addPendingPickedObject = useCanvasStore((state) => state.addPendingPickedObject); const getNode = useCanvasStore((state) => state.getNode); const [clickPosition, setClickPosition] = useState<{ x: number; y: number } | null>(null); const overlayRef = useRef(null); // 获取目标节点 const targetNode = pickerTargetNodeId ? getNode(pickerTargetNodeId) : null; const imageUrl = targetNode?.type === 'image' ? (targetNode.data as any)?.src as string | undefined : undefined; // 处理图片点击 const handleImageClick = useCallback(async (e: React.MouseEvent) => { if (isPickerLoading || !imageUrl || !pickerTargetNodeId) return; const img = e.currentTarget; const rect = img.getBoundingClientRect(); // 计算相对坐标 (0-1) const clickX = (e.clientX - rect.left) / rect.width; const clickY = (e.clientY - rect.top) / rect.height; // 显示点击位置 setClickPosition({ x: clickX, y: clickY }); setPickerLoading(true); try { // 直接使用图片 URL 调用 API(不再压缩为 Base64) const response = await fetch('/api/pick-object', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageUrl, // 直接发送图片 URL clickX, clickY, }), }); if (!response.ok) { throw new Error('识别失败'); } const result = await response.json(); if (result.success && result.object) { // 设置识别结果 const picked: PickedObject = { imageUrl, imageNodeId: pickerTargetNodeId, clickX, clickY, objectName: result.object.name, objectPosition: result.object.position, confidence: result.object.confidence, boundingBox: result.object.boundingBox, }; setPickedObject(picked); toast.success(`识别成功: ${result.object.name}`); } else { toast.error(result.error || '未能识别物品'); setClickPosition(null); } } catch (error) { console.error('物品拾取失败:', error); toast.error('识别失败,请重试'); setClickPosition(null); } finally { setPickerLoading(false); } }, [imageUrl, isPickerLoading, pickerTargetNodeId, setPickerLoading, setPickedObject]); // 确认添加到对话 const handleConfirm = useCallback(() => { if (pickedObject) { addPendingPickedObject(pickedObject); toast.success('已添加到对话框'); setPickerMode(false); setClickPosition(null); } }, [pickedObject, addPendingPickedObject, setPickerMode]); // 重新选择 const handleRetry = useCallback(() => { setPickedObject(null); setClickPosition(null); }, [setPickedObject]); // 关闭拾取模式 const handleClose = useCallback(() => { setPickerMode(false); setClickPosition(null); }, [setPickerMode]); if (!isPickerMode || !imageUrl) return null; return createPortal(
{ // 点击背景关闭 if (e.target === overlayRef.current) { handleClose(); } }} > {/* 顶部提示栏 */}
{isPickerLoading ? '正在识别...' : pickedObject ? '识别完成' : '点击图片中的物品进行拾取'}
{/* 图片容器 */}
{/* 图片 */} 拾取目标 {/* 点击位置标记(脉冲动画) */} {clickPosition && (
{/* 脉冲圆点 */}
)} {/* 边界框显示(识别成功后) */} {pickedObject && (
{/* 物品名称标签 */}
{pickedObject.objectName}
)} {/* 加载状态 */} {isPickerLoading && (
正在识别...
)}
{/* 识别结果确认框 */} {pickedObject && !isPickerLoading && (

{pickedObject.objectName}

{pickedObject.objectPosition}

置信度: {Math.round(pickedObject.confidence * 100)}%

)}
, document.body ); }