blob: 98382348c62cf99910a3bfde52a40cedfe3980a5 [file] [log] [blame]
import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { SketchTailwindElement } from "./sketch-tailwind-element";
@customElement("mobile-chat-input")
export class MobileChatInput extends SketchTailwindElement {
@property({ type: Boolean })
disabled = false;
@state()
private inputValue = "";
@state()
private uploadsInProgress = 0;
@state()
private showUploadProgress = false;
private textareaRef = createRef<HTMLTextAreaElement>();
private handleInput = (e: Event) => {
const target = e.target as HTMLTextAreaElement;
this.inputValue = target.value;
this.adjustTextareaHeight();
};
private handlePaste = async (e: ClipboardEvent) => {
// Check if the clipboard contains files
if (e.clipboardData && e.clipboardData.files.length > 0) {
const file = e.clipboardData.files[0];
// Handle the file upload
e.preventDefault(); // Prevent default paste behavior
// Get the current cursor position
const textarea = this.textareaRef.value;
const cursorPos = textarea
? textarea.selectionStart
: this.inputValue.length;
await this.uploadFile(file, cursorPos);
}
};
// Utility function to handle file uploads
private async uploadFile(file: File, insertPosition: number) {
// Insert a placeholder at the cursor position
const textBefore = this.inputValue.substring(0, insertPosition);
const textAfter = this.inputValue.substring(insertPosition);
// Add a loading indicator
const loadingText = `[🔄 Uploading ${file.name}...]`;
this.inputValue = `${textBefore}${loadingText}${textAfter}`;
// Increment uploads in progress counter
this.uploadsInProgress++;
this.showUploadProgress = true;
// Adjust spacing immediately to show loading indicator
this.adjustTextareaHeight();
try {
// Create a FormData object to send the file
const formData = new FormData();
formData.append("file", file);
// Upload the file to the server using a relative path
const response = await fetch("./upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const data = await response.json();
// Replace the loading placeholder with the actual file path
this.inputValue = this.inputValue.replace(loadingText, `[${data.path}]`);
return data.path;
} catch (error) {
console.error("Failed to upload file:", error);
// Replace loading indicator with error message
const errorText = `[Upload failed: ${error.message}]`;
this.inputValue = this.inputValue.replace(loadingText, errorText);
throw error;
} finally {
// Always decrement the counter, even if there was an error
this.uploadsInProgress--;
if (this.uploadsInProgress === 0) {
this.showUploadProgress = false;
}
this.adjustTextareaHeight();
if (this.textareaRef.value) {
this.textareaRef.value.focus();
}
}
}
private handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
};
private adjustTextareaHeight() {
if (this.textareaRef.value) {
const textarea = this.textareaRef.value;
textarea.style.height = "auto";
textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
}
}
private sendMessage = () => {
// Prevent sending if there are uploads in progress
if (this.uploadsInProgress > 0) {
console.log(
`Message send prevented: ${this.uploadsInProgress} uploads in progress`,
);
return;
}
const message = this.inputValue.trim();
if (message && !this.disabled) {
this.dispatchEvent(
new CustomEvent("send-message", {
detail: { message },
bubbles: true,
composed: true,
}),
);
this.inputValue = "";
if (this.textareaRef.value) {
this.textareaRef.value.value = "";
this.adjustTextareaHeight();
this.textareaRef.value.focus();
}
}
};
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
this.adjustTextareaHeight();
// Add paste event listener when component updates
if (this.textareaRef.value && !this.textareaRef.value.onpaste) {
this.textareaRef.value.addEventListener("paste", this.handlePaste);
}
}
disconnectedCallback() {
super.disconnectedCallback();
// Clean up paste event listener
if (this.textareaRef.value) {
this.textareaRef.value.removeEventListener("paste", this.handlePaste);
}
}
render() {
const canSend =
this.inputValue.trim().length > 0 &&
!this.disabled &&
this.uploadsInProgress === 0;
return html`
<div
class="block bg-white border-t border-gray-200 p-3 relative z-[1000]"
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));"
>
<div class="flex items-end gap-3 max-w-full">
<div class="flex-1 relative min-w-0">
<textarea
${ref(this.textareaRef)}
.value=${this.inputValue}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
placeholder="Message Sketch..."
?disabled=${this.disabled || this.uploadsInProgress > 0}
rows="1"
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"
style="font-size: 16px;"
></textarea>
${this.showUploadProgress
? html`
<div
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]"
>
Uploading ${this.uploadsInProgress}
file${this.uploadsInProgress > 1 ? "s" : ""}...
</div>
`
: ""}
</div>
<button
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"
@click=${this.sendMessage}
?disabled=${!canSend}
title=${this.uploadsInProgress > 0
? "Please wait for upload to complete"
: "Send message"}
>
${this.uploadsInProgress > 0
? html`<span class="text-xs">⏳</span>`
: html`<svg class="w-4 h-4 fill-current" viewBox="0 0 24 24">
<path d="M2,21L23,12L2,3V10L17,12L2,14V21Z" />
</svg>`}
</button>
</div>
</div>
`;
}
}