PageSourceSearch

https://yopagomenos.co/assets/controllers/article_editor_controller-0d023a88.js

js yopagomenos.co collected 2026-10-09 22:56:01 UTC 3,472 bytes, 122 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2import { formatAiUsage } from "controllers/ai_usage"
3
4const FIELDS = ["title", "subtitle", "meta_description", "body", "tags", "reading_time"]
5
6export default class extends Controller {
7  static targets = ["promptInput", "submitButton", "loading", "error", "undoButton", "usage"]
8  static values = { articleId: String }
9
10  connect() {
11    this.previousValues = null
12  }
13
14  getFieldValues() {
15    const values = {}
16    FIELDS.forEach(field => {
17      const el = document.getElementById(`article_${field}`)
18      if (el) values[field] = el.value
19    })
20    return values
21  }
22
23  setFieldValues(data) {
24    FIELDS.forEach(field => {
25      const el = document.getElementById(`article_${field}`)
26      if (el && data[field] != null) el.value = data[field]
27    })
28  }
29
30  async apply(event) {
31    event.preventDefault()
32
33    const prompt = this.promptInputTarget.value.trim()
34    if (!prompt) {
35      this.showError("Escribe una instrucción primero.")
36      return
37    }
38
39    this.previousValues = this.getFieldValues()
40    this.showLoading()
41
42    try {
43      const response = await fetch(`/admin/articles/${this.articleIdValue}/edit_with_ai`, {
44        method: "POST",
45        credentials: "same-origin",
46        headers: {
47          "Content-Type": "application/json",
48          "Accept": "application/json",
49          "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
50        },
51        body: JSON.stringify({
52          ...this.previousValues,
53          prompt: prompt
54        })
55      })
56
57      const data = await response.json()
58
59      if (!response.ok) {
60        throw new Error(data.error || "Error al iniciar la edición")
61      }
62
63      const result = await this.pollForResult(
64        `/admin/articles/edit_status?job_id=${data.job_id}`
65      )
66      this.setFieldValues(result)
67      // Totales acumulados del artículo (el servidor ya los ha guardado).
68      if (result.ai && this.hasUsageTarget) this.usageTarget.textContent = formatAiUsage(result.ai)
69      this.promptInputTarget.value = ""
70      this.undoButtonTarget.classList.remove("hidden")
71    } catch (e) {
72      this.showError(e.message)
73    } finally {
74      this.hideLoading()
75    }
76  }
77
78  async pollForResult(url) {
79    const timeout = 600000 // mismo tope que el read_timeout del servicio Ruby (10 min)
80    const interval = 2000
81    const start = Date.now()
82
83    while (Date.now() - start < timeout) {
84      await new Promise(r => setTimeout(r, interval))
85
86      const response = await fetch(url, {
87        credentials: "same-origin",
88        headers: { "Accept": "application/json" }
89      })
90      const data = await response.json()
91
92      if (data.status === "done") return data
93      if (data.status === "error") throw new Error(data.error)
94    }
95
96    throw new Error("Timeout: la edición tardó demasiado. Inténtalo de nuevo.")
97  }
98
99  undo() {
100    if (this.previousValues !== null) {
101      this.setFieldValues(this.previousValues)
102      this.previousValues = null
103      this.undoButtonTarget.classList.add("hidden")
104    }
105  }
106
107  showLoading() {
108    this.submitButtonTarget.disabled = true
109    this.loadingTarget.classList.remove("hidden")
110    this.errorTarget.classList.add("hidden")
111  }
112
113  hideLoading() {
114    this.submitButtonTarget.disabled = false
115    this.loadingTarget.classList.add("hidden")
116  }
117
118  showError(message) {
119    this.errorTarget.textContent = message
120    this.errorTarget.classList.remove("hidden")
121  }
122}

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.