blob: d634da510748fc0225b2e4642d3af83139ee45b7 [file] [log] [blame]
Philip Zeyligere08c7ff2025-06-06 13:22:12 -07001import { css, html, LitElement } from "lit";
2import { customElement, property, state } from "lit/decorators.js";
3import { AgentMessage } from "../types";
4import { createRef, ref } from "lit/directives/ref.js";
5
6@customElement("mobile-chat")
7export class MobileChat extends LitElement {
8 @property({ type: Array })
9 messages: AgentMessage[] = [];
10
11 @property({ type: Boolean })
12 isThinking = false;
13
14 private scrollContainer = createRef<HTMLDivElement>();
15
16 static styles = css`
17 :host {
18 display: block;
19 height: 100%;
20 overflow: hidden;
21 }
22
23 .chat-container {
24 height: 100%;
25 overflow-y: auto;
26 padding: 16px;
27 display: flex;
28 flex-direction: column;
29 gap: 16px;
30 scroll-behavior: smooth;
31 -webkit-overflow-scrolling: touch;
32 }
33
34 .message {
35 display: flex;
36 flex-direction: column;
37 max-width: 85%;
38 word-wrap: break-word;
39 }
40
41 .message.user {
42 align-self: flex-end;
43 align-items: flex-end;
44 }
45
46 .message.assistant {
47 align-self: flex-start;
48 align-items: flex-start;
49 }
50
51 .message-bubble {
52 padding: 8px 12px;
53 border-radius: 18px;
54 font-size: 16px;
55 line-height: 1.4;
56 }
57
58 .message.user .message-bubble {
59 background-color: #007bff;
60 color: white;
61 border-bottom-right-radius: 6px;
62 }
63
64 .message.assistant .message-bubble {
65 background-color: #f1f3f4;
66 color: #333;
67 border-bottom-left-radius: 6px;
68 }
69
70 .thinking-message {
71 align-self: flex-start;
72 align-items: flex-start;
73 max-width: 85%;
74 }
75
76 .thinking-bubble {
77 background-color: #f1f3f4;
78 padding: 16px;
79 border-radius: 18px;
80 border-bottom-left-radius: 6px;
81 display: flex;
82 align-items: center;
83 gap: 8px;
84 }
85
86 .thinking-text {
87 color: #6c757d;
88 font-style: italic;
89 }
90
91 .thinking-dots {
92 display: flex;
93 gap: 3px;
94 }
95
96 .thinking-dot {
97 width: 6px;
98 height: 6px;
99 border-radius: 50%;
100 background-color: #6c757d;
101 animation: thinking 1.4s ease-in-out infinite both;
102 }
103
104 .thinking-dot:nth-child(1) {
105 animation-delay: -0.32s;
106 }
107 .thinking-dot:nth-child(2) {
108 animation-delay: -0.16s;
109 }
110 .thinking-dot:nth-child(3) {
111 animation-delay: 0;
112 }
113
114 @keyframes thinking {
115 0%,
116 80%,
117 100% {
118 transform: scale(0.8);
119 opacity: 0.5;
120 }
121 40% {
122 transform: scale(1);
123 opacity: 1;
124 }
125 }
126
127 .empty-state {
128 flex: 1;
129 display: flex;
130 align-items: center;
131 justify-content: center;
132 color: #6c757d;
133 font-style: italic;
134 text-align: center;
135 padding: 32px;
136 }
137 `;
138
139 updated(changedProperties: Map<string, any>) {
140 super.updated(changedProperties);
141 if (
142 changedProperties.has("messages") ||
143 changedProperties.has("isThinking")
144 ) {
145 this.scrollToBottom();
146 }
147 }
148
149 private scrollToBottom() {
150 // Use requestAnimationFrame to ensure DOM is updated
151 requestAnimationFrame(() => {
152 if (this.scrollContainer.value) {
153 this.scrollContainer.value.scrollTop =
154 this.scrollContainer.value.scrollHeight;
155 }
156 });
157 }
158
159 private formatTime(timestamp: string): string {
160 const date = new Date(timestamp);
161 return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
162 }
163
164 private getMessageRole(message: AgentMessage): string {
165 if (message.type === "user") {
166 return "user";
167 }
168 return "assistant";
169 }
170
171 private getMessageText(message: AgentMessage): string {
172 return message.content || "";
173 }
174
175 private shouldShowMessage(message: AgentMessage): boolean {
176 // Show user, agent, and error messages with content
177 return (
178 (message.type === "user" ||
179 message.type === "agent" ||
180 message.type === "error") &&
181 message.content &&
182 message.content.trim().length > 0
183 );
184 }
185
186 render() {
187 const displayMessages = this.messages.filter((msg) =>
188 this.shouldShowMessage(msg),
189 );
190
191 return html`
192 <div class="chat-container" ${ref(this.scrollContainer)}>
193 ${displayMessages.length === 0
194 ? html`
195 <div class="empty-state">Start a conversation with Sketch...</div>
196 `
197 : displayMessages.map((message) => {
198 const role = this.getMessageRole(message);
199 const text = this.getMessageText(message);
200 const timestamp = message.timestamp;
201
202 return html`
203 <div class="message ${role}">
204 <div class="message-bubble">${text}</div>
205 </div>
206 `;
207 })}
208 ${this.isThinking
209 ? html`
210 <div class="thinking-message">
211 <div class="thinking-bubble">
212 <span class="thinking-text">Sketch is thinking</span>
213 <div class="thinking-dots">
214 <div class="thinking-dot"></div>
215 <div class="thinking-dot"></div>
216 <div class="thinking-dot"></div>
217 </div>
218 </div>
219 </div>
220 `
221 : ""}
222 </div>
223 `;
224 }
225}