blob: fb273bc6550d37b35a2696824f976392e0778745 [file] [log] [blame]
Sean McCullough86b56862025-04-18 13:04:03 -07001import { css, html, LitElement, PropertyValues } from "lit";
Philip Zeyliger73db6052025-04-23 13:09:07 -07002import { customElement, property, state, query } from "lit/decorators.js";
Sean McCullough86b56862025-04-18 13:04:03 -07003
4@customElement("sketch-chat-input")
5export class SketchChatInput extends LitElement {
Philip Zeyliger73db6052025-04-23 13:09:07 -07006 @state()
Sean McCullough86b56862025-04-18 13:04:03 -07007 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 {
Sean McCullough86b56862025-04-18 13:04:03 -070016 width: 100%;
17 background: #f0f0f0;
18 padding: 15px;
Sean McCullough86b56862025-04-18 13:04:03 -070019 min-height: 40px; /* Ensure minimum height */
20 }
21
22 .chat-input-wrapper {
23 display: flex;
24 max-width: 1200px;
25 margin: 0 auto;
26 gap: 10px;
27 }
28
29 #chatInput {
30 flex: 1;
31 padding: 12px;
32 border: 1px solid #ddd;
33 border-radius: 4px;
Sean McCullough07b3e392025-04-21 22:51:14 +000034 resize: vertical;
Sean McCullough86b56862025-04-18 13:04:03 -070035 font-family: monospace;
36 font-size: 12px;
37 min-height: 40px;
Sean McCullough07b3e392025-04-21 22:51:14 +000038 max-height: 300px;
Sean McCullough86b56862025-04-18 13:04:03 -070039 background: #f7f7f7;
Sean McCullough5164eee2025-04-21 18:20:23 -070040 overflow-y: auto;
Sean McCullough07b3e392025-04-21 22:51:14 +000041 box-sizing: border-box; /* Ensure padding is included in height calculation */
42 line-height: 1.4; /* Consistent line height for better height calculation */
Sean McCullough86b56862025-04-18 13:04:03 -070043 }
44
45 #sendChatButton {
46 background-color: #2196f3;
47 color: white;
48 border: none;
49 border-radius: 4px;
50 padding: 0 20px;
51 cursor: pointer;
52 font-weight: 600;
Pokey Rule97188fc2025-04-23 15:50:04 +010053 align-self: center;
54 height: 40px;
Sean McCullough86b56862025-04-18 13:04:03 -070055 }
56
57 #sendChatButton:hover {
58 background-color: #0d8bf2;
59 }
60 `;
61
62 constructor() {
63 super();
Philip Zeyliger73db6052025-04-23 13:09:07 -070064 this._handleDiffComment = this._handleDiffComment.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -070065 }
66
Sean McCullough86b56862025-04-18 13:04:03 -070067 connectedCallback() {
68 super.connectedCallback();
Philip Zeyliger73db6052025-04-23 13:09:07 -070069 window.addEventListener("diff-comment", this._handleDiffComment);
70 }
71
72 private _handleDiffComment(event: CustomEvent) {
73 const { comment } = event.detail;
74 if (!comment) return;
75
76 if (this.content != "") {
77 this.content += "\n\n";
78 }
79 this.content += comment;
80 requestAnimationFrame(() => this.adjustChatSpacing());
Sean McCullough86b56862025-04-18 13:04:03 -070081 }
82
83 // See https://lit.dev/docs/components/lifecycle/
84 disconnectedCallback() {
85 super.disconnectedCallback();
Philip Zeyliger73db6052025-04-23 13:09:07 -070086 window.removeEventListener("diff-comment", this._handleDiffComment);
Sean McCullough86b56862025-04-18 13:04:03 -070087 }
88
89 sendChatMessage() {
90 const event = new CustomEvent("send-chat", {
91 detail: { message: this.content },
92 bubbles: true,
93 composed: true,
94 });
95 this.dispatchEvent(event);
Philip Zeyliger73db6052025-04-23 13:09:07 -070096
97 // TODO(philip?): Ideally we only clear the content if the send is successful.
98 this.content = ""; // Clear content after sending
Sean McCullough86b56862025-04-18 13:04:03 -070099 }
100
101 adjustChatSpacing() {
Sean McCullough07b3e392025-04-21 22:51:14 +0000102 if (!this.chatInput) return;
Sean McCullough5164eee2025-04-21 18:20:23 -0700103
Sean McCullough07b3e392025-04-21 22:51:14 +0000104 // Reset height to minimal value to correctly calculate scrollHeight
Sean McCullough5164eee2025-04-21 18:20:23 -0700105 this.chatInput.style.height = "auto";
106
Sean McCullough07b3e392025-04-21 22:51:14 +0000107 // Get the scroll height (content height)
108 const scrollHeight = this.chatInput.scrollHeight;
Sean McCullough5164eee2025-04-21 18:20:23 -0700109
Sean McCullough07b3e392025-04-21 22:51:14 +0000110 // Set the height to match content (up to max-height which is handled by CSS)
111 this.chatInput.style.height = `${scrollHeight}px`;
Sean McCullough86b56862025-04-18 13:04:03 -0700112 }
113
Philip Zeyliger73db6052025-04-23 13:09:07 -0700114 async _sendChatClicked() {
Sean McCullough86b56862025-04-18 13:04:03 -0700115 this.sendChatMessage();
116 this.chatInput.focus(); // Refocus the input after sending
Sean McCullough07b3e392025-04-21 22:51:14 +0000117 // Reset height after sending a message
118 requestAnimationFrame(() => this.adjustChatSpacing());
Sean McCullough86b56862025-04-18 13:04:03 -0700119 }
120
121 _chatInputKeyDown(event: KeyboardEvent) {
122 // Send message if Enter is pressed without Shift key
123 if (event.key === "Enter" && !event.shiftKey) {
124 event.preventDefault(); // Prevent default newline
125 this.sendChatMessage();
126 }
127 }
128
129 _chatInputChanged(event) {
130 this.content = event.target.value;
Sean McCullough07b3e392025-04-21 22:51:14 +0000131 // Use requestAnimationFrame to ensure DOM updates have completed
Sean McCullough86b56862025-04-18 13:04:03 -0700132 requestAnimationFrame(() => this.adjustChatSpacing());
133 }
134
135 @query("#chatInput")
Sean McCullough07b3e392025-04-21 22:51:14 +0000136 chatInput: HTMLTextAreaElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700137
138 protected firstUpdated(): void {
139 if (this.chatInput) {
140 this.chatInput.focus();
Sean McCullough07b3e392025-04-21 22:51:14 +0000141 // Initialize the input height
142 this.adjustChatSpacing();
Sean McCullough86b56862025-04-18 13:04:03 -0700143 }
Josh Bleecher Snydere2c7f722025-05-01 21:58:41 +0000144
145 // Add window.onload handler to ensure the input is focused when the page fully loads
146 window.addEventListener(
147 "load",
148 () => {
149 if (this.chatInput) {
150 this.chatInput.focus();
151 }
152 },
153 { once: true },
154 );
Sean McCullough86b56862025-04-18 13:04:03 -0700155 }
156
157 render() {
158 return html`
159 <div class="chat-container">
160 <div class="chat-input-wrapper">
161 <textarea
162 id="chatInput"
163 placeholder="Type your message here and press Enter to send..."
164 autofocus
165 @keydown="${this._chatInputKeyDown}"
166 @input="${this._chatInputChanged}"
167 .value=${this.content || ""}
168 ></textarea>
169 <button @click="${this._sendChatClicked}" id="sendChatButton">
170 Send
171 </button>
172 </div>
173 </div>
174 `;
175 }
176}
177
178declare global {
179 interface HTMLElementTagNameMap {
180 "sketch-chat-input": SketchChatInput;
181 }
182}