diff --git a/inspiration-engine/src/app/api/search/route.ts b/inspiration-engine/src/app/api/search/route.ts new file mode 100644 index 0000000..b09e9a1 --- /dev/null +++ b/inspiration-engine/src/app/api/search/route.ts @@ -0,0 +1,177 @@ +import { NextRequest, NextResponse } from 'next/server'; + +// Mock data for search results - this will be replaced with actual database/vector search +const mockSearchData = [ + { + id: '1', + type: 'image', + title: 'Minimalist Brand Identity', + description: 'Clean, modern logo design with geometric shapes and orange accent colors', + url: 'https://example.com/image1.jpg', + tags: ['branding', 'minimalist', 'logo', 'orange', 'geometric'], + source: 'Instagram', + dateAdded: '2024-01-15', + collection: 'Brand Identity', + thumbnail: '/placeholder-thumbnail.jpg' + }, + { + id: '2', + type: 'image', + title: 'UI Design System', + description: 'Comprehensive design system with components, colors, and typography guidelines', + url: 'https://example.com/image2.jpg', + tags: ['ui', 'design-system', 'components', 'typography', 'colors'], + source: 'Dribbble', + dateAdded: '2024-01-14', + collection: 'UI Inspiration', + thumbnail: '/placeholder-thumbnail.jpg' + }, + { + id: '3', + type: 'link', + title: 'Typography Inspiration', + description: 'Collection of beautiful typography examples and font pairings', + url: 'https://example.com/typography', + tags: ['typography', 'fonts', 'pairings', 'design'], + source: 'Bookmarked', + dateAdded: '2024-01-13', + collection: 'Typography', + thumbnail: '/placeholder-thumbnail.jpg' + }, + { + id: '4', + type: 'video', + title: 'Motion Graphics Tutorial', + description: 'Advanced After Effects techniques for creating smooth animations', + url: 'https://example.com/video1.mp4', + tags: ['motion', 'animation', 'after-effects', 'tutorial'], + source: 'YouTube', + dateAdded: '2024-01-12', + collection: 'Motion Graphics', + thumbnail: '/placeholder-thumbnail.jpg' + }, + { + id: '5', + type: 'image', + title: 'Color Palette Inspiration', + description: 'Warm earth tones and orange gradients for autumn-themed designs', + url: 'https://example.com/image3.jpg', + tags: ['colors', 'palette', 'orange', 'earth-tones', 'autumn'], + source: 'Pinterest', + dateAdded: '2024-01-11', + collection: 'Color Palettes', + thumbnail: '/placeholder-thumbnail.jpg' + } +]; + +// Simple semantic search simulation +function performSemanticSearch(query: string, data: typeof mockSearchData) { + const normalizedQuery = query.toLowerCase(); + + // Simple keyword matching with scoring + return data + .map(item => { + let score = 0; + + // Title matching (highest weight) + if (item.title.toLowerCase().includes(normalizedQuery)) score += 10; + + // Description matching (medium weight) + if (item.description.toLowerCase().includes(normalizedQuery)) score += 5; + + // Tag matching (medium weight) + if (item.tags.some(tag => tag.toLowerCase().includes(normalizedQuery))) score += 5; + + // Collection matching (low weight) + if (item.collection.toLowerCase().includes(normalizedQuery)) score += 2; + + // Source matching (low weight) + if (item.source.toLowerCase().includes(normalizedQuery)) score += 1; + + return { ...item, relevanceScore: score }; + }) + .filter(item => item.relevanceScore > 0) + .sort((a, b) => b.relevanceScore - a.relevanceScore); +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const query = searchParams.get('q'); + const type = searchParams.get('type'); // image, video, link, all + const limit = parseInt(searchParams.get('limit') || '20'); + + if (!query || query.trim().length === 0) { + return NextResponse.json({ + results: [], + total: 0, + query: '', + message: 'Please provide a search query' + }); + } + + // Perform semantic search + let results = performSemanticSearch(query.trim(), mockSearchData); + + // Filter by type if specified + if (type && type !== 'all') { + results = results.filter(item => item.type === type); + } + + // Limit results + results = results.slice(0, limit); + + // Simulate search time for realistic feel + await new Promise(resolve => setTimeout(resolve, 150)); + + return NextResponse.json({ + results: results.map(({ relevanceScore, ...item }) => item), // Remove score from response + total: results.length, + query: query.trim(), + searchTime: '150ms' + }); + + } catch (error) { + console.error('Search API error:', error); + return NextResponse.json( + { error: 'Search failed', message: 'An error occurred while searching' }, + { status: 500 } + ); + } +} + +// Handle image-based search (for future implementation) +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const image = formData.get('image') as File; + + if (!image) { + return NextResponse.json( + { error: 'No image provided' }, + { status: 400 } + ); + } + + // TODO: Implement image-based semantic search + // This would involve: + // 1. Processing the uploaded image + // 2. Generating embeddings/features + // 3. Comparing with stored item embeddings + // 4. Returning similar items + + return NextResponse.json({ + results: [], + total: 0, + message: 'Image search not yet implemented', + searchTime: '0ms' + }); + + } catch (error) { + console.error('Image search API error:', error); + return NextResponse.json( + { error: 'Image search failed', message: 'An error occurred while searching by image' }, + { status: 500 } + ); + } +} diff --git a/inspiration-engine/src/app/api/upload/route.ts b/inspiration-engine/src/app/api/upload/route.ts new file mode 100644 index 0000000..2dd67a7 --- /dev/null +++ b/inspiration-engine/src/app/api/upload/route.ts @@ -0,0 +1,168 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const file = formData.get('file') as File; + const url = formData.get('url') as string; + + if (!file && !url) { + return NextResponse.json( + { error: 'No file or URL provided' }, + { status: 400 } + ); + } + + // TODO: Implement actual file upload to Cloudflare R2 + // For now, we'll simulate the upload process + + if (file) { + // Validate file type + if (!file.type.startsWith('image/')) { + return NextResponse.json( + { error: 'File must be an image' }, + { status: 400 } + ); + } + + // Validate file size (10MB max) + if (file.size > 10 * 1024 * 1024) { + return NextResponse.json( + { error: 'File size must be less than 10MB' }, + { status: 400 } + ); + } + + // Simulate upload processing + await new Promise(resolve => setTimeout(resolve, 1000)); + + // TODO: + // 1. Upload to Cloudflare R2 + // 2. Generate thumbnail + // 3. Extract metadata (dimensions, colors, etc.) + // 4. Generate embeddings for semantic search + // 5. Store in database with metadata + + const mockUploadResult = { + id: `upload_${Date.now()}`, + filename: file.name, + size: file.size, + type: file.type, + url: `https://storage.example.com/uploads/${file.name}`, + thumbnail: `https://storage.example.com/thumbnails/${file.name}`, + metadata: { + width: 1920, + height: 1080, + colors: ['#f97316', '#ea580c', '#fbbf24'], + dominantColor: '#f97316' + }, + uploadTime: new Date().toISOString() + }; + + return NextResponse.json({ + success: true, + data: mockUploadResult, + message: 'File uploaded successfully' + }); + } + + if (url) { + // TODO: Implement URL processing + // 1. Fetch image from URL + // 2. Validate it's an image + // 3. Download and process + // 4. Generate embeddings + // 5. Store in database + + const mockUrlResult = { + id: `url_${Date.now()}`, + url: url, + thumbnail: url, // For now, use original URL as thumbnail + metadata: { + width: 800, + height: 600, + colors: ['#f97316', '#ea580c'], + dominantColor: '#f97316' + }, + processedTime: new Date().toISOString() + }; + + return NextResponse.json({ + success: true, + data: mockUrlResult, + message: 'URL processed successfully' + }); + } + + } catch (error) { + console.error('Upload API error:', error); + return NextResponse.json( + { error: 'Upload failed', message: 'An error occurred while processing the upload' }, + { status: 500 } + ); + } +} + +// Handle image-based search after upload +export async function PUT(request: NextRequest) { + try { + const { imageId, searchType = 'similar' } = await request.json(); + + if (!imageId) { + return NextResponse.json( + { error: 'Image ID is required' }, + { status: 400 } + ); + } + + // TODO: Implement image-based semantic search + // This would involve: + // 1. Retrieve image embeddings from database + // 2. Perform vector similarity search + // 3. Return similar items from user's library + + // Mock search results for now + const mockSimilarResults = [ + { + id: 'similar_1', + type: 'image', + title: 'Similar Orange Design', + description: 'Another design with orange color scheme and similar composition', + url: 'https://example.com/similar1.jpg', + tags: ['orange', 'design', 'similar'], + source: 'Previous Upload', + dateAdded: '2024-01-10', + collection: 'Uploaded Images', + thumbnail: 'https://example.com/similar1-thumb.jpg', + similarity: 0.87 + }, + { + id: 'similar_2', + type: 'image', + title: 'Orange Brand Identity', + description: 'Brand identity with similar orange tones and geometric elements', + url: 'https://example.com/similar2.jpg', + tags: ['branding', 'orange', 'geometric'], + source: 'Instagram', + dateAdded: '2024-01-08', + collection: 'Brand Identity', + thumbnail: 'https://example.com/similar2-thumb.jpg', + similarity: 0.72 + } + ]; + + return NextResponse.json({ + results: mockSimilarResults, + total: mockSimilarResults.length, + searchType, + imageId + }); + + } catch (error) { + console.error('Image search API error:', error); + return NextResponse.json( + { error: 'Image search failed', message: 'An error occurred while searching by image' }, + { status: 500 } + ); + } +} diff --git a/inspiration-engine/src/app/page.tsx b/inspiration-engine/src/app/page.tsx index 92a967c..eeabdfa 100644 --- a/inspiration-engine/src/app/page.tsx +++ b/inspiration-engine/src/app/page.tsx @@ -1,14 +1,141 @@ 'use client'; -import React, { useState } from 'react'; -import { Search, Grid, List, User, Plus, Image, Upload } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { Search, Grid, List, User, Plus, Image, Upload, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent } from '@/components/ui/card'; +import { useSearch, SearchResult } from '@/hooks/useSearch'; +import { SearchResults } from '@/components/SearchResults'; +import { ImageDropzone } from '@/components/ImageDropzone'; +import { ItemDetailsModal } from '@/components/ItemDetailsModal'; export default function Home() { const [activeView, setActiveView] = useState('library'); const [showDropzone, setShowDropzone] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedItem, setSelectedItem] = useState(null); + const [showItemModal, setShowItemModal] = useState(false); + + const { + query, + results, + isLoading, + error, + hasSearched, + total, + searchTime, + search, + clearSearch + } = useSearch(); + + // Handle search input changes + const handleSearchChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchQuery(value); + + if (value.trim()) { + search(value); + } else { + clearSearch(); + } + }; + + // Handle search result item click + const handleItemClick = (item: SearchResult) => { + setSelectedItem(item); + setShowItemModal(true); + }; + + // Handle modal close + const handleModalClose = () => { + setShowItemModal(false); + setSelectedItem(null); + }; + + // Handle item actions + const handleEditItem = (item: SearchResult) => { + console.log('Edit item:', item); + // TODO: Open edit modal/form + }; + + const handleDeleteItem = (item: SearchResult) => { + console.log('Delete item:', item); + // TODO: Implement delete functionality + if (confirm('Are you sure you want to delete this item?')) { + // Delete logic here + handleModalClose(); + } + }; + + const handleAddToCollection = (item: SearchResult) => { + console.log('Add to collection:', item); + // TODO: Open collection selector + }; + + const handleToggleFavorite = (item: SearchResult) => { + console.log('Toggle favorite:', item); + // TODO: Implement favorite toggle + }; + + // Handle image upload + const handleImageUpload = async (file: File) => { + try { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch('/api/upload', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error('Upload failed'); + } + + const result = await response.json(); + console.log('Upload successful:', result); + + // TODO: Perform image-based search after upload + // This would call the search API with the uploaded image + + // For now, just show a success message + alert('Image uploaded successfully! Image-based search coming soon.'); + + } catch (error) { + console.error('Upload error:', error); + alert('Upload failed. Please try again.'); + } + }; + + // Handle URL upload + const handleUrlUpload = async (url: string) => { + try { + const formData = new FormData(); + formData.append('url', url); + + const response = await fetch('/api/upload', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error('URL processing failed'); + } + + const result = await response.json(); + console.log('URL processed:', result); + + // TODO: Perform image-based search after processing + + // For now, just show a success message + alert('URL processed successfully! Image-based search coming soon.'); + + } catch (error) { + console.error('URL processing error:', error); + alert('URL processing failed. Please check the URL and try again.'); + } + }; // Mock data for library items const mockItems = [ @@ -223,8 +350,23 @@ export default function Home() { + {searchQuery && ( + + )} - - - - - - )} + {/* Image Search Dropzone */} + setShowDropzone(false)} + onUpload={handleImageUpload} + onUrlUpload={handleUrlUpload} + /> {/* Active View */} - {activeView === 'library' ? : } + {hasSearched ? ( + + ) : ( + activeView === 'library' ? : + )} + + {/* Item Details Modal */} + ); } diff --git a/inspiration-engine/src/components/ImageDropzone.tsx b/inspiration-engine/src/components/ImageDropzone.tsx new file mode 100644 index 0000000..b53b307 --- /dev/null +++ b/inspiration-engine/src/components/ImageDropzone.tsx @@ -0,0 +1,172 @@ +'use client'; + +import React, { useCallback, useState } from 'react'; +import { useDropzone } from 'react-dropzone'; +import { Upload, Image, Link, X, FileImage } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent } from '@/components/ui/card'; + +interface ImageDropzoneProps { + isOpen: boolean; + onClose: () => void; + onUpload: (file: File) => void; + onUrlUpload: (url: string) => void; +} + +export function ImageDropzone({ isOpen, onClose, onUpload, onUrlUpload }: ImageDropzoneProps) { + const [urlInput, setUrlInput] = useState(''); + const [isDragActive, setIsDragActive] = useState(false); + + const onDrop = useCallback((acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + onUpload(acceptedFiles[0]); + onClose(); + } + }, [onUpload, onClose]); + + const { getRootProps, getInputProps, isDragReject } = useDropzone({ + onDrop, + accept: { + 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'], + }, + maxFiles: 1, + maxSize: 10 * 1024 * 1024, // 10MB + onDragEnter: () => setIsDragActive(true), + onDragLeave: () => setIsDragActive(false), + }); + + const handleUrlSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (urlInput.trim()) { + onUrlUpload(urlInput.trim()); + setUrlInput(''); + onClose(); + } + }; + + if (!isOpen) return null; + + return ( +
+ + +
+
+
+ +
+
+

