blob: 817190ed54e8f8639d35a4b9c7ba6ae8bba6a2d2 [file] [log] [blame]
Sean McCullough86b56862025-04-18 13:04:03 -07001import { css, html, LitElement, PropertyValues } from "lit";
2import { customElement, property, query } from "lit/decorators.js";
Sean McCullough86b56862025-04-18 13:04:03 -07003
4@customElement("sketch-chat-input")
5export class SketchChatInput extends LitElement {
6 @property()
7 content: string = "";
8
9 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
10 // Note that these styles only apply to the scope of this web component's
11 // shadow DOM node, so they won't leak out or collide with CSS declared in
12 // other components or the containing web page (...unless you want it to do that).
13 static styles = css`
14 /* Chat styles - exactly matching timeline.css */
15 .chat-container {
16 position: fixed;
17 bottom: 0;
18 left: 0;
19 width: 100%;
20 background: #f0f0f0;
21 padding: 15px;
22 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
23 z-index: 1000;
24 min-height: 40px; /* Ensure minimum height */
25 }
26
27 .chat-input-wrapper {
28 display: flex;
29 max-width: 1200px;
30 margin: 0 auto;
31 gap: 10px;
32 }
33
34 #chatInput {
35 flex: 1;
36 padding: 12px;
37 border: 1px solid #ddd;
38 border-radius: 4px;
Sean McCullough07b3e392025-04-21 22:51:14 +000039 resize: vertical;
Sean McCullough86b56862025-04-18 13:04:03 -070040 font-family: monospace;
41 font-size: 12px;
42 min-height: 40px;
Sean McCullough07b3e392025-04-21 22:51:14 +000043 max-height: 300px;
Sean McCullough86b56862025-04-18 13:04:03 -070044 background: #f7f7f7;
Sean McCullough5164eee2025-04-21 18:20:23 -070045 overflow-y: auto;
Sean McCullough07b3e392025-04-21 22:51:14 +000046 box-sizing: border-box; /* Ensure padding is included in height calculation */
47 line-height: 1.4; /* Consistent line height for better height calculation */
Sean McCullough86b56862025-04-18 13:04:03 -070048 }
49
50 #sendChatButton {
51 background-color: #2196f3;
52 color: white;
53 border: none;
54 border-radius: 4px;
55 padding: 0 20px;
56 cursor: pointer;
57 font-weight: 600;
Pokey Rule97188fc2025-04-23 15:50:04 +010058 align-self: center;
59 height: 40px;
Sean McCullough86b56862025-04-18 13:04:03 -070060 }
61
62 #sendChatButton:hover {
63 background-color: #0d8bf2;
64 }
65 `;
66
67 constructor() {
68 super();
69
70 // Binding methods
71 this._handleUpdateContent = this._handleUpdateContent.bind(this);
72 }
73
74 /**
75 * Handle update-content event
76 */
77 private _handleUpdateContent(event: CustomEvent) {
78 const { content } = event.detail;
79 if (content !== undefined) {
80 this.content = content;
81
82 // Update the textarea value directly, otherwise it won't update until next render
83 const textarea = this.shadowRoot?.querySelector(
Pokey Rulee2a8c2f2025-04-23 15:09:25 +010084 "#chatInput"
Sean McCullough86b56862025-04-18 13:04:03 -070085 ) as HTMLTextAreaElement;
86 if (textarea) {
87 textarea.value = content;
Sean McCullough07b3e392025-04-21 22:51:14 +000088 // Adjust height after content is updated programmatically
89 requestAnimationFrame(() => this.adjustChatSpacing());
Sean McCullough86b56862025-04-18 13:04:03 -070090 }
91 }
92 }
93
94 // See https://lit.dev/docs/components/lifecycle/
95 connectedCallback() {
96 super.connectedCallback();
97
98 // Listen for update-content events
99 this.addEventListener(
100 "update-content",
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100101 this._handleUpdateContent as EventListener
Sean McCullough86b56862025-04-18 13:04:03 -0700102 );
103 }
104
105 // See https://lit.dev/docs/components/lifecycle/
106 disconnectedCallback() {
107 super.disconnectedCallback();
108
109 // Remove event listeners
110 this.removeEventListener(
111 "update-content",
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100112 this._handleUpdateContent as EventListener
Sean McCullough86b56862025-04-18 13:04:03 -0700113 );
114 }
115
116 sendChatMessage() {
117 const event = new CustomEvent("send-chat", {
118 detail: { message: this.content },
119 bubbles: true,
120 composed: true,
121 });
122 this.dispatchEvent(event);
Philip Zeyligeraf2d7e32025-04-23 11:35:03 -0700123 // Clear the input after sending
124 this.content = "";
125 this.setAttribute("content", "");
Sean McCullough86b56862025-04-18 13:04:03 -0700126 }
127
128 adjustChatSpacing() {
Sean McCullough07b3e392025-04-21 22:51:14 +0000129 if (!this.chatInput) return;
Sean McCullough5164eee2025-04-21 18:20:23 -0700130
Sean McCullough07b3e392025-04-21 22:51:14 +0000131 // Reset height to minimal value to correctly calculate scrollHeight
Sean McCullough5164eee2025-04-21 18:20:23 -0700132 this.chatInput.style.height = "auto";
133
Sean McCullough07b3e392025-04-21 22:51:14 +0000134 // Get the scroll height (content height)
135 const scrollHeight = this.chatInput.scrollHeight;
Sean McCullough5164eee2025-04-21 18:20:23 -0700136
Sean McCullough07b3e392025-04-21 22:51:14 +0000137 // Set the height to match content (up to max-height which is handled by CSS)
138 this.chatInput.style.height = `${scrollHeight}px`;
Sean McCullough86b56862025-04-18 13:04:03 -0700139 }
140
141 _sendChatClicked() {
142 this.sendChatMessage();
143 this.chatInput.focus(); // Refocus the input after sending
Sean McCullough07b3e392025-04-21 22:51:14 +0000144 // Reset height after sending a message
145 requestAnimationFrame(() => this.adjustChatSpacing());
Sean McCullough86b56862025-04-18 13:04:03 -0700146 }
147
148 _chatInputKeyDown(event: KeyboardEvent) {
149 // Send message if Enter is pressed without Shift key
150 if (event.key === "Enter" && !event.shiftKey) {
151 event.preventDefault(); // Prevent default newline
152 this.sendChatMessage();
153 }
154 }
155
156 _chatInputChanged(event) {
157 this.content = event.target.value;
Sean McCullough07b3e392025-04-21 22:51:14 +0000158 // Use requestAnimationFrame to ensure DOM updates have completed
Sean McCullough86b56862025-04-18 13:04:03 -0700159 requestAnimationFrame(() => this.adjustChatSpacing());
160 }
161
162 @query("#chatInput")
Sean McCullough07b3e392025-04-21 22:51:14 +0000163 chatInput: HTMLTextAreaElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700164
165 protected firstUpdated(): void {
166 if (this.chatInput) {
167 this.chatInput.focus();
Sean McCullough07b3e392025-04-21 22:51:14 +0000168 // Initialize the input height
169 this.adjustChatSpacing();
Sean McCullough86b56862025-04-18 13:04:03 -0700170 }
171 }
172
173 render() {
174 return html`
175 <div class="chat-container">
176 <div class="chat-input-wrapper">
177 <textarea
178 id="chatInput"
179 placeholder="Type your message here and press Enter to send..."
180 autofocus
181 @keydown="${this._chatInputKeyDown}"
182 @input="${this._chatInputChanged}"
183 .value=${this.content || ""}
184 ></textarea>
185 <button @click="${this._sendChatClicked}" id="sendChatButton">
186 Send
187 </button>
188 </div>
189 </div>
190 `;
191 }
192}
193
194declare global {
195 interface HTMLElementTagNameMap {
196 "sketch-chat-input": SketchChatInput;
197 }
198}