1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["nameInput", "typeInput", "descriptionInput", "mainImageInput", 5 "preview", "previewImg", "generateBtn", "generateLabel"] 6 7 // å½ç¨æ·æå¨è¾å ¥/ä¸ä¼ å¾ç URL æ¶ï¼æ´æ°é¢è§ 8 onImageUrlChange() { 9 const url = this.mainImageInputTarget.value.trim() 10 if (url && url.startsWith("http")) { 11 this.showPreview(url) 12 } else { 13 this.hidePreview() 14 } 15 } 16 17 // AI çæäº§åå¾ç 18 async generateImage(event) { 19 event.preventDefault() 20 21 const name = this.nameInputTarget.value.trim() 22 const description = this.descriptionInputTarget.value.trim() 23 const selectedType = this.typeInputTargets.find(r => r.checked) 24 const productType = selectedType ? selectedType.value : "physical" 25 26 if (!name || !description) { 27 this.showToast("error", "Please enter product name and description first") 28 return 29 } 30 31 const btn = this.generateBtnTarget 32 const label = this.generateLabelTarget 33 const originalText = label.textContent 34 35 btn.disabled = true 36 label.textContent = "Generating..." 37 38 try { 39 const response = await fetch("/products/generate_image", { 40 method: "POST", 41 headers: { 42 "Content-Type": "application/json", 43 "X-CSRF-Token": this.getCSRFToken(), 44 "Accept": "application/json" 45 }, 46 body: JSON.stringify({ product_type: productType, name, description }) 47 }) 48 49 const data = await response.json() 50 if (data.success && data.main_image) { 51 // Only nuke the previous file if it was generated THIS session 52 // (tracked on dataset.sessionUploaded). 53 const oldUrl = this.mainImageInputTarget.value.trim() 54 if (oldUrl && this.mainImageInputTarget.dataset.sessionUploaded === oldUrl) { 55 this.deleteOldTempFile(oldUrl) 56 } 57 this.mainImageInputTarget.value = data.main_image 58 this.mainImageInputTarget.dataset.sessionUploaded = data.main_image 59 this.mainImageInputTarget.dispatchEvent(new Event('input', { bubbles: true })) 60 this.showPreview(data.main_image) 61 } else { 62 this.showToast("error", data.error || "Image generation failed") 63 } 64 } catch (error) { 65 console.error("Generate image failed:", error) 66 this.showToast("error", "Network error, please retry") 67 } finally { 68 btn.disabled = false 69 label.textContent = originalText 70 } 71 } 72 73 showPreview(url) { 74 this.previewImgTarget.src = url 75 this.previewTarget.classList.remove("hidden") 76 } 77 78 hidePreview() { 79 this.previewTarget.classList.add("hidden") 80 this.previewImgTarget.src = "" 81 } 82 83 async deleteOldTempFile(url) { 84 try { 85 await fetch('/delete_file', { 86 method: 'DELETE', 87 headers: { 88 'X-CSRF-Token': this.getCSRFToken(), 89 'Content-Type': 'application/json' 90 }, 91 body: JSON.stringify({ url }) 92 }) 93 } catch (e) { 94 // Best effort cleanup, don't block the flow 95 } 96 } 97 98 getCSRFToken() { 99 const token = document.querySelector('meta[name="csrf-token"]') 100 return token ? token.getAttribute("content") : "" 101 } 102 103 showToast(type, message) { 104 const container = document.getElementById("toast-container") || this.createToastContainer() 105 const bgColor = type === "success" ? "bg-ink-900" : "bg-danger-500" 106 const toast = document.createElement("div") 107 toast.className = `toast ${bgColor} text-white px-6 py-3 rounded-xl shadow-lifted flex items-center space-x-3 transform transition-all duration-300 opacity-0 translate-x-full w-full` 108 toast.setAttribute("data-controller", "toast") 109 toast.setAttribute("data-toast-type-value", type) 110 toast.innerHTML = ` 111 <svg class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> 112 ${type === "success" ? '<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>' : '<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>'} 113 </svg>
114 <span class="flex-1 min-w-0">${message}</span> 115 ` 116 container.appendChild(toast) 117 } 118 119 createToastContainer() { 120 const container = document.createElement("div") 121 container.id = "toast-container" 122 container.className = "fixed top-20 right-4 z-[9999] space-y-2 w-80" 123 document.body.appendChild(container) 124 return container 125 } 126}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.