Search by Image

+

+ Upload an image to find similar items in your library +

+
+
+ +
+ +
+ {/* Drag and Drop Zone */} +
+ + + {isDragActive ? ( +
+ +

Drop your image here

+
+ ) : ( +
+ +
+

Drop your image to search

+

+ Drag and drop, or click to browse +

+
+ +
+ )} + + {isDragReject && ( +
+
+

Invalid file type

+

Please select an image file

+
+
+ )} +
+ + {/* Divider */} +
+
+
+
+
+ or +
+
+ + {/* URL Input */} +
+
+
+ + setUrlInput(e.target.value)} + className="pl-10" + /> +
+ +
+
+ + {/* Supported formats */} +
+ Supports: JPEG, PNG, GIF, WebP, SVG (max 10MB) +
+
+ + +
+ ); +} diff --git a/inspiration-engine/src/components/ItemDetailsModal.tsx b/inspiration-engine/src/components/ItemDetailsModal.tsx new file mode 100644 index 0000000..d39a218 --- /dev/null +++ b/inspiration-engine/src/components/ItemDetailsModal.tsx @@ -0,0 +1,270 @@ +'use client'; + +import React from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Image, + Video, + Link, + FileText, + Calendar, + Tag, + ExternalLink, + Heart, + Plus, + Share, + Download, + Copy, + Trash2, + Edit +} from 'lucide-react'; +import { SearchResult } from '@/hooks/useSearch'; + +interface ItemDetailsModalProps { + item: SearchResult | null; + isOpen: boolean; + onClose: () => void; + onEdit?: (item: SearchResult) => void; + onDelete?: (item: SearchResult) => void; + onAddToCollection?: (item: SearchResult) => void; + onToggleFavorite?: (item: SearchResult) => void; +} + +const getTypeIcon = (type: SearchResult['type']) => { + switch (type) { + case 'image': + return ; + case 'video': + return