PageSourceSearch

https://agrihub-app.herokuapp.com/assets/controllers/ai-chat-messa…f26ba20170c44c1946ceddd8d4df22701a5f6fa4.js

js agrihub-app.herokuapp.com collected 2026-10-03 14:46:57 UTC 2,996 bytes, 89 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = ["content", "submit", "attachments", "attachmentButton", "attachmentCount"]
5
6  appendUserMessage() {
7    const content = this.contentTarget.value.trim()
8    const attachmentFiles = Array.from(this.attachmentsTarget.files || [])
9    if (content === "" && attachmentFiles.length === 0) return
10
11    document.getElementById("ai-chat-messages-empty")?.remove()
12    document.getElementById("ai-chat-messages")?.appendChild(this.buildUserMessage(content, attachmentFiles))
13
14    // スクロールを一番下に移動
15    const scrollContainer = document.getElementById("ai-chat-messages")
16    if (scrollContainer) {
17      scrollContainer.scrollTop = scrollContainer.scrollHeight
18    }
19
20    const statusElement = document.getElementById("ai-chat-thread-status")
21    if (statusElement) {
22      statusElement.textContent = "AIが応答中です..."
23    }
24
25    // ブラウザのフォーム送信処理(submit)を妨げないよう、disabled化を次のイベントループに回す
26    setTimeout(() => {
27      this.contentTarget.disabled = true
28      this.attachmentsTarget.disabled = true
29      this.attachmentButtonTarget.disabled = true
30      this.submitTarget.disabled = true
31    }, 0)
32  }
33
34  openFileDialog(event) {
35    event.preventDefault()
36    this.attachmentsTarget.focus()
37    this.attachmentsTarget.click()
38  }
39
40  previewAttachments() {
41    const attachmentCount = this.attachmentsTarget.files.length
42    this.attachmentButtonTarget.classList.toggle("has-attachments", attachmentCount > 0)
43    this.attachmentCountTarget.hidden = attachmentCount === 0
44    this.attachmentCountTarget.textContent = attachmentCount.toString()
45  }
46
47  buildUserMessage(content, attachmentFiles) {
48    const wrapper = document.createElement("div")
49    wrapper.className = "ai-chat-message-wrapper user-message"
50    wrapper.dataset.testid = "ai-chat-message-user"
51
52    const bubble = document.createElement("div")
53    bubble.className = "ai-chat-message-bubble"
54
55    if (content !== "") {
56      const text = document.createElement("p")
57      text.textContent = content
58      bubble.appendChild(text)
59    }
60
61    if (attachmentFiles.length > 0) {
62      bubble.appendChild(this.buildAttachmentPreview(attachmentFiles))
63    }
64
65    wrapper.appendChild(bubble)
66
67    return wrapper
68  }
69
70  buildAttachmentPreview(attachmentFiles) {
71    const attachments = document.createElement("div")
72    attachments.className = "ai-chat-message-attachments"
73
74    attachmentFiles.forEach((attachmentFile) => {
75      const image = document.createElement("img")
76      image.className = "ai-chat-message-image"
77      image.alt = attachmentFile.name
78      image.dataset.testid = "ai-chat-message-image"
79
80      const objectUrl = URL.createObjectURL(attachmentFile)
81      image.src = objectUrl
82      image.onload = () => URL.revokeObjectURL(objectUrl)
83
84      attachments.appendChild(image)
85    })
86
87    return attachments
88  }
89};

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.