blob: 5672be97387e399ff277105e56412207adb2bfda [file] [log] [blame]
import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ConnectionStatus, DataManager } from "../data";
import { AgentMessage, State } from "../types";
import { aggregateAgentMessages } from "./aggregateAgentMessages";
import { SketchTailwindElement } from "./sketch-tailwind-element";
import "./mobile-title";
import "./mobile-chat";
import "./mobile-chat-input";
import "./mobile-diff";
@customElement("mobile-shell")
export class MobileShell extends SketchTailwindElement {
private dataManager = new DataManager();
@state()
state: State | null = null;
@property({ attribute: false })
messages: AgentMessage[] = [];
@state()
connectionStatus: ConnectionStatus = "disconnected";
@state()
currentView: "chat" | "diff" = "chat";
connectedCallback() {
super.connectedCallback();
this.setupDataManager();
}
disconnectedCallback() {
super.disconnectedCallback();
// Remove event listeners
this.dataManager.removeEventListener(
"dataChanged",
this.handleDataChanged.bind(this),
);
this.dataManager.removeEventListener(
"connectionStatusChanged",
this.handleConnectionStatusChanged.bind(this),
);
}
private setupDataManager() {
// Add event listeners
this.dataManager.addEventListener(
"dataChanged",
this.handleDataChanged.bind(this),
);
this.dataManager.addEventListener(
"connectionStatusChanged",
this.handleConnectionStatusChanged.bind(this),
);
// Initialize the data manager - it will automatically connect to /stream?from=0
this.dataManager.initialize();
}
private handleDataChanged(eventData: {
state: State;
newMessages: AgentMessage[];
}) {
const { state, newMessages } = eventData;
if (state) {
this.state = state;
}
// Update messages using the same pattern as main app shell
this.messages = aggregateAgentMessages(this.messages, newMessages);
}
private handleConnectionStatusChanged(
status: ConnectionStatus,
_errorMessage?: string,
) {
this.connectionStatus = status;
}
private handleSendMessage = async (
event: CustomEvent<{ message: string }>,
) => {
const message = event.detail.message.trim();
if (!message) {
return;
}
try {
// Send the message to the server
const response = await fetch("chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message }),
});
if (!response.ok) {
console.error("Failed to send message:", response.statusText);
}
} catch (error) {
console.error("Error sending message:", error);
}
};
private handleViewChange = (
event: CustomEvent<{ view: "chat" | "diff" }>,
) => {
this.currentView = event.detail.view;
};
render() {
const isThinking =
this.state?.outstanding_llm_calls > 0 ||
(this.state?.outstanding_tool_calls?.length ?? 0) > 0;
return html`
<div
class="flex flex-col bg-white font-sans w-screen overflow-hidden"
style="height: 100dvh; height: 100vh; height: calc(var(--vh, 1vh) * 100); min-height: 100vh; min-height: -webkit-fill-available;"
>
<mobile-title
class="flex-shrink-0"
.connectionStatus=${this.connectionStatus}
.isThinking=${isThinking}
.skabandAddr=${this.state?.skaband_addr}
.currentView=${this.currentView}
.slug=${this.state?.slug || ""}
@view-change=${this.handleViewChange}
></mobile-title>
${this.currentView === "chat"
? html`
<mobile-chat
class="flex-1 overflow-hidden min-h-0"
.messages=${this.messages}
.isThinking=${isThinking}
></mobile-chat>
`
: html`<mobile-diff
class="flex-1 overflow-hidden min-h-0"
></mobile-diff>`}
<mobile-chat-input
class="flex-shrink-0 min-h-[64px]"
.disabled=${this.connectionStatus !== "connected"}
@send-message=${this.handleSendMessage}
></mobile-chat-input>
</div>
`;
}
}