PageSourceSearch

https://autowhisper.xyz/assets/controllers/create_product_image_controller-91649ff3.js

js autowhisper.xyz collected 2026-10-02 01:11:08 UTC 4,634 bytes, 126 lines download raw bytes

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.