mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-11-03 03:18:52 +00:00
14 lines
43 KiB
JSON
14 lines
43 KiB
JSON
{
|
||
"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"
|
||
} |