| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 1 | import { css, html, LitElement, PropertyValues } from "lit"; |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 2 | import { customElement, property, state, query } from "lit/decorators.js"; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 3 | |
| 4 | @customElement("sketch-chat-input") |
| 5 | export class SketchChatInput extends LitElement { |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 6 | @state() |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 7 | content: string = ""; |
| 8 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 9 | @state() |
| 10 | isDraggingOver: boolean = false; |
| 11 | |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 12 | @state() |
| 13 | uploadsInProgress: number = 0; |
| 14 | |
| 15 | @state() |
| 16 | showUploadInProgressMessage: boolean = false; |
| 17 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 18 | // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS. |
| 19 | // Note that these styles only apply to the scope of this web component's |
| 20 | // shadow DOM node, so they won't leak out or collide with CSS declared in |
| 21 | // other components or the containing web page (...unless you want it to do that). |
| 22 | static styles = css` |
| 23 | /* Chat styles - exactly matching timeline.css */ |
| 24 | .chat-container { |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 25 | width: 100%; |
| 26 | background: #f0f0f0; |
| 27 | padding: 15px; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 28 | min-height: 40px; /* Ensure minimum height */ |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 29 | position: relative; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 30 | } |
| 31 | |
| 32 | .chat-input-wrapper { |
| 33 | display: flex; |
| 34 | max-width: 1200px; |
| 35 | margin: 0 auto; |
| 36 | gap: 10px; |
| 37 | } |
| 38 | |
| 39 | #chatInput { |
| 40 | flex: 1; |
| 41 | padding: 12px; |
| 42 | border: 1px solid #ddd; |
| 43 | border-radius: 4px; |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 44 | resize: vertical; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 45 | font-family: monospace; |
| 46 | font-size: 12px; |
| 47 | min-height: 40px; |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 48 | max-height: 300px; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 49 | background: #f7f7f7; |
| Sean McCullough | 5164eee | 2025-04-21 18:20:23 -0700 | [diff] [blame] | 50 | overflow-y: auto; |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 51 | box-sizing: border-box; /* Ensure padding is included in height calculation */ |
| 52 | line-height: 1.4; /* Consistent line height for better height calculation */ |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 53 | } |
| 54 | |
| 55 | #sendChatButton { |
| 56 | background-color: #2196f3; |
| 57 | color: white; |
| 58 | border: none; |
| 59 | border-radius: 4px; |
| 60 | padding: 0 20px; |
| 61 | cursor: pointer; |
| 62 | font-weight: 600; |
| Pokey Rule | 97188fc | 2025-04-23 15:50:04 +0100 | [diff] [blame] | 63 | align-self: center; |
| 64 | height: 40px; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 65 | } |
| 66 | |
| 67 | #sendChatButton:hover { |
| 68 | background-color: #0d8bf2; |
| 69 | } |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 70 | |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 71 | #sendChatButton:disabled { |
| 72 | background-color: #b0b0b0; |
| 73 | cursor: not-allowed; |
| 74 | } |
| 75 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 76 | /* Drop zone styling */ |
| 77 | .drop-zone-overlay { |
| 78 | position: absolute; |
| 79 | top: 0; |
| 80 | left: 0; |
| 81 | right: 0; |
| 82 | bottom: 0; |
| 83 | background-color: rgba(33, 150, 243, 0.1); |
| 84 | border: 2px dashed #2196f3; |
| 85 | border-radius: 4px; |
| 86 | display: flex; |
| 87 | justify-content: center; |
| 88 | align-items: center; |
| 89 | z-index: 10; |
| 90 | pointer-events: none; |
| 91 | } |
| 92 | |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 93 | .drop-zone-message, |
| 94 | .upload-progress-message { |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 95 | background-color: #ffffff; |
| 96 | padding: 15px 20px; |
| 97 | border-radius: 4px; |
| 98 | font-weight: 600; |
| 99 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
| 100 | } |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 101 | |
| 102 | .upload-progress-message { |
| 103 | position: absolute; |
| 104 | bottom: 70px; |
| 105 | left: 50%; |
| 106 | transform: translateX(-50%); |
| 107 | background-color: #fff9c4; |
| 108 | border: 1px solid #fbc02d; |
| 109 | z-index: 20; |
| 110 | font-size: 14px; |
| 111 | animation: fadeIn 0.3s ease-in-out; |
| 112 | } |
| 113 | |
| 114 | @keyframes fadeIn { |
| 115 | from { |
| 116 | opacity: 0; |
| 117 | transform: translateX(-50%) translateY(10px); |
| 118 | } |
| 119 | to { |
| 120 | opacity: 1; |
| 121 | transform: translateX(-50%) translateY(0); |
| 122 | } |
| 123 | } |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 124 | `; |
| 125 | |
| 126 | constructor() { |
| 127 | super(); |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 128 | this._handleDiffComment = this._handleDiffComment.bind(this); |
| Josh Bleecher Snyder | bd52faf | 2025-06-02 21:21:37 +0000 | [diff] [blame] | 129 | this._handleTodoComment = this._handleTodoComment.bind(this); |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 130 | this._handleDragOver = this._handleDragOver.bind(this); |
| 131 | this._handleDragEnter = this._handleDragEnter.bind(this); |
| 132 | this._handleDragLeave = this._handleDragLeave.bind(this); |
| 133 | this._handleDrop = this._handleDrop.bind(this); |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 134 | } |
| 135 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 136 | connectedCallback() { |
| 137 | super.connectedCallback(); |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 138 | window.addEventListener("diff-comment", this._handleDiffComment); |
| Josh Bleecher Snyder | bd52faf | 2025-06-02 21:21:37 +0000 | [diff] [blame] | 139 | window.addEventListener("todo-comment", this._handleTodoComment); |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 140 | } |
| 141 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 142 | // Utility function to handle file uploads (used by both paste and drop handlers) |
| 143 | private async _uploadFile(file: File, insertPosition: number) { |
| 144 | // Insert a placeholder at the cursor position |
| 145 | const textBefore = this.content.substring(0, insertPosition); |
| 146 | const textAfter = this.content.substring(insertPosition); |
| 147 | |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 148 | // Add a loading indicator with a visual cue |
| 149 | const loadingText = `[🔄 Uploading ${file.name}...]`; |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 150 | this.content = `${textBefore}${loadingText}${textAfter}`; |
| 151 | |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 152 | // Increment uploads in progress counter |
| 153 | this.uploadsInProgress++; |
| 154 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 155 | // Adjust spacing immediately to show loading indicator |
| 156 | requestAnimationFrame(() => this.adjustChatSpacing()); |
| 157 | |
| 158 | try { |
| 159 | // Create a FormData object to send the file |
| 160 | const formData = new FormData(); |
| 161 | formData.append("file", file); |
| 162 | |
| 163 | // Upload the file to the server using a relative path |
| 164 | const response = await fetch("./upload", { |
| 165 | method: "POST", |
| 166 | body: formData, |
| 167 | }); |
| 168 | |
| 169 | if (!response.ok) { |
| 170 | throw new Error(`Upload failed: ${response.statusText}`); |
| 171 | } |
| 172 | |
| 173 | const data = await response.json(); |
| 174 | |
| 175 | // Replace the loading placeholder with the actual file path |
| 176 | this.content = this.content.replace(loadingText, `[${data.path}]`); |
| 177 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 178 | return data.path; |
| 179 | } catch (error) { |
| 180 | console.error("Failed to upload file:", error); |
| 181 | |
| 182 | // Replace loading indicator with error message |
| 183 | const errorText = `[Upload failed: ${error.message}]`; |
| 184 | this.content = this.content.replace(loadingText, errorText); |
| 185 | |
| 186 | // Adjust spacing to show error message |
| 187 | requestAnimationFrame(() => { |
| 188 | this.adjustChatSpacing(); |
| 189 | this.chatInput.focus(); |
| 190 | }); |
| 191 | |
| 192 | throw error; |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 193 | } finally { |
| 194 | // Always decrement the counter, even if there was an error |
| 195 | this.uploadsInProgress--; |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 196 | } |
| 197 | } |
| 198 | |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 199 | // Handle paste events for files (including images) |
| 200 | private _handlePaste = async (event: ClipboardEvent) => { |
| 201 | // Check if the clipboard contains files |
| 202 | if (event.clipboardData && event.clipboardData.files.length > 0) { |
| 203 | const file = event.clipboardData.files[0]; |
| 204 | |
| 205 | // Handle the file upload (for any file type, not just images) |
| 206 | event.preventDefault(); // Prevent default paste behavior |
| 207 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 208 | // Get the current cursor position |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 209 | const cursorPos = this.chatInput.selectionStart; |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 210 | await this._uploadFile(file, cursorPos); |
| 211 | } |
| 212 | }; |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 213 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 214 | // Handle drag events for file drop operation |
| 215 | private _handleDragOver(event: DragEvent) { |
| 216 | event.preventDefault(); // Necessary to allow dropping |
| 217 | event.stopPropagation(); |
| 218 | } |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 219 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 220 | private _handleDragEnter(event: DragEvent) { |
| 221 | event.preventDefault(); |
| 222 | event.stopPropagation(); |
| 223 | this.isDraggingOver = true; |
| 224 | } |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 225 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 226 | private _handleDragLeave(event: DragEvent) { |
| 227 | event.preventDefault(); |
| 228 | event.stopPropagation(); |
| 229 | // Only set to false if we're leaving the container (not entering a child element) |
| 230 | if (event.target === this.renderRoot.querySelector(".chat-container")) { |
| 231 | this.isDraggingOver = false; |
| 232 | } |
| 233 | } |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 234 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 235 | private _handleDrop = async (event: DragEvent) => { |
| 236 | event.preventDefault(); |
| 237 | event.stopPropagation(); |
| 238 | this.isDraggingOver = false; |
| 239 | |
| 240 | // Check if the dataTransfer contains files |
| 241 | if (event.dataTransfer && event.dataTransfer.files.length > 0) { |
| 242 | // Process all dropped files |
| 243 | for (let i = 0; i < event.dataTransfer.files.length; i++) { |
| 244 | const file = event.dataTransfer.files[i]; |
| 245 | try { |
| 246 | // For the first file, insert at the cursor position |
| 247 | // For subsequent files, append at the end of the content |
| 248 | const insertPosition = |
| 249 | i === 0 ? this.chatInput.selectionStart : this.content.length; |
| 250 | await this._uploadFile(file, insertPosition); |
| 251 | |
| 252 | // Add a space between multiple files |
| 253 | if (i < event.dataTransfer.files.length - 1) { |
| 254 | this.content += " "; |
| 255 | } |
| 256 | } catch (error) { |
| 257 | // Error already handled in _uploadFile |
| 258 | console.error("Failed to process dropped file:", error); |
| 259 | // Continue with the next file |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 260 | } |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 261 | } |
| 262 | } |
| 263 | }; |
| 264 | |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 265 | private _handleDiffComment(event: CustomEvent) { |
| 266 | const { comment } = event.detail; |
| 267 | if (!comment) return; |
| 268 | |
| 269 | if (this.content != "") { |
| 270 | this.content += "\n\n"; |
| 271 | } |
| 272 | this.content += comment; |
| 273 | requestAnimationFrame(() => this.adjustChatSpacing()); |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 274 | } |
| 275 | |
| Josh Bleecher Snyder | bd52faf | 2025-06-02 21:21:37 +0000 | [diff] [blame] | 276 | private _handleTodoComment(event: CustomEvent) { |
| 277 | const { comment } = event.detail; |
| 278 | if (!comment) return; |
| 279 | |
| 280 | if (this.content != "") { |
| 281 | this.content += "\n\n"; |
| 282 | } |
| 283 | this.content += comment; |
| 284 | requestAnimationFrame(() => this.adjustChatSpacing()); |
| 285 | } |
| 286 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 287 | // See https://lit.dev/docs/components/lifecycle/ |
| 288 | disconnectedCallback() { |
| 289 | super.disconnectedCallback(); |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 290 | window.removeEventListener("diff-comment", this._handleDiffComment); |
| Josh Bleecher Snyder | bd52faf | 2025-06-02 21:21:37 +0000 | [diff] [blame] | 291 | window.removeEventListener("todo-comment", this._handleTodoComment); |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 292 | |
| 293 | // Clean up drag and drop event listeners |
| 294 | const container = this.renderRoot.querySelector(".chat-container"); |
| 295 | if (container) { |
| 296 | container.removeEventListener("dragover", this._handleDragOver); |
| 297 | container.removeEventListener("dragenter", this._handleDragEnter); |
| 298 | container.removeEventListener("dragleave", this._handleDragLeave); |
| 299 | container.removeEventListener("drop", this._handleDrop); |
| 300 | } |
| 301 | |
| 302 | // Clean up paste event listener |
| 303 | if (this.chatInput) { |
| 304 | this.chatInput.removeEventListener("paste", this._handlePaste); |
| 305 | } |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 306 | } |
| 307 | |
| 308 | sendChatMessage() { |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 309 | // Prevent sending if there are uploads in progress |
| 310 | if (this.uploadsInProgress > 0) { |
| 311 | console.log( |
| 312 | `Message send prevented: ${this.uploadsInProgress} uploads in progress`, |
| 313 | ); |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 314 | |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 315 | // Show message to user |
| 316 | this.showUploadInProgressMessage = true; |
| 317 | |
| 318 | // Hide the message after 3 seconds |
| 319 | setTimeout(() => { |
| 320 | this.showUploadInProgressMessage = false; |
| 321 | }, 3000); |
| 322 | |
| 323 | return; |
| 324 | } |
| 325 | |
| 326 | // Only send if there's actual content (not just whitespace) |
| 327 | if (this.content.trim()) { |
| 328 | const event = new CustomEvent("send-chat", { |
| 329 | detail: { message: this.content }, |
| 330 | bubbles: true, |
| 331 | composed: true, |
| 332 | }); |
| 333 | this.dispatchEvent(event); |
| 334 | |
| 335 | // TODO(philip?): Ideally we only clear the content if the send is successful. |
| 336 | this.content = ""; // Clear content after sending |
| 337 | } |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 338 | } |
| 339 | |
| 340 | adjustChatSpacing() { |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 341 | if (!this.chatInput) return; |
| Sean McCullough | 5164eee | 2025-04-21 18:20:23 -0700 | [diff] [blame] | 342 | |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 343 | // Reset height to minimal value to correctly calculate scrollHeight |
| Sean McCullough | 5164eee | 2025-04-21 18:20:23 -0700 | [diff] [blame] | 344 | this.chatInput.style.height = "auto"; |
| 345 | |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 346 | // Get the scroll height (content height) |
| 347 | const scrollHeight = this.chatInput.scrollHeight; |
| Sean McCullough | 5164eee | 2025-04-21 18:20:23 -0700 | [diff] [blame] | 348 | |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 349 | // Set the height to match content (up to max-height which is handled by CSS) |
| 350 | this.chatInput.style.height = `${scrollHeight}px`; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 351 | } |
| 352 | |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 353 | async _sendChatClicked() { |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 354 | this.sendChatMessage(); |
| 355 | this.chatInput.focus(); // Refocus the input after sending |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 356 | // Reset height after sending a message |
| 357 | requestAnimationFrame(() => this.adjustChatSpacing()); |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 358 | } |
| 359 | |
| 360 | _chatInputKeyDown(event: KeyboardEvent) { |
| 361 | // Send message if Enter is pressed without Shift key |
| 362 | if (event.key === "Enter" && !event.shiftKey) { |
| 363 | event.preventDefault(); // Prevent default newline |
| 364 | this.sendChatMessage(); |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | _chatInputChanged(event) { |
| 369 | this.content = event.target.value; |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 370 | // Use requestAnimationFrame to ensure DOM updates have completed |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 371 | requestAnimationFrame(() => this.adjustChatSpacing()); |
| 372 | } |
| 373 | |
| 374 | @query("#chatInput") |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 375 | chatInput: HTMLTextAreaElement; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 376 | |
| 377 | protected firstUpdated(): void { |
| 378 | if (this.chatInput) { |
| 379 | this.chatInput.focus(); |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 380 | // Initialize the input height |
| 381 | this.adjustChatSpacing(); |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 382 | |
| 383 | // Add paste event listener for image handling |
| 384 | this.chatInput.addEventListener("paste", this._handlePaste); |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 385 | |
| 386 | // Add drag and drop event listeners |
| 387 | const container = this.renderRoot.querySelector(".chat-container"); |
| 388 | if (container) { |
| 389 | container.addEventListener("dragover", this._handleDragOver); |
| 390 | container.addEventListener("dragenter", this._handleDragEnter); |
| 391 | container.addEventListener("dragleave", this._handleDragLeave); |
| 392 | container.addEventListener("drop", this._handleDrop); |
| 393 | } |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 394 | } |
| Josh Bleecher Snyder | e2c7f72 | 2025-05-01 21:58:41 +0000 | [diff] [blame] | 395 | |
| 396 | // Add window.onload handler to ensure the input is focused when the page fully loads |
| 397 | window.addEventListener( |
| 398 | "load", |
| 399 | () => { |
| 400 | if (this.chatInput) { |
| 401 | this.chatInput.focus(); |
| 402 | } |
| 403 | }, |
| 404 | { once: true }, |
| 405 | ); |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 406 | } |
| 407 | |
| 408 | render() { |
| 409 | return html` |
| 410 | <div class="chat-container"> |
| 411 | <div class="chat-input-wrapper"> |
| 412 | <textarea |
| 413 | id="chatInput" |
| 414 | placeholder="Type your message here and press Enter to send..." |
| 415 | autofocus |
| 416 | @keydown="${this._chatInputKeyDown}" |
| 417 | @input="${this._chatInputChanged}" |
| 418 | .value=${this.content || ""} |
| 419 | ></textarea> |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 420 | <button |
| 421 | @click="${this._sendChatClicked}" |
| 422 | id="sendChatButton" |
| 423 | ?disabled=${this.uploadsInProgress > 0} |
| 424 | > |
| 425 | ${this.uploadsInProgress > 0 ? "Uploading..." : "Send"} |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 426 | </button> |
| 427 | </div> |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 428 | ${this.isDraggingOver |
| 429 | ? html` |
| 430 | <div class="drop-zone-overlay"> |
| 431 | <div class="drop-zone-message">Drop files here</div> |
| 432 | </div> |
| 433 | ` |
| 434 | : ""} |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 435 | ${this.showUploadInProgressMessage |
| 436 | ? html` |
| 437 | <div class="upload-progress-message"> |
| 438 | Please wait for file upload to complete before sending |
| 439 | </div> |
| 440 | ` |
| 441 | : ""} |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 442 | </div> |
| 443 | `; |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | declare global { |
| 448 | interface HTMLElementTagNameMap { |
| 449 | "sketch-chat-input": SketchChatInput; |
| 450 | } |
| 451 | } |