blob: 98382348c62cf99910a3bfde52a40cedfe3980a5 [file] [log] [blame]
banksean23a35b82025-07-20 21:18:31 +00001import { html } from "lit";
Philip Zeyligere08c7ff2025-06-06 13:22:12 -07002import { customElement, property, state } from "lit/decorators.js";
3import { createRef, ref } from "lit/directives/ref.js";
banksean23a35b82025-07-20 21:18:31 +00004import { SketchTailwindElement } from "./sketch-tailwind-element";
Philip Zeyligere08c7ff2025-06-06 13:22:12 -07005
6@customElement("mobile-chat-input")
banksean23a35b82025-07-20 21:18:31 +00007export class MobileChatInput extends SketchTailwindElement {
Philip Zeyligere08c7ff2025-06-06 13:22:12 -07008 @property({ type: Boolean })
9 disabled = false;
10
11 @state()
12 private inputValue = "";
13
Philip Zeyliger73757082025-06-07 00:06:27 +000014 @state()
15 private uploadsInProgress = 0;
16
17 @state()
18 private showUploadProgress = false;
19
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070020 private textareaRef = createRef<HTMLTextAreaElement>();
21
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070022 private handleInput = (e: Event) => {
23 const target = e.target as HTMLTextAreaElement;
24 this.inputValue = target.value;
25 this.adjustTextareaHeight();
26 };
27
Philip Zeyliger73757082025-06-07 00:06:27 +000028 private handlePaste = async (e: ClipboardEvent) => {
29 // Check if the clipboard contains files
30 if (e.clipboardData && e.clipboardData.files.length > 0) {
31 const file = e.clipboardData.files[0];
Autoformatterf825e692025-06-07 04:19:43 +000032
Philip Zeyliger73757082025-06-07 00:06:27 +000033 // Handle the file upload
34 e.preventDefault(); // Prevent default paste behavior
Autoformatterf825e692025-06-07 04:19:43 +000035
Philip Zeyliger73757082025-06-07 00:06:27 +000036 // Get the current cursor position
37 const textarea = this.textareaRef.value;
Autoformatterf825e692025-06-07 04:19:43 +000038 const cursorPos = textarea
39 ? textarea.selectionStart
40 : this.inputValue.length;
Philip Zeyliger73757082025-06-07 00:06:27 +000041 await this.uploadFile(file, cursorPos);
42 }
43 };
44
45 // Utility function to handle file uploads
46 private async uploadFile(file: File, insertPosition: number) {
47 // Insert a placeholder at the cursor position
48 const textBefore = this.inputValue.substring(0, insertPosition);
49 const textAfter = this.inputValue.substring(insertPosition);
50
51 // Add a loading indicator
52 const loadingText = `[🔄 Uploading ${file.name}...]`;
53 this.inputValue = `${textBefore}${loadingText}${textAfter}`;
54
55 // Increment uploads in progress counter
56 this.uploadsInProgress++;
57 this.showUploadProgress = true;
58
59 // Adjust spacing immediately to show loading indicator
60 this.adjustTextareaHeight();
61
62 try {
63 // Create a FormData object to send the file
64 const formData = new FormData();
65 formData.append("file", file);
66
67 // Upload the file to the server using a relative path
68 const response = await fetch("./upload", {
69 method: "POST",
70 body: formData,
71 });
72
73 if (!response.ok) {
74 throw new Error(`Upload failed: ${response.statusText}`);
75 }
76
77 const data = await response.json();
78
79 // Replace the loading placeholder with the actual file path
80 this.inputValue = this.inputValue.replace(loadingText, `[${data.path}]`);
81
82 return data.path;
83 } catch (error) {
84 console.error("Failed to upload file:", error);
85
86 // Replace loading indicator with error message
87 const errorText = `[Upload failed: ${error.message}]`;
88 this.inputValue = this.inputValue.replace(loadingText, errorText);
89
90 throw error;
91 } finally {
92 // Always decrement the counter, even if there was an error
93 this.uploadsInProgress--;
94 if (this.uploadsInProgress === 0) {
95 this.showUploadProgress = false;
96 }
97 this.adjustTextareaHeight();
98 if (this.textareaRef.value) {
99 this.textareaRef.value.focus();
100 }
101 }
102 }
103
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700104 private handleKeyDown = (e: KeyboardEvent) => {
105 if (e.key === "Enter" && !e.shiftKey) {
106 e.preventDefault();
107 this.sendMessage();
108 }
109 };
110
111 private adjustTextareaHeight() {
112 if (this.textareaRef.value) {
113 const textarea = this.textareaRef.value;
114 textarea.style.height = "auto";
115 textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
116 }
117 }
118
119 private sendMessage = () => {
Philip Zeyliger73757082025-06-07 00:06:27 +0000120 // Prevent sending if there are uploads in progress
121 if (this.uploadsInProgress > 0) {
Autoformatterf825e692025-06-07 04:19:43 +0000122 console.log(
123 `Message send prevented: ${this.uploadsInProgress} uploads in progress`,
124 );
Philip Zeyliger73757082025-06-07 00:06:27 +0000125 return;
126 }
127
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700128 const message = this.inputValue.trim();
129 if (message && !this.disabled) {
130 this.dispatchEvent(
131 new CustomEvent("send-message", {
132 detail: { message },
133 bubbles: true,
134 composed: true,
135 }),
136 );
137
138 this.inputValue = "";
139 if (this.textareaRef.value) {
140 this.textareaRef.value.value = "";
141 this.adjustTextareaHeight();
142 this.textareaRef.value.focus();
143 }
144 }
145 };
146
147 updated(changedProperties: Map<string, any>) {
148 super.updated(changedProperties);
149 this.adjustTextareaHeight();
Autoformatterf825e692025-06-07 04:19:43 +0000150
Philip Zeyliger73757082025-06-07 00:06:27 +0000151 // Add paste event listener when component updates
152 if (this.textareaRef.value && !this.textareaRef.value.onpaste) {
Autoformatterf825e692025-06-07 04:19:43 +0000153 this.textareaRef.value.addEventListener("paste", this.handlePaste);
Philip Zeyliger73757082025-06-07 00:06:27 +0000154 }
155 }
156
157 disconnectedCallback() {
158 super.disconnectedCallback();
159 // Clean up paste event listener
160 if (this.textareaRef.value) {
Autoformatterf825e692025-06-07 04:19:43 +0000161 this.textareaRef.value.removeEventListener("paste", this.handlePaste);
Philip Zeyliger73757082025-06-07 00:06:27 +0000162 }
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700163 }
164
165 render() {
Autoformatterf825e692025-06-07 04:19:43 +0000166 const canSend =
167 this.inputValue.trim().length > 0 &&
168 !this.disabled &&
169 this.uploadsInProgress === 0;
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700170
171 return html`
banksean23a35b82025-07-20 21:18:31 +0000172 <div
173 class="block bg-white border-t border-gray-200 p-3 relative z-[1000]"
174 style="padding-bottom: max(12px, env(safe-area-inset-bottom)); padding-left: max(16px, env(safe-area-inset-left)); padding-right: max(16px, env(safe-area-inset-right));"
175 >
176 <div class="flex items-end gap-3 max-w-full">
177 <div class="flex-1 relative min-w-0">
178 <textarea
179 ${ref(this.textareaRef)}
180 .value=${this.inputValue}
181 @input=${this.handleInput}
182 @keydown=${this.handleKeyDown}
183 placeholder="Message Sketch..."
184 ?disabled=${this.disabled || this.uploadsInProgress > 0}
185 rows="1"
186 class="w-full min-h-[40px] max-h-[120px] p-3 border border-gray-300 rounded-[20px] text-base font-inherit leading-relaxed resize-none outline-none bg-gray-50 transition-colors duration-200 box-border focus:border-blue-500 focus:bg-white disabled:bg-gray-200 disabled:text-gray-500 disabled:cursor-not-allowed placeholder:text-gray-500"
187 style="font-size: 16px;"
188 ></textarea>
Autoformatterf825e692025-06-07 04:19:43 +0000189
banksean23a35b82025-07-20 21:18:31 +0000190 ${this.showUploadProgress
191 ? html`
192 <div
193 class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-yellow-50 text-yellow-800 px-2 py-1 rounded text-xs whitespace-nowrap shadow-sm z-[1000]"
194 >
195 Uploading ${this.uploadsInProgress}
196 file${this.uploadsInProgress > 1 ? "s" : ""}...
197 </div>
198 `
199 : ""}
200 </div>
201
202 <button
203 class="flex-shrink-0 w-10 h-10 border-none rounded-full bg-blue-500 text-white cursor-pointer flex items-center justify-center text-lg transition-all duration-200 outline-none hover:bg-blue-600 active:scale-95 disabled:bg-gray-500 disabled:cursor-not-allowed disabled:opacity-60"
204 @click=${this.sendMessage}
205 ?disabled=${!canSend}
206 title=${this.uploadsInProgress > 0
207 ? "Please wait for upload to complete"
208 : "Send message"}
209 >
210 ${this.uploadsInProgress > 0
211 ? html`<span class="text-xs">⏳</span>`
212 : html`<svg class="w-4 h-4 fill-current" viewBox="0 0 24 24">
213 <path d="M2,21L23,12L2,3V10L17,12L2,14V21Z" />
214 </svg>`}
215 </button>
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700216 </div>
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700217 </div>
218 `;
219 }
220}