"use client"; import React, { useState, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; import { X, Loader2, 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 setPickerMode = useCanvasStore((state) => state.setPickerMode); const setPickerLoading = useCanvasStore((state) => state.setPickerLoading); 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(无任何标记) 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, }; // 直接添加到对话框,不询问确认 addPendingPickedObject(picked); toast.success(`已添加: ${result.object.name}`); // 关闭拾取模式 setPickerMode(false); setClickPosition(null); } else { toast.error(result.error || '未能识别物品'); setClickPosition(null); } } catch (error) { console.error('物品拾取失败:', error); toast.error('识别失败,请重试'); setClickPosition(null); } finally { setPickerLoading(false); } }, [imageUrl, isPickerLoading, pickerTargetNodeId, setPickerLoading, addPendingPickedObject, setPickerMode]); // 关闭拾取模式 const handleClose = useCallback(() => { setPickerMode(false); setClickPosition(null); }, [setPickerMode]); if (!isPickerMode || !imageUrl) return null; return createPortal(
{ // 点击背景关闭 if (e.target === overlayRef.current) { handleClose(); } }} > {/* 顶部提示栏 */}
{isPickerLoading ? '正在识别...' : '点击图片中的物品进行拾取'}
{/* 图片容器 */}
{/* 图片 - 原始图片,无任何标记 */} 拾取目标 {/* 点击位置标记(脉冲动画)- 仅在前端显示,不影响发送给 AI 的图片 */} {clickPosition && (
{/* 脉冲圆点 */}
)} {/* 加载状态 */} {isPickerLoading && (
正在识别...
)}
, document.body ); }