| 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); |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 129 | this._handleDragOver = this._handleDragOver.bind(this); |
| 130 | this._handleDragEnter = this._handleDragEnter.bind(this); |
| 131 | this._handleDragLeave = this._handleDragLeave.bind(this); |
| 132 | this._handleDrop = this._handleDrop.bind(this); |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 133 | } |
| 134 | |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 135 | connectedCallback() { |
| 136 | super.connectedCallback(); |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 137 | window.addEventListener("diff-comment", this._handleDiffComment); |
| 138 | } |
| 139 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 140 | // Utility function to handle file uploads (used by both paste and drop handlers) |
| 141 | private async _uploadFile(file: File, insertPosition: number) { |
| 142 | // Insert a placeholder at the cursor position |
| 143 | const textBefore = this.content.substring(0, insertPosition); |
| 144 | const textAfter = this.content.substring(insertPosition); |
| 145 | |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 146 | // Add a loading indicator with a visual cue |
| 147 | const loadingText = `[🔄 Uploading ${file.name}...]`; |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 148 | this.content = `${textBefore}${loadingText}${textAfter}`; |
| 149 | |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 150 | // Increment uploads in progress counter |
| 151 | this.uploadsInProgress++; |
| 152 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 153 | // Adjust spacing immediately to show loading indicator |
| 154 | requestAnimationFrame(() => this.adjustChatSpacing()); |
| 155 | |
| 156 | try { |
| 157 | // Create a FormData object to send the file |
| 158 | const formData = new FormData(); |
| 159 | formData.append("file", file); |
| 160 | |
| 161 | // Upload the file to the server using a relative path |
| 162 | const response = await fetch("./upload", { |
| 163 | method: "POST", |
| 164 | body: formData, |
| 165 | }); |
| 166 | |
| 167 | if (!response.ok) { |
| 168 | throw new Error(`Upload failed: ${response.statusText}`); |
| 169 | } |
| 170 | |
| 171 | const data = await response.json(); |
| 172 | |
| 173 | // Replace the loading placeholder with the actual file path |
| 174 | this.content = this.content.replace(loadingText, `[${data.path}]`); |
| 175 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 176 | return data.path; |
| 177 | } catch (error) { |
| 178 | console.error("Failed to upload file:", error); |
| 179 | |
| 180 | // Replace loading indicator with error message |
| 181 | const errorText = `[Upload failed: ${error.message}]`; |
| 182 | this.content = this.content.replace(loadingText, errorText); |
| 183 | |
| 184 | // Adjust spacing to show error message |
| 185 | requestAnimationFrame(() => { |
| 186 | this.adjustChatSpacing(); |
| 187 | this.chatInput.focus(); |
| 188 | }); |
| 189 | |
| 190 | throw error; |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 191 | } finally { |
| 192 | // Always decrement the counter, even if there was an error |
| 193 | this.uploadsInProgress--; |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 194 | } |
| 195 | } |
| 196 | |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 197 | // Handle paste events for files (including images) |
| 198 | private _handlePaste = async (event: ClipboardEvent) => { |
| 199 | // Check if the clipboard contains files |
| 200 | if (event.clipboardData && event.clipboardData.files.length > 0) { |
| 201 | const file = event.clipboardData.files[0]; |
| 202 | |
| 203 | // Handle the file upload (for any file type, not just images) |
| 204 | event.preventDefault(); // Prevent default paste behavior |
| 205 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 206 | // Get the current cursor position |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 207 | const cursorPos = this.chatInput.selectionStart; |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 208 | await this._uploadFile(file, cursorPos); |
| 209 | } |
| 210 | }; |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 211 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 212 | // Handle drag events for file drop operation |
| 213 | private _handleDragOver(event: DragEvent) { |
| 214 | event.preventDefault(); // Necessary to allow dropping |
| 215 | event.stopPropagation(); |
| 216 | } |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 217 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 218 | private _handleDragEnter(event: DragEvent) { |
| 219 | event.preventDefault(); |
| 220 | event.stopPropagation(); |
| 221 | this.isDraggingOver = true; |
| 222 | } |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 223 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 224 | private _handleDragLeave(event: DragEvent) { |
| 225 | event.preventDefault(); |
| 226 | event.stopPropagation(); |
| 227 | // Only set to false if we're leaving the container (not entering a child element) |
| 228 | if (event.target === this.renderRoot.querySelector(".chat-container")) { |
| 229 | this.isDraggingOver = false; |
| 230 | } |
| 231 | } |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 232 | |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 233 | private _handleDrop = async (event: DragEvent) => { |
| 234 | event.preventDefault(); |
| 235 | event.stopPropagation(); |
| 236 | this.isDraggingOver = false; |
| 237 | |
| 238 | // Check if the dataTransfer contains files |
| 239 | if (event.dataTransfer && event.dataTransfer.files.length > 0) { |
| 240 | // Process all dropped files |
| 241 | for (let i = 0; i < event.dataTransfer.files.length; i++) { |
| 242 | const file = event.dataTransfer.files[i]; |
| 243 | try { |
| 244 | // For the first file, insert at the cursor position |
| 245 | // For subsequent files, append at the end of the content |
| 246 | const insertPosition = |
| 247 | i === 0 ? this.chatInput.selectionStart : this.content.length; |
| 248 | await this._uploadFile(file, insertPosition); |
| 249 | |
| 250 | // Add a space between multiple files |
| 251 | if (i < event.dataTransfer.files.length - 1) { |
| 252 | this.content += " "; |
| 253 | } |
| 254 | } catch (error) { |
| 255 | // Error already handled in _uploadFile |
| 256 | console.error("Failed to process dropped file:", error); |
| 257 | // Continue with the next file |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 258 | } |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 259 | } |
| 260 | } |
| 261 | }; |
| 262 | |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 263 | private _handleDiffComment(event: CustomEvent) { |
| 264 | const { comment } = event.detail; |
| 265 | if (!comment) return; |
| 266 | |
| 267 | if (this.content != "") { |
| 268 | this.content += "\n\n"; |
| 269 | } |
| 270 | this.content += comment; |
| 271 | requestAnimationFrame(() => this.adjustChatSpacing()); |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 272 | } |
| 273 | |
| 274 | // See https://lit.dev/docs/components/lifecycle/ |
| 275 | disconnectedCallback() { |
| 276 | super.disconnectedCallback(); |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 277 | window.removeEventListener("diff-comment", this._handleDiffComment); |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 278 | |
| 279 | // Clean up drag and drop event listeners |
| 280 | const container = this.renderRoot.querySelector(".chat-container"); |
| 281 | if (container) { |
| 282 | container.removeEventListener("dragover", this._handleDragOver); |
| 283 | container.removeEventListener("dragenter", this._handleDragEnter); |
| 284 | container.removeEventListener("dragleave", this._handleDragLeave); |
| 285 | container.removeEventListener("drop", this._handleDrop); |
| 286 | } |
| 287 | |
| 288 | // Clean up paste event listener |
| 289 | if (this.chatInput) { |
| 290 | this.chatInput.removeEventListener("paste", this._handlePaste); |
| 291 | } |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 292 | } |
| 293 | |
| 294 | sendChatMessage() { |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 295 | // Prevent sending if there are uploads in progress |
| 296 | if (this.uploadsInProgress > 0) { |
| 297 | console.log( |
| 298 | `Message send prevented: ${this.uploadsInProgress} uploads in progress`, |
| 299 | ); |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 300 | |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 301 | // Show message to user |
| 302 | this.showUploadInProgressMessage = true; |
| 303 | |
| 304 | // Hide the message after 3 seconds |
| 305 | setTimeout(() => { |
| 306 | this.showUploadInProgressMessage = false; |
| 307 | }, 3000); |
| 308 | |
| 309 | return; |
| 310 | } |
| 311 | |
| 312 | // Only send if there's actual content (not just whitespace) |
| 313 | if (this.content.trim()) { |
| 314 | const event = new CustomEvent("send-chat", { |
| 315 | detail: { message: this.content }, |
| 316 | bubbles: true, |
| 317 | composed: true, |
| 318 | }); |
| 319 | this.dispatchEvent(event); |
| 320 | |
| 321 | // TODO(philip?): Ideally we only clear the content if the send is successful. |
| 322 | this.content = ""; // Clear content after sending |
| 323 | } |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 324 | } |
| 325 | |
| 326 | adjustChatSpacing() { |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 327 | if (!this.chatInput) return; |
| Sean McCullough | 5164eee | 2025-04-21 18:20:23 -0700 | [diff] [blame] | 328 | |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 329 | // Reset height to minimal value to correctly calculate scrollHeight |
| Sean McCullough | 5164eee | 2025-04-21 18:20:23 -0700 | [diff] [blame] | 330 | this.chatInput.style.height = "auto"; |
| 331 | |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 332 | // Get the scroll height (content height) |
| 333 | const scrollHeight = this.chatInput.scrollHeight; |
| Sean McCullough | 5164eee | 2025-04-21 18:20:23 -0700 | [diff] [blame] | 334 | |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 335 | // Set the height to match content (up to max-height which is handled by CSS) |
| 336 | this.chatInput.style.height = `${scrollHeight}px`; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 337 | } |
| 338 | |
| Philip Zeyliger | 73db605 | 2025-04-23 13:09:07 -0700 | [diff] [blame] | 339 | async _sendChatClicked() { |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 340 | this.sendChatMessage(); |
| 341 | this.chatInput.focus(); // Refocus the input after sending |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 342 | // Reset height after sending a message |
| 343 | requestAnimationFrame(() => this.adjustChatSpacing()); |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 344 | } |
| 345 | |
| 346 | _chatInputKeyDown(event: KeyboardEvent) { |
| 347 | // Send message if Enter is pressed without Shift key |
| 348 | if (event.key === "Enter" && !event.shiftKey) { |
| 349 | event.preventDefault(); // Prevent default newline |
| 350 | this.sendChatMessage(); |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | _chatInputChanged(event) { |
| 355 | this.content = event.target.value; |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 356 | // Use requestAnimationFrame to ensure DOM updates have completed |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 357 | requestAnimationFrame(() => this.adjustChatSpacing()); |
| 358 | } |
| 359 | |
| 360 | @query("#chatInput") |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 361 | chatInput: HTMLTextAreaElement; |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 362 | |
| 363 | protected firstUpdated(): void { |
| 364 | if (this.chatInput) { |
| 365 | this.chatInput.focus(); |
| Sean McCullough | 07b3e39 | 2025-04-21 22:51:14 +0000 | [diff] [blame] | 366 | // Initialize the input height |
| 367 | this.adjustChatSpacing(); |
| Philip Zeyliger | f84e88c | 2025-05-14 23:19:01 +0000 | [diff] [blame] | 368 | |
| 369 | // Add paste event listener for image handling |
| 370 | this.chatInput.addEventListener("paste", this._handlePaste); |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 371 | |
| 372 | // Add drag and drop event listeners |
| 373 | const container = this.renderRoot.querySelector(".chat-container"); |
| 374 | if (container) { |
| 375 | container.addEventListener("dragover", this._handleDragOver); |
| 376 | container.addEventListener("dragenter", this._handleDragEnter); |
| 377 | container.addEventListener("dragleave", this._handleDragLeave); |
| 378 | container.addEventListener("drop", this._handleDrop); |
| 379 | } |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 380 | } |
| Josh Bleecher Snyder | e2c7f72 | 2025-05-01 21:58:41 +0000 | [diff] [blame] | 381 | |
| 382 | // Add window.onload handler to ensure the input is focused when the page fully loads |
| 383 | window.addEventListener( |
| 384 | "load", |
| 385 | () => { |
| 386 | if (this.chatInput) { |
| 387 | this.chatInput.focus(); |
| 388 | } |
| 389 | }, |
| 390 | { once: true }, |
| 391 | ); |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 392 | } |
| 393 | |
| 394 | render() { |
| 395 | return html` |
| 396 | <div class="chat-container"> |
| 397 | <div class="chat-input-wrapper"> |
| 398 | <textarea |
| 399 | id="chatInput" |
| 400 | placeholder="Type your message here and press Enter to send..." |
| 401 | autofocus |
| 402 | @keydown="${this._chatInputKeyDown}" |
| 403 | @input="${this._chatInputChanged}" |
| 404 | .value=${this.content || ""} |
| 405 | ></textarea> |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 406 | <button |
| 407 | @click="${this._sendChatClicked}" |
| 408 | id="sendChatButton" |
| 409 | ?disabled=${this.uploadsInProgress > 0} |
| 410 | > |
| 411 | ${this.uploadsInProgress > 0 ? "Uploading..." : "Send"} |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 412 | </button> |
| 413 | </div> |
| Pokey Rule | 339b56e | 2025-05-15 14:48:07 +0000 | [diff] [blame] | 414 | ${this.isDraggingOver |
| 415 | ? html` |
| 416 | <div class="drop-zone-overlay"> |
| 417 | <div class="drop-zone-message">Drop files here</div> |
| 418 | </div> |
| 419 | ` |
| 420 | : ""} |
| Pokey Rule | 044a62e | 2025-05-16 10:40:59 +0000 | [diff] [blame] | 421 | ${this.showUploadInProgressMessage |
| 422 | ? html` |
| 423 | <div class="upload-progress-message"> |
| 424 | Please wait for file upload to complete before sending |
| 425 | </div> |
| 426 | ` |
| 427 | : ""} |
| Sean McCullough | 86b5686 | 2025-04-18 13:04:03 -0700 | [diff] [blame] | 428 | </div> |
| 429 | `; |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | declare global { |
| 434 | interface HTMLElementTagNameMap { |
| 435 | "sketch-chat-input": SketchChatInput; |
| 436 | } |
| 437 | } |