blob: 65b698b2e9ba17c76b4ee09637a8f8690461857c [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,
126 errorMessage?: string,
127 ) {
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
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000157 private handleViewChange = (event: CustomEvent<{ view: "chat" | "diff" }>) => {
158 this.currentView = event.detail.view;
159 };
160
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700161 render() {
162 const isThinking =
163 this.state?.outstanding_llm_calls > 0 ||
164 (this.state?.outstanding_tool_calls?.length ?? 0) > 0;
165
166 return html`
167 <div class="mobile-container">
168 <mobile-title
169 .connectionStatus=${this.connectionStatus}
170 .isThinking=${isThinking}
Philip Zeyliger0113be52025-06-07 23:53:41 +0000171 .skabandAddr=${this.state?.skaband_addr}
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000172 .currentView=${this.currentView}
173 .slug=${this.state?.slug || ""}
174 @view-change=${this.handleViewChange}
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700175 ></mobile-title>
176
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000177 ${this.currentView === "chat"
178 ? html`
179 <mobile-chat
180 .messages=${this.messages}
181 .isThinking=${isThinking}
182 ></mobile-chat>
183 `
184 : html`
185 <mobile-diff></mobile-diff>
186 `}
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}