Files

14 lines
43 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"name": "ai-vision",
"description": "Image analysis and computer vision components",
"dependencies": [
"@hanzo/ui"
],
"files": [
{
"name": "ai-vision.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Grid } from \"@hanzo/ui/grid\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport {\n BarChart3,\n Camera,\n Copy,\n Download,\n Eye,\n FileText,\n Link,\n Loader2,\n Move,\n Palette,\n RotateCw,\n Smile,\n Square,\n Tag,\n Target,\n Type,\n Upload,\n X,\n ZoomIn,\n ZoomOut,\n} from \"lucide-react\"\nimport { toast } from \"sonner\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/registry/default/ui/badge\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/registry/default/ui/card\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/registry/default/ui/dropdown-menu\"\nimport { Input } from \"@/registry/default/ui/input\"\nimport { Label } from \"@/registry/default/ui/label\"\nimport { ScrollArea } from \"@/registry/default/ui/scroll-area\"\nimport { Separator } from \"@/registry/default/ui/separator\"\nimport { Slider } from \"@/registry/default/ui/slider\"\nimport { Switch } from \"@/registry/default/ui/switch\"\nimport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from \"@/registry/default/ui/tabs\"\nimport { Textarea } from \"@/registry/default/ui/textarea\"\n\n// Types\nexport type VisionCapability =\n | \"object-detection\"\n | \"scene-description\"\n | \"ocr\"\n | \"color-analysis\"\n | \"tagging\"\n | \"face-detection\"\n | \"sentiment-analysis\"\n | \"image-generation\"\n | \"image-editing\"\n\nexport interface BoundingBox {\n x: number\n y: number\n width: number\n height: number\n confidence: number\n label: string\n color?: string\n}\n\nexport interface DetectedText {\n text: string\n confidence: number\n boundingBox: BoundingBox\n}\n\nexport interface ColorPalette {\n color: string\n percentage: number\n name?: string\n}\n\nexport interface FaceDetection {\n boundingBox: BoundingBox\n confidence: number\n age?: number\n gender?: string\n emotion?: string\n landmarks?: { x: number; y: number }[]\n}\n\nexport interface VisionResults {\n objects?: BoundingBox[]\n description?: string\n text?: DetectedText[]\n colors?: ColorPalette[]\n tags?: { label: string; confidence: number }[]\n faces?: FaceDetection[]\n sentiment?: {\n emotion: string\n confidence: number\n valence: number\n arousal: number\n }\n metadata?: {\n width: number\n height: number\n format: string\n size: number\n exif?: Record<string, any>\n }\n}\n\nexport interface AIVisionProps extends React.HTMLAttributes<HTMLDivElement> {\n onAnalysis?: (results: VisionResults) => void\n capabilities?: VisionCapability[]\n maxFileSize?: number\n acceptedFormats?: string[]\n apiEndpoint?: string\n apiKey?: string\n defaultImage?: string\n enableCamera?: boolean\n enableUrl?: boolean\n enableGeneration?: boolean\n enableEditing?: boolean\n showConfidence?: boolean\n minConfidence?: number\n}\n\n// Canvas overlay component for visualizations\ninterface CanvasOverlayProps {\n imageRef: React.RefObject<HTMLImageElement | null>\n results: VisionResults\n showBoundingBoxes: boolean\n showConfidence: boolean\n minConfidence: number\n selectedTypes: string[]\n}\n\nconst CanvasOverlay: React.FC<CanvasOverlayProps> = ({\n imageRef,\n results,\n showBoundingBoxes,\n showConfidence,\n minConfidence,\n selectedTypes,\n}) => {\n const canvasRef = React.useRef<HTMLCanvasElement>(null)\n\n React.useEffect(() => {\n if (!canvasRef.current || !imageRef.current || !results) return\n\n const canvas = canvasRef.current\n const ctx = canvas.getContext(\"2d\")\n const image = imageRef.current\n\n if (!ctx) return\n\n // Set canvas size to match image\n canvas.width = image.naturalWidth\n canvas.height = image.naturalHeight\n\n // Clear canvas\n ctx.clearRect(0, 0, canvas.width, canvas.height)\n\n // Draw bounding boxes\n if (\n showBoundingBoxes &&\n results.objects &&\n selectedTypes.includes(\"objects\")\n ) {\n results.objects\n .filter((obj) => obj.confidence >= minConfidence)\n .forEach((obj) => {\n ctx.strokeStyle = obj.color || \"#3b82f6\"\n ctx.lineWidth = 3\n ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)\n\n if (showConfidence) {\n ctx.fillStyle = obj.color || \"#3b82f6\"\n ctx.font = \"14px Inter, sans-serif\"\n const text = `${obj.label} (${Math.round(obj.confidence * 100)}%)`\n const textWidth = ctx.measureText(text).width\n\n ctx.fillRect(obj.x, obj.y - 25, textWidth + 10, 25)\n ctx.fillStyle = \"white\"\n ctx.fillText(text, obj.x + 5, obj.y - 8)\n }\n })\n }\n\n // Draw text regions\n if (showBoundingBoxes && results.text && selectedTypes.includes(\"text\")) {\n results.text\n .filter((text) => text.confidence >= minConfidence)\n .forEach((textItem) => {\n ctx.strokeStyle = \"#10b981\"\n ctx.lineWidth = 2\n ctx.strokeRect(\n textItem.boundingBox.x,\n textItem.boundingBox.y,\n textItem.boundingBox.width,\n textItem.boundingBox.height\n )\n\n if (showConfidence) {\n ctx.fillStyle = \"#10b981\"\n ctx.font = \"12px Inter, sans-serif\"\n const text = `OCR (${Math.round(textItem.confidence * 100)}%)`\n ctx.fillRect(\n textItem.boundingBox.x,\n textItem.boundingBox.y - 20,\n 80,\n 20\n )\n ctx.fillStyle = \"white\"\n ctx.fillText(\n text,\n textItem.boundingBox.x + 5,\n textItem.boundingBox.y - 6\n )\n }\n })\n }\n\n // Draw face detection\n if (showBoundingBoxes && results.faces && selectedTypes.includes(\"faces\")) {\n results.faces\n .filter((face) => face.confidence >= minConfidence)\n .forEach((face) => {\n ctx.strokeStyle = \"#f59e0b\"\n ctx.lineWidth = 3\n ctx.strokeRect(\n face.boundingBox.x,\n face.boundingBox.y,\n face.boundingBox.width,\n face.boundingBox.height\n )\n\n if (showConfidence) {\n ctx.fillStyle = \"#f59e0b\"\n ctx.font = \"14px Inter, sans-serif\"\n const text = `Face (${Math.round(face.confidence * 100)}%)`\n const textWidth = ctx.measureText(text).width\n\n ctx.fillRect(\n face.boundingBox.x,\n face.boundingBox.y - 25,\n textWidth + 10,\n 25\n )\n ctx.fillStyle = \"white\"\n ctx.fillText(text, face.boundingBox.x + 5, face.boundingBox.y - 8)\n }\n\n // Draw landmarks if available\n if (face.landmarks) {\n ctx.fillStyle = \"#f59e0b\"\n face.landmarks.forEach((landmark) => {\n ctx.beginPath()\n ctx.arc(landmark.x, landmark.y, 2, 0, 2 * Math.PI)\n ctx.fill()\n })\n }\n })\n }\n }, [\n imageRef,\n results,\n showBoundingBoxes,\n showConfidence,\n minConfidence,\n selectedTypes,\n ])\n\n return (\n <canvas\n ref={canvasRef}\n className=\"absolute inset-0 pointer-events-none\"\n style={{\n width: imageRef.current?.offsetWidth || \"100%\",\n height: imageRef.current?.offsetHeight || \"100%\",\n }}\n />\n )\n}\n\n// Results panel component\ninterface ResultsPanelProps {\n results: VisionResults | null\n isAnalyzing: boolean\n onExport: (format: \"json\" | \"pdf\") => void\n}\n\nconst ResultsPanel: React.FC<ResultsPanelProps> = ({\n results,\n isAnalyzing,\n onExport,\n}) => {\n const copyToClipboard = (text: string) => {\n navigator.clipboard.writeText(text)\n }\n\n const exportResults = (format: \"json\" | \"pdf\") => {\n if (!results) return\n onExport(format)\n }\n\n if (isAnalyzing) {\n return (\n <Grid\n columns={1}\n gap={16}\n className=\"h-64\"\n style={{ placeContent: \"center\", justifyItems: \"center\" }}\n >\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n <p className=\"text-sm text-muted-foreground\">Analyzing image...</p>\n </Grid>\n )\n }\n\n if (!results) {\n return (\n <Grid\n columns={1}\n gap={16}\n className=\"h-64 text-center\"\n style={{ placeContent: \"center\", justifyItems: \"center\" }}\n >\n <Eye className=\"h-8 w-8 text-muted-foreground\" />\n <p className=\"text-sm text-muted-foreground\">\n Upload an image to see analysis results\n </p>\n </Grid>\n )\n }\n\n return (\n <Grid columns={1} gap={16}>\n {/* Export Controls */}\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-lg font-semibold\">Analysis Results</h3>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\">\n <Download className=\"h-4 w-4 mr-2\" />\n Export\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent>\n <DropdownMenuItem onClick={() => exportResults(\"json\")}>\n <FileText className=\"h-4 w-4 mr-2\" />\n Export as JSON\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => exportResults(\"pdf\")}>\n <FileText className=\"h-4 w-4 mr-2\" />\n Export as PDF\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n\n <Tabs defaultValue=\"overview\" className=\"w-full\">\n <TabsList className=\"grid w-full grid-cols-4\">\n <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n <TabsTrigger value=\"objects\">Objects</TabsTrigger>\n <TabsTrigger value=\"text\">Text</TabsTrigger>\n <TabsTrigger value=\"details\">Details</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"overview\">\n <Grid columns={1} gap={16}>\n {/* Scene Description */}\n {results.description && (\n <Card>\n <CardHeader className=\"pb-3\">\n <CardTitle className=\"text-sm flex items-center\">\n <Eye className=\"h-4 w-4 mr-2\" />\n Scene Description\n </CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm\">{results.description}</p>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 h-6 px-2\"\n onClick={() => copyToClipboard(results.description!)}\n >\n <Copy className=\"h-3 w-3\" />\n </Button>\n </CardContent>\n </Card>\n )}\n\n {/* Tags */}\n {results.tags && results.tags.length > 0 && (\n <Card>\n <CardHeader className=\"pb-3\">\n <CardTitle className=\"text-sm flex items-center\">\n <Tag className=\"h-4 w-4 mr-2\" />\n Tags\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex flex-wrap gap-2\">\n {results.tags.map((tag, index) => (\n <Badge\n key={index}\n variant=\"secondary\"\n className=\"text-xs\"\n >\n {tag.label} ({Math.round(tag.confidence * 100)}%)\n </Badge>\n ))}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Sentiment */}\n {results.sentiment && (\n <Card>\n <CardHeader className=\"pb-3\">\n <CardTitle className=\"text-sm flex items-center\">\n <Smile className=\"h-4 w-4 mr-2\" />\n Sentiment Analysis\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Grid columns={1} gap={8}>\n <Grid\n columns=\"minmax(0, 1fr) auto\"\n gap={8}\n style={{ alignItems: \"center\" }}\n >\n <span className=\"text-sm\">Emotion:</span>\n <Badge>{results.sentiment.emotion}</Badge>\n </Grid>\n <Grid\n columns=\"minmax(0, 1fr) auto\"\n gap={8}\n style={{ alignItems: \"center\" }}\n >\n <span className=\"text-sm\">Confidence:</span>\n <span className=\"text-sm\">\n {Math.round(results.sentiment.confidence * 100)}%\n </span>\n </Grid>\n </Grid>\n </CardContent>\n </Card>\n )}\n </Grid>\n </TabsContent>\n\n <TabsContent value=\"objects\">\n {results.objects && results.objects.length > 0 ? (\n <ScrollArea className=\"h-64\">\n <Grid columns={1} gap={8}>\n {results.objects.map((obj, index) => (\n <Card key={index}>\n <CardContent className=\"p-3\">\n <Grid columns={1} gap={4}>\n <Grid\n columns=\"minmax(0, 1fr) auto\"\n gap={8}\n style={{ alignItems: \"center\" }}\n >\n <div className=\"flex items-center gap-2\">\n <div\n className=\"w-3 h-3 rounded-sm shrink-0\"\n style={{\n backgroundColor: obj.color || \"#3b82f6\",\n }}\n />\n <span className=\"text-sm font-medium\">\n {obj.label}\n </span>\n </div>\n <Badge variant=\"outline\">\n {Math.round(obj.confidence * 100)}%\n </Badge>\n </Grid>\n <div className=\"text-xs text-muted-foreground\">\n Position: ({obj.x}, {obj.y}) Size: {obj.width}×\n {obj.height}\n </div>\n </Grid>\n </CardContent>\n </Card>\n ))}\n </Grid>\n </ScrollArea>\n ) : (\n <Grid\n columns={1}\n gap={8}\n className=\"py-8 text-center\"\n style={{ justifyItems: \"center\" }}\n >\n <Target className=\"h-8 w-8 text-muted-foreground\" />\n <p className=\"text-sm text-muted-foreground\">\n No objects detected\n </p>\n </Grid>\n )}\n </TabsContent>\n\n <TabsContent value=\"text\">\n {results.text && results.text.length > 0 ? (\n <ScrollArea className=\"h-64\">\n <Grid columns={1} gap={8}>\n {results.text.map((textItem, index) => (\n <Card key={index}>\n <CardContent className=\"p-3\">\n <Grid\n columns=\"minmax(0, 1fr) auto\"\n gap={8}\n style={{ alignItems: \"start\" }}\n >\n <Grid columns={1} gap={4}>\n <p className=\"text-sm font-medium\">{textItem.text}</p>\n <div className=\"text-xs text-muted-foreground\">\n Confidence: {Math.round(textItem.confidence * 100)}%\n </div>\n </Grid>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-6 w-6 p-0\"\n onClick={() => copyToClipboard(textItem.text)}\n >\n <Copy className=\"h-3 w-3\" />\n </Button>\n </Grid>\n </CardContent>\n </Card>\n ))}\n </Grid>\n </ScrollArea>\n ) : (\n <Grid\n columns={1}\n gap={8}\n className=\"py-8 text-center\"\n style={{ justifyItems: \"center\" }}\n >\n <Type className=\"h-8 w-8 text-muted-foreground\" />\n <p className=\"text-sm text-muted-foreground\">No text detected</p>\n </Grid>\n )}\n </TabsContent>\n\n <TabsContent value=\"details\">\n <Grid columns={1} gap={16}>\n {/* Color Palette */}\n {results.colors && results.colors.length > 0 && (\n <Card>\n <CardHeader className=\"pb-3\">\n <CardTitle className=\"text-sm flex items-center\">\n <Palette className=\"h-4 w-4 mr-2\" />\n Color Palette\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Grid columns={1} gap={8}>\n {results.colors.map((color, index) => (\n <Grid\n key={index}\n columns=\"auto minmax(0, 1fr) auto\"\n gap={12}\n style={{ alignItems: \"center\" }}\n >\n <div\n className=\"w-6 h-6 rounded border\"\n style={{ backgroundColor: color.color }}\n />\n <div>\n <div className=\"text-sm font-medium\">\n {color.color}\n </div>\n {color.name && (\n <div className=\"text-xs text-muted-foreground\">\n {color.name}\n </div>\n )}\n </div>\n <div className=\"text-sm\">\n {color.percentage.toFixed(1)}%\n </div>\n </Grid>\n ))}\n </Grid>\n </CardContent>\n </Card>\n )}\n\n {/* Face Detection */}\n {results.faces && results.faces.length > 0 && (\n <Card>\n <CardHeader className=\"pb-3\">\n <CardTitle className=\"text-sm flex items-center\">\n <Smile className=\"h-4 w-4 mr-2\" />\n Face Detection\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Grid columns={1} gap={12}>\n {results.faces.map((face, index) => (\n <Grid key={index} columns={1} gap={8}>\n <Grid\n columns=\"minmax(0, 1fr) auto\"\n gap={8}\n style={{ alignItems: \"center\" }}\n >\n <span className=\"text-sm font-medium\">\n Face {index + 1}\n </span>\n <Badge variant=\"outline\">\n {Math.round(face.confidence * 100)}%\n </Badge>\n </Grid>\n {face.age && (\n <div className=\"text-sm\">\n <span className=\"text-muted-foreground\">Age:</span>{\" \"}\n ~{face.age}\n </div>\n )}\n {face.gender && (\n <div className=\"text-sm\">\n <span className=\"text-muted-foreground\">\n Gender:\n </span>{\" \"}\n {face.gender}\n </div>\n )}\n {face.emotion && (\n <div className=\"text-sm\">\n <span className=\"text-muted-foreground\">\n Emotion:\n </span>{\" \"}\n {face.emotion}\n </div>\n )}\n </Grid>\n ))}\n </Grid>\n </CardContent>\n </Card>\n )}\n\n {/* Metadata */}\n {results.metadata && (\n <Card>\n <CardHeader className=\"pb-3\">\n <CardTitle className=\"text-sm flex items-center\">\n <BarChart3 className=\"h-4 w-4 mr-2\" />\n Image Metadata\n </CardTitle>\n </CardHeader>\n <CardContent>\n {/* Label and value in two tracks, so every value in the table\n lines up on the same edge instead of each row justifying\n itself. */}\n <Grid\n columns=\"minmax(0, 1fr) auto\"\n gap={8}\n className=\"text-sm\"\n >\n <span className=\"text-muted-foreground\">Dimensions:</span>\n <span>\n {results.metadata.width} × {results.metadata.height}\n </span>\n <span className=\"text-muted-foreground\">Format:</span>\n <span>{results.metadata.format.toUpperCase()}</span>\n <span className=\"text-muted-foreground\">Size:</span>\n <span>\n {(results.metadata.size / 1024 / 1024).toFixed(2)} MB\n </span>\n </Grid>\n </CardContent>\n </Card>\n )}\n </Grid>\n </TabsContent>\n </Tabs>\n </Grid>\n )\n}\n\n// Main AI Vision component\nconst AIVision = React.forwardRef<HTMLDivElement, AIVisionProps>(\n (\n {\n className,\n onAnalysis,\n capabilities = [\n \"object-detection\",\n \"scene-description\",\n \"ocr\",\n \"color-analysis\",\n \"tagging\",\n ],\n maxFileSize = 10 * 1024 * 1024, // 10MB\n acceptedFormats = [\"image/jpeg\", \"image/png\", \"image/webp\", \"image/gif\"],\n apiEndpoint = \"/api/vision\",\n apiKey,\n defaultImage,\n enableCamera = true,\n enableUrl = true,\n enableGeneration = false,\n enableEditing = false,\n showConfidence = true,\n minConfidence = 0.5,\n ...props\n },\n ref\n ) => {\n // State\n const [currentImage, setCurrentImage] = React.useState<string | null>(\n defaultImage || null\n )\n const [isAnalyzing, setIsAnalyzing] = React.useState(false)\n const [results, setResults] = React.useState<VisionResults | null>(null)\n const [isDragging, setIsDragging] = React.useState(false)\n const [urlInput, setUrlInput] = React.useState(\"\")\n const [showBoundingBoxes, setShowBoundingBoxes] = React.useState(true)\n const [confidenceThreshold, setConfidenceThreshold] =\n React.useState(minConfidence)\n const [selectedTypes, setSelectedTypes] = React.useState<string[]>([\n \"objects\",\n \"text\",\n \"faces\",\n ])\n const [generationPrompt, setGenerationPrompt] = React.useState(\"\")\n const [isGenerating, setIsGenerating] = React.useState(false)\n\n // Refs\n const fileInputRef = React.useRef<HTMLInputElement>(null)\n const imageRef = React.useRef<HTMLImageElement>(null)\n const videoRef = React.useRef<HTMLVideoElement>(null)\n const [isCameraActive, setIsCameraActive] = React.useState(false)\n\n // Mock analysis function - replace with actual API call\n const analyzeImage = async (imageData: string): Promise<VisionResults> => {\n setIsAnalyzing(true)\n\n // Simulate API delay\n await new Promise((resolve) => setTimeout(resolve, 2000))\n\n // Mock results - replace with actual API call\n const mockResults: VisionResults = {\n objects: [\n {\n x: 100,\n y: 150,\n width: 200,\n height: 150,\n confidence: 0.95,\n label: \"Person\",\n color: \"#3b82f6\",\n },\n {\n x: 350,\n y: 200,\n width: 120,\n height: 80,\n confidence: 0.87,\n label: \"Car\",\n color: \"#ef4444\",\n },\n ],\n description:\n \"A street scene with a person walking next to a parked car. The image appears to be taken during daytime with good lighting conditions.\",\n text: [\n {\n text: \"STOP\",\n confidence: 0.96,\n boundingBox: {\n x: 50,\n y: 50,\n width: 80,\n height: 30,\n confidence: 0.96,\n label: \"STOP\",\n },\n },\n ],\n colors: [\n { color: \"#2563eb\", percentage: 25.3, name: \"Blue\" },\n { color: \"#dc2626\", percentage: 18.7, name: \"Red\" },\n { color: \"#16a34a\", percentage: 15.2, name: \"Green\" },\n { color: \"#6b7280\", percentage: 41.8, name: \"Gray\" },\n ],\n tags: [\n { label: \"outdoor\", confidence: 0.98 },\n { label: \"street\", confidence: 0.92 },\n { label: \"urban\", confidence: 0.85 },\n { label: \"daytime\", confidence: 0.89 },\n ],\n faces: [\n {\n boundingBox: {\n x: 120,\n y: 160,\n width: 60,\n height: 80,\n confidence: 0.94,\n label: \"Face\",\n },\n confidence: 0.94,\n age: 35,\n gender: \"male\",\n emotion: \"neutral\",\n },\n ],\n sentiment: {\n emotion: \"neutral\",\n confidence: 0.76,\n valence: 0.2,\n arousal: 0.1,\n },\n metadata: {\n width: 800,\n height: 600,\n format: \"jpeg\",\n size: 1024 * 500, // 500KB\n },\n }\n\n setIsAnalyzing(false)\n return mockResults\n }\n\n // Handle file upload\n const handleFileUpload = async (files: FileList | null) => {\n if (!files || files.length === 0) return\n\n const file = files[0]\n\n // Validate file\n if (!acceptedFormats.includes(file.type)) {\n toast.error(\"Unsupported file format\", {\n description: `Please use: ${acceptedFormats.join(\", \")}`,\n })\n return\n }\n\n if (file.size > maxFileSize) {\n toast.error(\"File too large\", {\n description: `Maximum size: ${maxFileSize / 1024 / 1024}MB`,\n })\n return\n }\n\n // Create image URL\n const imageUrl = URL.createObjectURL(file)\n setCurrentImage(imageUrl)\n\n // Analyze image\n try {\n const results = await analyzeImage(imageUrl)\n setResults(results)\n onAnalysis?.(results)\n } catch (error) {\n console.error(\"Analysis failed:\", error)\n toast.error(\"Failed to analyze image\", {\n description: \"Please try again\",\n action: {\n label: \"Retry\",\n onClick: () => handleFileUpload(files),\n },\n })\n }\n }\n\n // Handle URL input\n const handleUrlSubmit = async () => {\n if (!urlInput.trim()) return\n\n try {\n // Validate URL\n new URL(urlInput)\n setCurrentImage(urlInput)\n\n const results = await analyzeImage(urlInput)\n setResults(results)\n onAnalysis?.(results)\n } catch (error) {\n toast.error(\"Invalid URL or failed to load image\", {\n description: \"Please check the URL and try again\",\n action: {\n label: \"Retry\",\n onClick: () => handleUrlSubmit(),\n },\n })\n }\n }\n\n // Handle camera capture\n const startCamera = async () => {\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { width: 640, height: 480 },\n })\n if (videoRef.current) {\n videoRef.current.srcObject = stream\n setIsCameraActive(true)\n }\n } catch (error) {\n toast.error(\"Failed to access camera\", {\n description: \"Please check camera permissions\",\n })\n }\n }\n\n const capturePhoto = () => {\n if (!videoRef.current) return\n\n const canvas = document.createElement(\"canvas\")\n const video = videoRef.current\n canvas.width = video.videoWidth\n canvas.height = video.videoHeight\n\n const ctx = canvas.getContext(\"2d\")\n ctx?.drawImage(video, 0, 0)\n\n const imageData = canvas.toDataURL(\"image/jpeg\")\n setCurrentImage(imageData)\n\n // Stop camera\n const stream = video.srcObject as MediaStream\n stream?.getTracks().forEach((track) => track.stop())\n setIsCameraActive(false)\n\n // Analyze captured image\n analyzeImage(imageData).then((results) => {\n setResults(results)\n onAnalysis?.(results)\n })\n }\n\n // Handle drag and drop\n const handleDragOver = (e: React.DragEvent) => {\n e.preventDefault()\n setIsDragging(true)\n }\n\n const handleDragLeave = (e: React.DragEvent) => {\n e.preventDefault()\n setIsDragging(false)\n }\n\n const handleDrop = (e: React.DragEvent) => {\n e.preventDefault()\n setIsDragging(false)\n handleFileUpload(e.dataTransfer.files)\n }\n\n // Generate image (mock function)\n const generateImage = async () => {\n if (!generationPrompt.trim()) return\n\n setIsGenerating(true)\n // Simulate generation delay\n await new Promise((resolve) => setTimeout(resolve, 3000))\n\n // Mock generated image URL\n const generatedUrl = `https://picsum.photos/800/600?random=${Date.now()}`\n setCurrentImage(generatedUrl)\n setIsGenerating(false)\n\n // Analyze generated image\n const results = await analyzeImage(generatedUrl)\n setResults(results)\n onAnalysis?.(results)\n }\n\n // Export results\n const exportResults = (format: \"json\" | \"pdf\") => {\n if (!results) return\n\n if (format === \"json\") {\n const dataStr = JSON.stringify(results, null, 2)\n const dataBlob = new Blob([dataStr], { type: \"application/json\" })\n const url = URL.createObjectURL(dataBlob)\n const link = document.createElement(\"a\")\n link.href = url\n link.download = \"vision-analysis.json\"\n link.click()\n URL.revokeObjectURL(url)\n toast.success(\"Analysis exported\", {\n description: \"JSON file has been downloaded\",\n })\n } else if (format === \"pdf\") {\n // PDF export would require a library like jsPDF\n toast.info(\"PDF export not available\", {\n description: \"This feature is not implemented in the demo\",\n })\n }\n }\n\n return (\n <Grid\n ref={ref}\n columns={1}\n rows=\"auto minmax(0, 1fr)\"\n gap={0}\n className={cn(\"h-full\", className)}\n {...props}\n >\n {/* Header */}\n <div className=\"border-b p-4\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold\">AI Vision</h2>\n <p className=\"text-sm text-muted-foreground\">\n Analyze images with AI-powered computer vision\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <Label htmlFor=\"bounding-boxes\" className=\"text-sm\">\n Show Overlays\n </Label>\n <Switch\n id=\"bounding-boxes\"\n checked={showBoundingBoxes}\n onCheckedChange={setShowBoundingBoxes}\n />\n </div>\n </div>\n </div>\n\n <Grid\n columns=\"minmax(0, 1fr) 24rem\"\n gap={0}\n className=\"overflow-hidden\"\n >\n {/* Main Content */}\n <Grid columns={1} rows=\"auto minmax(0, 1fr)\" gap={0}>\n {/* Input Methods */}\n <div className=\"border-b p-4\">\n <Tabs defaultValue=\"upload\" className=\"w-full\">\n <TabsList className=\"grid w-full grid-cols-4\">\n <TabsTrigger value=\"upload\">Upload</TabsTrigger>\n {enableUrl && <TabsTrigger value=\"url\">URL</TabsTrigger>}\n {enableCamera && (\n <TabsTrigger value=\"camera\">Camera</TabsTrigger>\n )}\n {enableGeneration && (\n <TabsTrigger value=\"generate\">Generate</TabsTrigger>\n )}\n </TabsList>\n\n <TabsContent value=\"upload\" className=\"mt-4\">\n <div\n className={cn(\n \"border-2 border-dashed rounded-lg p-8 text-center transition-colors\",\n isDragging\n ? \"border-primary bg-primary/10\"\n : \"border-muted-foreground/25\",\n \"hover:border-primary/50\"\n )}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n onClick={() => fileInputRef.current?.click()}\n >\n <Upload className=\"h-10 w-10 mx-auto mb-4 text-muted-foreground\" />\n <p className=\"text-sm font-medium mb-2\">\n Drop an image here or click to upload\n </p>\n <p className=\"text-xs text-muted-foreground\">\n Supports:{\" \"}\n {acceptedFormats.map((f) => f.split(\"/\")[1]).join(\", \")}\n (Max: {maxFileSize / 1024 / 1024}MB)\n </p>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={acceptedFormats.join(\",\")}\n onChange={(e) => handleFileUpload(e.target.files)}\n className=\"hidden\"\n />\n </div>\n </TabsContent>\n\n {enableUrl && (\n <TabsContent value=\"url\" className=\"mt-4\">\n <Grid columns=\"minmax(0, 1fr) auto\" gap={8}>\n <Input\n placeholder=\"Enter image URL...\"\n value={urlInput}\n onChange={(e) => setUrlInput(e.target.value)}\n onKeyDown={(e) =>\n e.key === \"Enter\" && handleUrlSubmit()\n }\n />\n <Button\n onClick={handleUrlSubmit}\n disabled={!urlInput.trim()}\n >\n <Link className=\"h-4 w-4\" />\n </Button>\n </Grid>\n </TabsContent>\n )}\n\n {enableCamera && (\n <TabsContent value=\"camera\" className=\"mt-4\">\n {!isCameraActive ? (\n <Button onClick={startCamera} className=\"w-full\">\n <Camera className=\"h-4 w-4 mr-2\" />\n Start Camera\n </Button>\n ) : (\n <Grid columns={1} gap={8}>\n <video\n ref={videoRef}\n autoPlay\n playsInline\n className=\"w-full rounded-lg border\"\n />\n <Button onClick={capturePhoto} className=\"w-full\">\n Capture Photo\n </Button>\n </Grid>\n )}\n </TabsContent>\n )}\n\n {enableGeneration && (\n <TabsContent value=\"generate\" className=\"mt-4\">\n <Grid columns={1} gap={12}>\n <Textarea\n placeholder=\"Describe the image you want to generate...\"\n value={generationPrompt}\n onChange={(e) => setGenerationPrompt(e.target.value)}\n className=\"min-h-20\"\n />\n <Button\n onClick={generateImage}\n disabled={!generationPrompt.trim() || isGenerating}\n className=\"w-full\"\n >\n {isGenerating ? (\n <>\n <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n Generating...\n </>\n ) : (\n \"Generate Image\"\n )}\n </Button>\n </Grid>\n </TabsContent>\n )}\n </Tabs>\n </div>\n\n {/* Image Display */}\n <div className=\"p-4 overflow-hidden\">\n {currentImage ? (\n <div className=\"relative h-full border rounded-lg overflow-hidden\">\n <img\n ref={imageRef}\n src={currentImage}\n alt=\"Analysis target\"\n className=\"w-full h-full object-contain\"\n onLoad={() => {\n // Trigger canvas redraw when image loads\n if (results) {\n // Force re-render of canvas overlay\n setShowBoundingBoxes((prev) => !prev)\n setTimeout(() => setShowBoundingBoxes(true), 10)\n }\n }}\n />\n {results && showBoundingBoxes && (\n <CanvasOverlay\n imageRef={imageRef}\n results={results}\n showBoundingBoxes={showBoundingBoxes}\n showConfidence={showConfidence}\n minConfidence={confidenceThreshold}\n selectedTypes={selectedTypes}\n />\n )}\n </div>\n ) : (\n <Grid\n columns={1}\n gap={16}\n className=\"h-full border-2 border-dashed rounded-lg text-center\"\n style={{ placeContent: \"center\", justifyItems: \"center\" }}\n >\n <Eye className=\"h-12 w-12 text-muted-foreground\" />\n <div>\n <p className=\"text-lg font-medium mb-2\">\n No image selected\n </p>\n <p className=\"text-sm text-muted-foreground\">\n Upload an image to start analysis\n </p>\n </div>\n </Grid>\n )}\n </div>\n </Grid>\n\n {/* Results Sidebar */}\n <Grid\n columns={1}\n rows=\"auto minmax(0, 1fr)\"\n gap={0}\n className=\"border-l overflow-hidden\"\n >\n <div className=\"p-4 border-b\">\n <Grid columns={1} gap={16}>\n {/* Confidence Threshold */}\n <Grid columns={1} gap={8}>\n <Label className=\"text-sm\">\n Confidence Threshold:{\" \"}\n {Math.round(confidenceThreshold * 100)}%\n </Label>\n <Slider\n value={[confidenceThreshold]}\n onValueChange={([value]) => setConfidenceThreshold(value)}\n max={1}\n min={0}\n step={0.05}\n className=\"w-full\"\n />\n </Grid>\n\n {/* Detection Types */}\n <Grid columns={1} gap={8}>\n <Label className=\"text-sm\">Show Detection Types</Label>\n <div className=\"flex flex-wrap gap-2\">\n {[\n { key: \"objects\", label: \"Objects\", icon: Target },\n { key: \"text\", label: \"Text\", icon: Type },\n { key: \"faces\", label: \"Faces\", icon: Smile },\n ].map(({ key, label, icon: Icon }) => (\n <Button\n key={key}\n variant={\n selectedTypes.includes(key) ? \"default\" : \"outline\"\n }\n size=\"sm\"\n onClick={() => {\n setSelectedTypes((prev) =>\n prev.includes(key)\n ? prev.filter((t) => t !== key)\n : [...prev, key]\n )\n }}\n >\n <Icon className=\"h-3 w-3 mr-1\" />\n {label}\n </Button>\n ))}\n </div>\n </Grid>\n </Grid>\n </div>\n\n <div className=\"overflow-hidden\">\n <ScrollArea className=\"h-full p-4\">\n <ResultsPanel\n results={results}\n isAnalyzing={isAnalyzing}\n onExport={exportResults}\n />\n </ScrollArea>\n </div>\n </Grid>\n </Grid>\n </Grid>\n )\n }\n)\n\nAIVision.displayName = \"AIVision\"\n\nexport { AIVision }\n"
}
],
"type": "components:ai"
}