blob: 583549dc1369a5c6456602c4b14a5b1c1fde1a26 [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";
10
11@customElement("mobile-shell")
12export class MobileShell extends LitElement {
13 private dataManager = new DataManager();
14
15 @state()
16 state: State | null = null;
17
18 @property({ attribute: false })
19 messages: AgentMessage[] = [];
20
21 @state()
22 connectionStatus: ConnectionStatus = "disconnected";
23
24 static styles = css`
25 :host {
26 display: flex;
27 flex-direction: column;
28 /* Use dynamic viewport height for better iOS support */
29 height: 100dvh;
30 /* Fallback for browsers that don't support dvh */
31 height: 100vh;
32 /* iOS Safari custom property fallback */
33 height: calc(var(--vh, 1vh) * 100);
34 /* Additional iOS Safari fix */
35 min-height: 100vh;
36 min-height: -webkit-fill-available;
37 width: 100vw;
38 background-color: #ffffff;
39 font-family:
40 -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif;
41 }
42
43 .mobile-container {
44 display: flex;
45 flex-direction: column;
46 height: 100%;
47 overflow: hidden;
48 }
49
50 mobile-title {
51 flex-shrink: 0;
52 }
53
54 mobile-chat {
55 flex: 1;
56 overflow: hidden;
57 }
58
59 mobile-chat-input {
60 flex-shrink: 0;
61 }
62 `;
63
64 connectedCallback() {
65 super.connectedCallback();
66 this.setupDataManager();
67 }
68
69 disconnectedCallback() {
70 super.disconnectedCallback();
71 // Remove event listeners
72 this.dataManager.removeEventListener(
73 "dataChanged",
74 this.handleDataChanged.bind(this),
75 );
76 this.dataManager.removeEventListener(
77 "connectionStatusChanged",
78 this.handleConnectionStatusChanged.bind(this),
79 );
80 }
81
82 private setupDataManager() {
83 // Add event listeners
84 this.dataManager.addEventListener(
85 "dataChanged",
86 this.handleDataChanged.bind(this),
87 );
88 this.dataManager.addEventListener(
89 "connectionStatusChanged",
90 this.handleConnectionStatusChanged.bind(this),
91 );
92
93 // Initialize the data manager - it will automatically connect to /stream?from=0
94 this.dataManager.initialize();
95 }
96
97 private handleDataChanged(eventData: {
98 state: State;
99 newMessages: AgentMessage[];
100 }) {
101 const { state, newMessages } = eventData;
102
103 if (state) {
104 this.state = state;
105 }
106
107 // Update messages using the same pattern as main app shell
108 this.messages = aggregateAgentMessages(this.messages, newMessages);
109 }
110
111 private handleConnectionStatusChanged(
112 status: ConnectionStatus,
113 errorMessage?: string,
114 ) {
115 this.connectionStatus = status;
116 }
117
118 private handleSendMessage = async (
119 event: CustomEvent<{ message: string }>,
120 ) => {
121 const message = event.detail.message.trim();
122 if (!message) {
123 return;
124 }
125
126 try {
127 // Send the message to the server
128 const response = await fetch("chat", {
129 method: "POST",
130 headers: {
131 "Content-Type": "application/json",
132 },
133 body: JSON.stringify({ message }),
134 });
135
136 if (!response.ok) {
137 console.error("Failed to send message:", response.statusText);
138 }
139 } catch (error) {
140 console.error("Error sending message:", error);
141 }
142 };
143
144 render() {
145 const isThinking =
146 this.state?.outstanding_llm_calls > 0 ||
147 (this.state?.outstanding_tool_calls?.length ?? 0) > 0;
148
149 return html`
150 <div class="mobile-container">
151 <mobile-title
152 .connectionStatus=${this.connectionStatus}
153 .isThinking=${isThinking}
154 ></mobile-title>
155
156 <mobile-chat
157 .messages=${this.messages}
158 .isThinking=${isThinking}
159 ></mobile-chat>
160
161 <mobile-chat-input
162 .disabled=${this.connectionStatus !== "connected"}
163 @send-message=${this.handleSendMessage}
164 ></mobile-chat-input>
165 </div>
166 `;
167 }
168}