blob: c181724a700551e30a8ed52d3075dded1756b2ca [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;
58 }
59
60 #sendChatButton:hover {
61 background-color: #0d8bf2;
62 }
63 `;
64
65 constructor() {
66 super();
67
68 // Binding methods
69 this._handleUpdateContent = this._handleUpdateContent.bind(this);
70 }
71
72 /**
73 * Handle update-content event
74 */
75 private _handleUpdateContent(event: CustomEvent) {
76 const { content } = event.detail;
77 if (content !== undefined) {
78 this.content = content;
79
80 // Update the textarea value directly, otherwise it won't update until next render
81 const textarea = this.shadowRoot?.querySelector(
Sean McCullough71941bd2025-04-18 13:31:48 -070082 "#chatInput",
Sean McCullough86b56862025-04-18 13:04:03 -070083 ) as HTMLTextAreaElement;
84 if (textarea) {
85 textarea.value = content;
Sean McCullough07b3e392025-04-21 22:51:14 +000086 // Adjust height after content is updated programmatically
87 requestAnimationFrame(() => this.adjustChatSpacing());
Sean McCullough86b56862025-04-18 13:04:03 -070088 }
89 }
90 }
91
92 // See https://lit.dev/docs/components/lifecycle/
93 connectedCallback() {
94 super.connectedCallback();
95
96 // Listen for update-content events
97 this.addEventListener(
98 "update-content",
Sean McCullough71941bd2025-04-18 13:31:48 -070099 this._handleUpdateContent as EventListener,
Sean McCullough86b56862025-04-18 13:04:03 -0700100 );
101 }
102
103 // See https://lit.dev/docs/components/lifecycle/
104 disconnectedCallback() {
105 super.disconnectedCallback();
106
107 // Remove event listeners
108 this.removeEventListener(
109 "update-content",
Sean McCullough71941bd2025-04-18 13:31:48 -0700110 this._handleUpdateContent as EventListener,
Sean McCullough86b56862025-04-18 13:04:03 -0700111 );
112 }
113
114 sendChatMessage() {
115 const event = new CustomEvent("send-chat", {
116 detail: { message: this.content },
117 bubbles: true,
118 composed: true,
119 });
120 this.dispatchEvent(event);
121 this.content = ""; // Clear the input after sending
122 }
123
124 adjustChatSpacing() {
Sean McCullough07b3e392025-04-21 22:51:14 +0000125 if (!this.chatInput) return;
Sean McCullough5164eee2025-04-21 18:20:23 -0700126
Sean McCullough07b3e392025-04-21 22:51:14 +0000127 // Reset height to minimal value to correctly calculate scrollHeight
Sean McCullough5164eee2025-04-21 18:20:23 -0700128 this.chatInput.style.height = "auto";
129
Sean McCullough07b3e392025-04-21 22:51:14 +0000130 // Get the scroll height (content height)
131 const scrollHeight = this.chatInput.scrollHeight;
Sean McCullough5164eee2025-04-21 18:20:23 -0700132
Sean McCullough07b3e392025-04-21 22:51:14 +0000133 // Set the height to match content (up to max-height which is handled by CSS)
134 this.chatInput.style.height = `${scrollHeight}px`;
Sean McCullough86b56862025-04-18 13:04:03 -0700135 }
136
137 _sendChatClicked() {
138 this.sendChatMessage();
139 this.chatInput.focus(); // Refocus the input after sending
Sean McCullough07b3e392025-04-21 22:51:14 +0000140 // Reset height after sending a message
141 requestAnimationFrame(() => this.adjustChatSpacing());
Sean McCullough86b56862025-04-18 13:04:03 -0700142 }
143
144 _chatInputKeyDown(event: KeyboardEvent) {
145 // Send message if Enter is pressed without Shift key
146 if (event.key === "Enter" && !event.shiftKey) {
147 event.preventDefault(); // Prevent default newline
148 this.sendChatMessage();
149 }
150 }
151
152 _chatInputChanged(event) {
153 this.content = event.target.value;
Sean McCullough07b3e392025-04-21 22:51:14 +0000154 // Use requestAnimationFrame to ensure DOM updates have completed
Sean McCullough86b56862025-04-18 13:04:03 -0700155 requestAnimationFrame(() => this.adjustChatSpacing());
156 }
157
158 @query("#chatInput")
Sean McCullough07b3e392025-04-21 22:51:14 +0000159 chatInput: HTMLTextAreaElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700160
161 protected firstUpdated(): void {
162 if (this.chatInput) {
163 this.chatInput.focus();
Sean McCullough07b3e392025-04-21 22:51:14 +0000164 // Initialize the input height
165 this.adjustChatSpacing();
Sean McCullough86b56862025-04-18 13:04:03 -0700166 }
167 }
168
169 render() {
170 return html`
171 <div class="chat-container">
172 <div class="chat-input-wrapper">
173 <textarea
174 id="chatInput"
175 placeholder="Type your message here and press Enter to send..."
176 autofocus
177 @keydown="${this._chatInputKeyDown}"
178 @input="${this._chatInputChanged}"
179 .value=${this.content || ""}
180 ></textarea>
181 <button @click="${this._sendChatClicked}" id="sendChatButton">
182 Send
183 </button>
184 </div>
185 </div>
186 `;
187 }
188}
189
190declare global {
191 interface HTMLElementTagNameMap {
192 "sketch-chat-input": SketchChatInput;
193 }
194}