blob: dd59ad8b5c6469c393c61ae83f3e93a0f6143084 [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 { ConnectionStatus, DataManager } from "../data";
4import { AgentMessage, State } from "../types";
5import { aggregateAgentMessages } from "./aggregateAgentMessages";
6
7import "./mobile-title";
8import "./mobile-chat";
9import "./mobile-chat-input";
philip.zeyliger6b8b7662025-06-16 03:06:30 +000010import "./mobile-diff";
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070011
12@customElement("mobile-shell")
13export class MobileShell extends LitElement {
14 private dataManager = new DataManager();
15
16 @state()
17 state: State | null = null;
18
19 @property({ attribute: false })
20 messages: AgentMessage[] = [];
21
22 @state()
23 connectionStatus: ConnectionStatus = "disconnected";
24
philip.zeyliger6b8b7662025-06-16 03:06:30 +000025 @state()
26 currentView: "chat" | "diff" = "chat";
27
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070028 static styles = css`
29 :host {
30 display: flex;
31 flex-direction: column;
32 /* Use dynamic viewport height for better iOS support */
33 height: 100dvh;
34 /* Fallback for browsers that don't support dvh */
35 height: 100vh;
36 /* iOS Safari custom property fallback */
37 height: calc(var(--vh, 1vh) * 100);
38 /* Additional iOS Safari fix */
39 min-height: 100vh;
40 min-height: -webkit-fill-available;
41 width: 100vw;
42 background-color: #ffffff;
43 font-family:
44 -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif;
45 }
46
47 .mobile-container {
48 display: flex;
49 flex-direction: column;
50 height: 100%;
51 overflow: hidden;
52 }
53
54 mobile-title {
55 flex-shrink: 0;
56 }
57
58 mobile-chat {
59 flex: 1;
60 overflow: hidden;
philip.zeyliger6b8b7662025-06-16 03:06:30 +000061 min-height: 0;
62 }
63
64 mobile-diff {
65 flex: 1;
66 overflow: hidden;
67 min-height: 0;
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070068 }
69
70 mobile-chat-input {
71 flex-shrink: 0;
philip.zeyliger6b8b7662025-06-16 03:06:30 +000072 /* Ensure proper height calculation */
73 min-height: 64px;
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070074 }
75 `;
76
77 connectedCallback() {
78 super.connectedCallback();
79 this.setupDataManager();
80 }
81
82 disconnectedCallback() {
83 super.disconnectedCallback();
84 // Remove event listeners
85 this.dataManager.removeEventListener(
86 "dataChanged",
87 this.handleDataChanged.bind(this),
88 );
89 this.dataManager.removeEventListener(
90 "connectionStatusChanged",
91 this.handleConnectionStatusChanged.bind(this),
92 );
93 }
94
95 private setupDataManager() {
96 // Add event listeners
97 this.dataManager.addEventListener(
98 "dataChanged",
99 this.handleDataChanged.bind(this),
100 );
101 this.dataManager.addEventListener(
102 "connectionStatusChanged",
103 this.handleConnectionStatusChanged.bind(this),
104 );
105
106 // Initialize the data manager - it will automatically connect to /stream?from=0
107 this.dataManager.initialize();
108 }
109
110 private handleDataChanged(eventData: {
111 state: State;
112 newMessages: AgentMessage[];
113 }) {
114 const { state, newMessages } = eventData;
115
116 if (state) {
117 this.state = state;
118 }
119
120 // Update messages using the same pattern as main app shell
121 this.messages = aggregateAgentMessages(this.messages, newMessages);
122 }
123
124 private handleConnectionStatusChanged(
125 status: ConnectionStatus,
philip.zeyliger26bc6592025-06-30 20:15:30 -0700126 _errorMessage?: string,
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700127 ) {
128 this.connectionStatus = status;
129 }
130
131 private handleSendMessage = async (
132 event: CustomEvent<{ message: string }>,
133 ) => {
134 const message = event.detail.message.trim();
135 if (!message) {
136 return;
137 }
138
139 try {
140 // Send the message to the server
141 const response = await fetch("chat", {
142 method: "POST",
143 headers: {
144 "Content-Type": "application/json",
145 },
146 body: JSON.stringify({ message }),
147 });
148
149 if (!response.ok) {
150 console.error("Failed to send message:", response.statusText);
151 }
152 } catch (error) {
153 console.error("Error sending message:", error);
154 }
155 };
156
Autoformatterf964b502025-06-17 04:30:35 +0000157 private handleViewChange = (
158 event: CustomEvent<{ view: "chat" | "diff" }>,
159 ) => {
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000160 this.currentView = event.detail.view;
161 };
162
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700163 render() {
164 const isThinking =
165 this.state?.outstanding_llm_calls > 0 ||
166 (this.state?.outstanding_tool_calls?.length ?? 0) > 0;
167
168 return html`
169 <div class="mobile-container">
170 <mobile-title
171 .connectionStatus=${this.connectionStatus}
172 .isThinking=${isThinking}
Philip Zeyliger0113be52025-06-07 23:53:41 +0000173 .skabandAddr=${this.state?.skaband_addr}
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000174 .currentView=${this.currentView}
175 .slug=${this.state?.slug || ""}
176 @view-change=${this.handleViewChange}
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700177 ></mobile-title>
178
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000179 ${this.currentView === "chat"
180 ? html`
181 <mobile-chat
182 .messages=${this.messages}
183 .isThinking=${isThinking}
184 ></mobile-chat>
185 `
Autoformatterf964b502025-06-17 04:30:35 +0000186 : html` <mobile-diff></mobile-diff> `}
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700187
188 <mobile-chat-input
189 .disabled=${this.connectionStatus !== "connected"}
190 @send-message=${this.handleSendMessage}
191 ></mobile-chat-input>
192 </div>
193 `;
194 }
195}