blob: 74e462f3b80fda19c15a421491c5dfdcccc950e7 [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 {
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();
Philip Zeyliger73db6052025-04-23 13:09:07 -070069 this._handleDiffComment = this._handleDiffComment.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -070070 }
71
Sean McCullough86b56862025-04-18 13:04:03 -070072 connectedCallback() {
73 super.connectedCallback();
Philip Zeyliger73db6052025-04-23 13:09:07 -070074 window.addEventListener("diff-comment", this._handleDiffComment);
75 }
76
77 private _handleDiffComment(event: CustomEvent) {
78 const { comment } = event.detail;
79 if (!comment) return;
80
81 if (this.content != "") {
82 this.content += "\n\n";
83 }
84 this.content += comment;
85 requestAnimationFrame(() => this.adjustChatSpacing());
Sean McCullough86b56862025-04-18 13:04:03 -070086 }
87
88 // See https://lit.dev/docs/components/lifecycle/
89 disconnectedCallback() {
90 super.disconnectedCallback();
Philip Zeyliger73db6052025-04-23 13:09:07 -070091 window.removeEventListener("diff-comment", this._handleDiffComment);
Sean McCullough86b56862025-04-18 13:04:03 -070092 }
93
94 sendChatMessage() {
95 const event = new CustomEvent("send-chat", {
96 detail: { message: this.content },
97 bubbles: true,
98 composed: true,
99 });
100 this.dispatchEvent(event);
Philip Zeyliger73db6052025-04-23 13:09:07 -0700101
102 // TODO(philip?): Ideally we only clear the content if the send is successful.
103 this.content = ""; // Clear content after sending
Sean McCullough86b56862025-04-18 13:04:03 -0700104 }
105
106 adjustChatSpacing() {
Sean McCullough07b3e392025-04-21 22:51:14 +0000107 if (!this.chatInput) return;
Sean McCullough5164eee2025-04-21 18:20:23 -0700108
Sean McCullough07b3e392025-04-21 22:51:14 +0000109 // Reset height to minimal value to correctly calculate scrollHeight
Sean McCullough5164eee2025-04-21 18:20:23 -0700110 this.chatInput.style.height = "auto";
111
Sean McCullough07b3e392025-04-21 22:51:14 +0000112 // Get the scroll height (content height)
113 const scrollHeight = this.chatInput.scrollHeight;
Sean McCullough5164eee2025-04-21 18:20:23 -0700114
Sean McCullough07b3e392025-04-21 22:51:14 +0000115 // Set the height to match content (up to max-height which is handled by CSS)
116 this.chatInput.style.height = `${scrollHeight}px`;
Sean McCullough86b56862025-04-18 13:04:03 -0700117 }
118
Philip Zeyliger73db6052025-04-23 13:09:07 -0700119 async _sendChatClicked() {
Sean McCullough86b56862025-04-18 13:04:03 -0700120 this.sendChatMessage();
121 this.chatInput.focus(); // Refocus the input after sending
Sean McCullough07b3e392025-04-21 22:51:14 +0000122 // Reset height after sending a message
123 requestAnimationFrame(() => this.adjustChatSpacing());
Sean McCullough86b56862025-04-18 13:04:03 -0700124 }
125
126 _chatInputKeyDown(event: KeyboardEvent) {
127 // Send message if Enter is pressed without Shift key
128 if (event.key === "Enter" && !event.shiftKey) {
129 event.preventDefault(); // Prevent default newline
130 this.sendChatMessage();
131 }
132 }
133
134 _chatInputChanged(event) {
135 this.content = event.target.value;
Sean McCullough07b3e392025-04-21 22:51:14 +0000136 // Use requestAnimationFrame to ensure DOM updates have completed
Sean McCullough86b56862025-04-18 13:04:03 -0700137 requestAnimationFrame(() => this.adjustChatSpacing());
138 }
139
140 @query("#chatInput")
Sean McCullough07b3e392025-04-21 22:51:14 +0000141 chatInput: HTMLTextAreaElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700142
143 protected firstUpdated(): void {
144 if (this.chatInput) {
145 this.chatInput.focus();
Sean McCullough07b3e392025-04-21 22:51:14 +0000146 // Initialize the input height
147 this.adjustChatSpacing();
Sean McCullough86b56862025-04-18 13:04:03 -0700148 }
149 }
150
151 render() {
152 return html`
153 <div class="chat-container">
154 <div class="chat-input-wrapper">
155 <textarea
156 id="chatInput"
157 placeholder="Type your message here and press Enter to send..."
158 autofocus
159 @keydown="${this._chatInputKeyDown}"
160 @input="${this._chatInputChanged}"
161 .value=${this.content || ""}
162 ></textarea>
163 <button @click="${this._sendChatClicked}" id="sendChatButton">
164 Send
165 </button>
166 </div>
167 </div>
168 `;
169 }
170}
171
172declare global {
173 interface HTMLElementTagNameMap {
174 "sketch-chat-input": SketchChatInput;
175 }
176}