Your IP : 216.73.216.79


Current Path : /var/www/pythonian/qwen/static/
Upload File :
Current File : /var/www/pythonian/qwen/static/image-editor.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> Image Editor</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        /* Previous styles remain the same */
        .drop-zone.dragover {
            border-color: #3b82f6;
            background-color: #ebf5ff;
        }
        
        .image-preview {
            position: relative;
            transition: transform 0.2s;
        }
        
        .image-preview:hover {
            transform: scale(1.02);
        }
        
        .image-preview:hover .image-overlay {
            opacity: 1;
        }
        
        .file-size-warning {
            animation: pulse 2s infinite;
        }
        
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.7; }
        }
    </style>
</head>
<body class="bg-gray-50 min-h-screen" x-data="imageEditor()" x-init="init()">
    <div class="container mx-auto px-4 py-8 max-w-6xl">
        <!-- Header -->
        <header class="mb-10 text-center">
            <h1 class="text-4xl font-bold text-gray-800 mb-2">
                <i class="fas fa-magic text-blue-500 mr-2"></i>Image Editor
            </h1>
            <p class="text-gray-600">Upload images or use URLs to edit with AI prompts</p>
        </header>

        <!-- Main Card -->
        <div class="bg-white rounded-xl shadow-lg p-6 mb-8">
            <!-- Upload Method Toggle -->
            <div class="mb-6">
                <div class="flex border border-gray-300 rounded-lg overflow-hidden w-fit">
                    <button 
                        @click="uploadMethod = 'file'"
                        :class="uploadMethod === 'file' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700'"
                        class="px-6 py-2 font-medium transition-colors"
                    >
                        <i class="fas fa-upload mr-2"></i>Upload Files
                    </button>
                    <button 
                        @click="uploadMethod = 'url'"
                        :class="uploadMethod === 'url' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700'"
                        class="px-6 py-2 font-medium transition-colors"
                    >
                        <i class="fas fa-link mr-2"></i>Use URLs
                    </button>
                </div>
            </div>

            <!-- File Upload Section -->
            <div x-show="uploadMethod === 'file'" x-transition class="mb-8">
                <h2 class="text-xl font-semibold text-gray-800 mb-4">
                    <i class="fas fa-cloud-upload-alt text-blue-500 mr-2"></i>Upload Images (Max: 3)
                </h2>
                
                <!-- Drop Zone -->
                <div 
                    class="drop-zone rounded-lg p-8 text-center cursor-pointer mb-4"
                    :class="isDragging ? 'dragover' : ''"
                    @dragover.prevent="isDragging = true"
                    @dragleave.prevent="isDragging = false"
                    @drop.prevent="handleDrop($event)"
                    @click="openFilePicker"
                >
                    <input 
                        type="file" 
                        id="fileInput" 
                        @change="handleFileSelect($event)"
                        multiple
                        accept=".png,.jpg,.jpeg,.gif,.webp,.bmp"
                        class="hidden"
                    >
                    
                    <div class="mb-4">
                        <i class="fas fa-cloud-upload-alt text-4xl text-blue-400 mb-2"></i>
                        <p class="text-lg font-medium text-gray-700">Drop images here or click to upload</p>
                        <p class="text-sm text-gray-500 mt-1">Supported: PNG, JPG, JPEG, GIF, WEBP, BMP</p>
                        <p class="text-xs text-gray-400 mt-1">Max 3 images, 5MB each</p>
                    </div>
                </div>
                
                <!-- File Previews -->
                <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
                    <template x-for="(file, index) in uploadedFiles" :key="file.id">
                        <div class="image-preview border border-gray-200 rounded-lg overflow-hidden bg-gray-50 fade-in">
                            <div class="relative" style="padding-top: 75%;">
                                <img 
                                    :src="file.preview" 
                                    :alt="file.name"
                                    class="absolute top-0 left-0 w-full h-full object-cover"
                                >
                                <div class="image-overlay absolute top-0 left-0 w-full h-full bg-black/50 opacity-0 transition-opacity flex items-center justify-center">
                                    <button 
                                        @click="removeUploadedFile(index)"
                                        class="bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors"
                                    >
                                        <i class="fas fa-trash"></i>
                                    </button>
                                </div>
                                <!-- File size warning -->
                                <div x-show="file.size > 4 * 1024 * 1024" class="absolute top-2 left-2">
                                    <span class="bg-yellow-500 text-white text-xs px-2 py-1 rounded-full flex items-center">
                                        <i class="fas fa-exclamation-triangle mr-1"></i> Large
                                    </span>
                                </div>
                            </div>
                            <div class="p-3">
                                <div class="flex justify-between items-center mb-1">
                                    <span class="text-sm font-medium text-gray-700 truncate" x-text="file.name"></span>
                                    <span class="text-xs text-gray-500" x-text="formatFileSize(file.size)"></span>
                                </div>
                                <div class="text-xs text-gray-500">
                                    <span x-text="file.type.toUpperCase()"></span>
                                    <span class="mx-1">•</span>
                                    <span x-text="file.dimensions || 'Loading...'"></span>
                                </div>
                                <div x-show="file.size > 4 * 1024 * 1024" class="mt-2 text-xs text-yellow-600">
                                    <i class="fas fa-info-circle mr-1"></i>Large file may take longer
                                </div>
                            </div>
                        </div>
                    </template>
                </div>
                
                <div x-show="uploadedFiles.length === 0" class="text-center py-6 text-gray-500">
                    <i class="fas fa-image text-3xl mb-2 opacity-50"></i>
                    <p>No images uploaded yet</p>
                </div>
                
                <div x-show="uploadedFiles.length > 0" class="mt-4 text-right">
                    <button 
                        @click="clearUploadedFiles()"
                        class="px-4 py-2 text-sm text-red-600 hover:text-red-800"
                    >
                        <i class="fas fa-times mr-1"></i>Clear All
                    </button>
                </div>
            </div>

            <!-- URL Input Section -->
            <div x-show="uploadMethod === 'url'" x-transition class="mb-8">
                <div class="flex items-center justify-between mb-4">
                    <h2 class="text-xl font-semibold text-gray-800">
                        <i class="fas fa-link text-blue-500 mr-2"></i>Image URLs (Max: 3)
                    </h2>
                    <button 
                        @click="addUrlInput()" 
                        class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                        :disabled="imageUrls.length >= 3"
                    >
                        <i class="fas fa-plus mr-1"></i> Add URL
                    </button>
                </div>
                
                <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
                    <template x-for="(url, index) in imageUrls" :key="index">
                        <div class="border border-gray-200 rounded-lg p-4 bg-gray-50 fade-in">
                            <div class="flex justify-between items-center mb-2">
                                <span class="font-medium text-gray-700">URL #<span x-text="index+1"></span></span>
                                <button 
                                    @click="removeUrlInput(index)" 
                                    class="text-red-500 hover:text-red-700"
                                    title="Remove URL"
                                >
                                    <i class="fas fa-times"></i>
                                </button>
                            </div>
                            <input 
                                type="text" 
                                x-model="imageUrls[index].url"
                                placeholder="https://example.com/image.jpg"
                                class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                                @input="validateImageUrl(index)"
                            />
                            <div class="mt-2 flex items-center">
                                <span x-show="imageUrls[index].valid" class="text-green-600 text-sm">
                                    <i class="fas fa-check-circle mr-1"></i> Valid URL
                                </span>
                                <span x-show="!imageUrls[index].valid && imageUrls[index].url" class="text-red-600 text-sm">
                                    <i class="fas fa-exclamation-circle mr-1"></i> Invalid URL
                                </span>
                            </div>
                        </div>
                    </template>
                </div>
                
                <div x-show="imageUrls.length === 0" class="text-center py-8 border-2 border-dashed border-gray-300 rounded-lg">
                    <i class="fas fa-link text-gray-400 text-4xl mb-2"></i>
                    <p class="text-gray-500">Add up to 3 image URLs to get started</p>
                </div>
            </div>

            <!-- Prompt Section -->
            <div class="mb-8">
                <h2 class="text-xl font-semibold text-gray-800 mb-4">
                    <i class="fas fa-comment-dots text-blue-500 mr-2"></i>Edit Instructions
                </h2>
                <textarea 
                    x-model="prompt" 
                    rows="4" 
                    placeholder="Describe how you want to edit the image(s). For example: 'Make the background a sunset beach' or 'Replace the car with a bicycle'"
                    class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                ></textarea>
                <div class="flex justify-between mt-2">
                    <span class="text-sm text-gray-500">Required field</span>
                    <span class="text-sm" :class="prompt.length > 0 ? 'text-green-600' : 'text-gray-500'">
                        <span x-text="prompt.length"></span> characters
                    </span>
                </div>
            </div>

            <!-- Advanced Options (unchanged) -->
            <!-- ... same as before ... -->

            <!-- Action Buttons -->
            <div class="flex flex-col sm:flex-row justify-between items-center pt-6 border-t border-gray-200">
                <div class="mb-4 sm:mb-0">
                    <button 
                        @click="resetForm()" 
                        class="px-5 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
                    >
                        <i class="fas fa-redo mr-2"></i>Reset Form
                    </button>
                </div>
                <div class="flex space-x-4">
                    <button 
                        @click="validateAndSubmit()" 
                        :disabled="isLoading || !isFormValid"
                        class="px-8 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-lg hover:from-blue-600 hover:to-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center min-w-[140px]"
                    >
                        <template x-if="isLoading">
                            <div class="spinner mr-2"></div>
                        </template>
                        <template x-if="!isLoading">
                            <i class="fas fa-magic mr-2"></i>
                        </template>
                        <span x-text="isLoading ? 'Editing...' : 'Edit Images'"></span>
                    </button>
                </div>
            </div>
        </div>

        <!-- Results Section -->
        <div x-show="results.length > 0" x-transition class="mt-12 fade-in">
            <div class="flex items-center justify-between mb-6">
                <h2 class="text-2xl font-bold text-gray-800">
                    <i class="fas fa-images text-green-500 mr-2"></i>Generated Images
                </h2>
                <div class="text-sm text-gray-600">
                    <span x-text="results.length"></span> result<span x-show="results.length !== 1">s</span>
                </div>
            </div>
            
            <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
                <template x-for="(result, resultIndex) in results" :key="resultIndex">
                    <div class="bg-white rounded-xl shadow-md overflow-hidden border border-gray-200">
                        <div class="relative overflow-hidden bg-gray-100" style="padding-top: 75%;">
                            <img 
                                :src="result.image_urls[0]" 
                                :alt="'Generated image ' + (resultIndex + 1)"
                                class="absolute top-0 left-0 w-full h-full object-cover"
                                @error="result.error = true"
                            >
                            <div x-show="result.error" class="absolute top-0 left-0 w-full h-full flex items-center justify-center bg-gray-200">
                                <div class="text-center">
                                    <i class="fas fa-exclamation-triangle text-gray-400 text-3xl mb-2"></i>
                                    <p class="text-gray-500">Image failed to load</p>
                                </div>
                            </div>
                            <div class="absolute top-3 right-3 bg-black/70 text-white text-xs px-2 py-1 rounded">
                                <span x-text="result.size || '1024x1536'"></span>
                            </div>
                        </div>
                        
                        <div class="p-4">
                            <div class="mb-3">
                                <div class="text-sm font-medium text-gray-700 mb-1">Prompt:</div>
                                <div class="text-sm text-gray-600 line-clamp-2" x-text="result.prompt"></div>
                            </div>
                            
                            <div class="flex justify-between items-center pt-3 border-t border-gray-100">
                                <a 
                                    :href="result.image_urls[0]" 
                                    target="_blank" 
                                    class="px-3 py-1.5 bg-blue-50 text-blue-600 rounded-md hover:bg-blue-100 transition-colors text-sm font-medium"
                                >
                                    <i class="fas fa-external-link-alt mr-1"></i> Open
                                </a>
                                <button 
                                    @click="downloadImage(result.image_urls[0], resultIndex)"
                                    class="px-3 py-1.5 bg-green-50 text-green-600 rounded-md hover:bg-green-100 transition-colors text-sm font-medium"
                                >
                                    <i class="fas fa-download mr-1"></i> Save
                                </button>
                                <div class="text-xs text-gray-500">
                                    <i class="far fa-clock mr-1"></i>
                                    <span x-text="new Date(result.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})"></span>
                                </div>
                            </div>
                        </div>
                    </div>
                </template>
            </div>
        </div>

        <!-- Error Modal -->
        <div 
            x-show="errorMessage" 
            x-transition 
            class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
            @click.self="errorMessage = null"
        >
            <div class="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
                <div class="flex items-center mb-4">
                    <div class="flex-shrink-0 w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
                        <i class="fas fa-exclamation-triangle text-red-600"></i>
                    </div>
                    <div class="ml-4">
                        <h3 class="text-lg font-semibold text-gray-800">Error</h3>
                    </div>
                </div>
                <div class="mb-6">
                    <p class="text-gray-600" x-text="errorMessage"></p>
                </div>
                <div class="flex justify-end">
                    <button 
                        @click="errorMessage = null" 
                        class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
                    >
                        Close
                    </button>
                </div>
            </div>
        </div>

        <!-- Success Toast -->
        <div 
            x-show="showSuccessToast" 
            x-transition
            class="fixed bottom-4 right-4 bg-green-500 text-white rounded-lg shadow-lg p-4 max-w-sm z-50 fade-in"
        >
            <div class="flex items-center">
                <i class="fas fa-check-circle text-xl mr-3"></i>
                <div>
                    <p class="font-medium">Images generated successfully!</p>
                    <p class="text-sm opacity-90" x-text="'Request ID: ' + lastRequestId"></p>
                </div>
                <button @click="showSuccessToast = false" class="ml-4 text-white/80 hover:text-white">
                    <i class="fas fa-times"></i>
                </button>
            </div>
        </div>
    </div>

    <script>
        function imageEditor() {
            return {
                // State
                uploadMethod: 'file',
                uploadedFiles: [],
                imageUrls: [],
                isDragging: false,
                prompt: '',
                negativePrompt: '',
                n: 1,
                size: '1024*1536',
                promptExtend: true,
                watermark: false,
                model: 'qwen-image-edit-max',
                showAdvanced: false,
                isLoading: false,
                results: [],
                errorMessage: null,
                showSuccessToast: false,
                lastRequestId: null,
                
                // Constants
                modelOptions: [
                    { value: 'qwen-image-edit-max', label: 'Max', description: 'High quality' },
                    { value: 'qwen-image-edit-plus', label: 'Plus', description: 'Fast & efficient' }
                ],
                sizeOptions: [
                    { value: '1024*1024', label: 'Square (1024x1024)' },
                    { value: '1024*1536', label: 'Portrait (1024x1536)' },
                    { value: '1536*1024', label: 'Landscape (1536x1024)' }
                ],
                
                // Computed properties
                get isFormValid() {
                    if (this.uploadMethod === 'file') {
                        return this.uploadedFiles.length > 0 && this.prompt.trim().length > 0;
                    } else {
                        return this.imageUrls.length > 0 && 
                               this.imageUrls.every(img => img.valid) && 
                               this.prompt.trim().length > 0;
                    }
                },
                
                // Methods
                init() {
                    this.addUrlInput();
                    this.loadSavedResults();
                },
                
                loadSavedResults() {
                    const savedResults = localStorage.getItem('qwenImageEditorResults');
                    if (savedResults) {
                        try {
                            this.results = JSON.parse(savedResults);
                        } catch (e) {
                            console.error('Failed to load saved results:', e);
                        }
                    }
                },
                
                // File Upload Methods
                openFilePicker() {
                    document.getElementById('fileInput').click();
                },
                
                handleFileSelect(event) {
                    const files = Array.from(event.target.files);
                    this.processFiles(files);
                    event.target.value = '';
                },
                
                handleDrop(event) {
                    this.isDragging = false;
                    const files = Array.from(event.dataTransfer.files);
                    this.processFiles(files);
                },
                
                async processFiles(files) {
                    const remainingSlots = 3 - this.uploadedFiles.length;
                    const filesToAdd = files.slice(0, remainingSlots);
                    
                    for (const file of filesToAdd) {
                        // Validate file type
                        const fileType = file.type.split('/')[0];
                        if (fileType !== 'image') {
                            this.errorMessage = `File "${file.name}" is not an image`;
                            continue;
                        }
                        
                        // Validate file size (5MB max for Base64)
                        if (file.size > 5 * 1024 * 1024) {
                            this.errorMessage = `File "${file.name}" exceeds 5MB limit for Base64 conversion`;
                            continue;
                        }
                        
                        // Create preview and get dimensions
                        const preview = await this.readFileAsDataURL(file);
                        const dimensions = await this.getImageDimensions(file);
                        
                        this.uploadedFiles.push({
                            id: Date.now() + Math.random(),
                            file: file,
                            name: file.name,
                            size: file.size,
                            type: file.type.split('/')[1].toLowerCase(),
                            preview: preview,
                            dimensions: dimensions
                        });
                    }
                    
                    if (files.length > remainingSlots) {
                        this.errorMessage = `Maximum 3 images allowed. Only ${remainingSlots} were added.`;
                    }
                },
                
                readFileAsDataURL(file) {
                    return new Promise((resolve) => {
                        const reader = new FileReader();
                        reader.onload = (e) => resolve(e.target.result);
                        reader.readAsDataURL(file);
                    });
                },
                
                getImageDimensions(file) {
                    return new Promise((resolve) => {
                        const img = new Image();
                        img.onload = () => {
                            resolve(`${img.width}×${img.height}`);
                        };
                        img.onerror = () => resolve('Unknown');
                        img.src = URL.createObjectURL(file);
                    });
                },
                
                removeUploadedFile(index) {
                    this.uploadedFiles.splice(index, 1);
                },
                
                clearUploadedFiles() {
                    this.uploadedFiles = [];
                },
                
                // URL Methods
                addUrlInput() {
                    if (this.imageUrls.length < 3) {
                        this.imageUrls.push({
                            url: '',
                            valid: false
                        });
                    }
                },
                
                removeUrlInput(index) {
                    this.imageUrls.splice(index, 1);
                },
                
                validateImageUrl(index) {
                    const url = this.imageUrls[index].url.trim();
                    if (!url) {
                        this.imageUrls[index].valid = false;
                        return;
                    }
                    
                    const urlPattern = /^https?:\/\/.+/;
                    this.imageUrls[index].valid = urlPattern.test(url);
                },
                
                formatFileSize(bytes) {
                    if (bytes === 0) return '0 Bytes';
                    const k = 1024;
                    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
                    const i = Math.floor(Math.log(bytes) / Math.log(k));
                    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
                },
                
                // Submit Method
                async validateAndSubmit() {
                    if (!this.isFormValid) {
                        this.errorMessage = this.uploadMethod === 'file' 
                            ? 'Please upload at least one image and provide a prompt'
                            : 'Please provide at least one valid image URL and a prompt';
                        return;
                    }
                    
                    this.isLoading = true;
                    
                    try {
                        let formData = new FormData();
                        
                        // Add images based on upload method
                        if (this.uploadMethod === 'file') {
                            // Add uploaded files
                            this.uploadedFiles.forEach(fileObj => {
                                formData.append('images', fileObj.file);
                            });
                        } else {
                            // For URLs, we need to send as JSON
                            const validUrls = this.imageUrls
                                .filter(img => img.valid)
                                .map(img => img.url.trim());
                            
                            if (validUrls.length === 0) {
                                throw new Error('No valid URLs provided');
                            }
                            
                            // Create JSON payload for URLs
                            const payload = {
                                images: validUrls,
                                prompt: this.prompt.trim(),
                                negative_prompt: this.negativePrompt.trim(),
                                n: this.n,
                                size: this.size,
                                prompt_extend: this.promptExtend,
                                watermark: this.watermark,
                                model: this.model
                            };
                            
                            const response = await fetch('/edit-image', {
                                method: 'POST',
                                headers: {
                                    'Content-Type': 'application/json',
                                },
                                body: JSON.stringify(payload)
                            });
                            
                            return this.handleResponse(response);
                        }
                        
                        // Add other form data for file upload
                        formData.append('prompt', this.prompt.trim());
                        formData.append('negative_prompt', this.negativePrompt.trim());
                        formData.append('n', this.n.toString());
                        formData.append('size', this.size);
                        formData.append('prompt_extend', this.promptExtend.toString());
                        formData.append('watermark', this.watermark.toString());
                        formData.append('model', this.model);
                        
                        const response = await fetch('/edit-image', {
                            method: 'POST',
                            body: formData
                        });
                        
                        return this.handleResponse(response);
                        
                    } catch (error) {
                        this.errorMessage = `Upload failed: ${error.message}`;
                        console.error('Upload Error:', error);
                        this.isLoading = false;
                    }
                },
                
                async handleResponse(response) {
                    const result = await response.json();
                    
                    if (response.ok) {
                        // Add timestamp and store result
                        result.timestamp = new Date().toISOString();
                        this.results.unshift(result);
                        
                        // Keep only last 6 results
                        if (this.results.length > 6) {
                            this.results = this.results.slice(0, 6);
                        }
                        
                        // Save to localStorage
                        localStorage.setItem('qwenImageEditorResults', JSON.stringify(this.results));
                        
                        // Show success
                        this.lastRequestId = result.request_id;
                        this.showSuccessToast = true;
                        
                        setTimeout(() => {
                            this.showSuccessToast = false;
                        }, 5000);
                    } else {
                        this.errorMessage = result.error || `Error: ${response.statusText}`;
                    }
                    
                    this.isLoading = false;
                },
                
                downloadImage(url, index) {
                    const link = document.createElement('a');
                    link.href = url;
                    link.download = `qwen-edited-image-${index + 1}.jpg`;
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                },
                
                resetForm() {
                    this.uploadMethod = 'file';
                    this.uploadedFiles = [];
                    this.imageUrls = [{ url: '', valid: false }];
                    this.prompt = '';
                    this.negativePrompt = '';
                    this.n = 1;
                    this.size = '1024*1536';
                    this.promptExtend = true;
                    this.watermark = false;
                    this.model = 'qwen-image-edit-max';
                }
            }
        }
    </script>
</body>
</html>