mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
iOS: add welcome home canvas
This commit is contained in:
committed by
Nimrod Gutman
parent
8ba1b6eff1
commit
67746a12de
@@ -34,18 +34,11 @@ extension NodeAppModel {
|
||||
}
|
||||
|
||||
func showA2UIOnConnectIfNeeded() async {
|
||||
let current = self.screen.urlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if current.isEmpty || current == self.lastAutoA2uiURL {
|
||||
if let canvasUrl = await self.resolveCanvasHostURLWithCapabilityRefresh(),
|
||||
let url = URL(string: canvasUrl),
|
||||
await Self.probeTCP(url: url, timeoutSeconds: 2.5)
|
||||
{
|
||||
self.screen.navigate(to: canvasUrl)
|
||||
self.lastAutoA2uiURL = canvasUrl
|
||||
} else {
|
||||
self.lastAutoA2uiURL = nil
|
||||
self.screen.showDefaultCanvas()
|
||||
}
|
||||
await MainActor.run {
|
||||
// Keep the bundled home canvas as the default connected view.
|
||||
// Agents can still explicitly present a remote or local canvas later.
|
||||
self.lastAutoA2uiURL = nil
|
||||
self.screen.showDefaultCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ final class NodeAppModel {
|
||||
var selectedAgentId: String?
|
||||
var gatewayDefaultAgentId: String?
|
||||
var gatewayAgents: [AgentSummary] = []
|
||||
var homeCanvasRevision: Int = 0
|
||||
var lastShareEventText: String = "No share events yet."
|
||||
var openChatRequestID: Int = 0
|
||||
private(set) var pendingAgentDeepLinkPrompt: AgentDeepLinkPrompt?
|
||||
@@ -548,6 +549,7 @@ final class NodeAppModel {
|
||||
self.seamColorHex = raw.isEmpty ? nil : raw
|
||||
self.mainSessionBaseKey = mainKey
|
||||
self.talkMode.updateMainSessionKey(self.mainSessionKey)
|
||||
self.homeCanvasRevision &+= 1
|
||||
}
|
||||
} catch {
|
||||
if let gatewayError = error as? GatewayResponseError {
|
||||
@@ -574,12 +576,19 @@ final class NodeAppModel {
|
||||
self.selectedAgentId = nil
|
||||
}
|
||||
self.talkMode.updateMainSessionKey(self.mainSessionKey)
|
||||
self.homeCanvasRevision &+= 1
|
||||
}
|
||||
} catch {
|
||||
// Best-effort only.
|
||||
}
|
||||
}
|
||||
|
||||
func refreshGatewayOverviewIfConnected() async {
|
||||
guard await self.isOperatorConnected() else { return }
|
||||
await self.refreshBrandingFromGateway()
|
||||
await self.refreshAgentsFromGateway()
|
||||
}
|
||||
|
||||
func setSelectedAgentId(_ agentId: String?) {
|
||||
let trimmed = (agentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let stableID = (self.connectedGatewayID ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -590,6 +599,7 @@ final class NodeAppModel {
|
||||
GatewaySettingsStore.saveGatewaySelectedAgentId(stableID: stableID, agentId: self.selectedAgentId)
|
||||
}
|
||||
self.talkMode.updateMainSessionKey(self.mainSessionKey)
|
||||
self.homeCanvasRevision &+= 1
|
||||
if let relay = ShareGatewayRelaySettings.loadConfig() {
|
||||
ShareGatewayRelaySettings.saveConfig(
|
||||
ShareGatewayRelayConfig(
|
||||
@@ -1749,6 +1759,7 @@ private extension NodeAppModel {
|
||||
self.gatewayDefaultAgentId = nil
|
||||
self.gatewayAgents = []
|
||||
self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID)
|
||||
self.homeCanvasRevision &+= 1
|
||||
self.apnsLastRegisteredTokenHex = nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import OpenClawProtocol
|
||||
|
||||
struct RootCanvas: View {
|
||||
@Environment(NodeAppModel.self) private var appModel
|
||||
@@ -137,16 +138,33 @@ struct RootCanvas: View {
|
||||
.environment(self.gatewayController)
|
||||
}
|
||||
.onAppear { self.updateIdleTimer() }
|
||||
.onAppear { self.updateHomeCanvasState() }
|
||||
.onAppear { self.evaluateOnboardingPresentation(force: false) }
|
||||
.onAppear { self.maybeAutoOpenSettings() }
|
||||
.onChange(of: self.preventSleep) { _, _ in self.updateIdleTimer() }
|
||||
.onChange(of: self.scenePhase) { _, _ in self.updateIdleTimer() }
|
||||
.onChange(of: self.scenePhase) { _, newValue in
|
||||
self.updateIdleTimer()
|
||||
self.updateHomeCanvasState()
|
||||
guard newValue == .active else { return }
|
||||
Task {
|
||||
await self.appModel.refreshGatewayOverviewIfConnected()
|
||||
await MainActor.run {
|
||||
self.updateHomeCanvasState()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear { self.maybeShowQuickSetup() }
|
||||
.onChange(of: self.gatewayController.gateways.count) { _, _ in self.maybeShowQuickSetup() }
|
||||
.onAppear { self.updateCanvasDebugStatus() }
|
||||
.onChange(of: self.canvasDebugStatusEnabled) { _, _ in self.updateCanvasDebugStatus() }
|
||||
.onChange(of: self.appModel.gatewayStatusText) { _, _ in self.updateCanvasDebugStatus() }
|
||||
.onChange(of: self.appModel.gatewayServerName) { _, _ in self.updateCanvasDebugStatus() }
|
||||
.onChange(of: self.appModel.gatewayStatusText) { _, _ in
|
||||
self.updateCanvasDebugStatus()
|
||||
self.updateHomeCanvasState()
|
||||
}
|
||||
.onChange(of: self.appModel.gatewayServerName) { _, _ in
|
||||
self.updateCanvasDebugStatus()
|
||||
self.updateHomeCanvasState()
|
||||
}
|
||||
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
|
||||
if newValue != nil {
|
||||
self.showOnboarding = false
|
||||
@@ -155,7 +173,13 @@ struct RootCanvas: View {
|
||||
.onChange(of: self.onboardingRequestID) { _, _ in
|
||||
self.evaluateOnboardingPresentation(force: true)
|
||||
}
|
||||
.onChange(of: self.appModel.gatewayRemoteAddress) { _, _ in self.updateCanvasDebugStatus() }
|
||||
.onChange(of: self.appModel.gatewayRemoteAddress) { _, _ in
|
||||
self.updateCanvasDebugStatus()
|
||||
self.updateHomeCanvasState()
|
||||
}
|
||||
.onChange(of: self.appModel.homeCanvasRevision) { _, _ in
|
||||
self.updateHomeCanvasState()
|
||||
}
|
||||
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
|
||||
if newValue != nil {
|
||||
self.onboardingComplete = true
|
||||
@@ -209,6 +233,134 @@ struct RootCanvas: View {
|
||||
self.appModel.screen.updateDebugStatus(title: title, subtitle: subtitle)
|
||||
}
|
||||
|
||||
private func updateHomeCanvasState() {
|
||||
let payload = self.makeHomeCanvasPayload()
|
||||
guard let data = try? JSONEncoder().encode(payload),
|
||||
let json = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
self.appModel.screen.updateHomeCanvasState(json: nil)
|
||||
return
|
||||
}
|
||||
self.appModel.screen.updateHomeCanvasState(json: json)
|
||||
}
|
||||
|
||||
private func makeHomeCanvasPayload() -> HomeCanvasPayload {
|
||||
let gatewayName = self.normalized(self.appModel.gatewayServerName)
|
||||
let gatewayAddress = self.normalized(self.appModel.gatewayRemoteAddress)
|
||||
let gatewayLabel = gatewayName ?? gatewayAddress ?? "Gateway"
|
||||
let activeAgentID = self.resolveActiveAgentID()
|
||||
let agents = self.homeCanvasAgents(activeAgentID: activeAgentID)
|
||||
|
||||
switch self.gatewayStatus {
|
||||
case .connected:
|
||||
return HomeCanvasPayload(
|
||||
gatewayState: "connected",
|
||||
eyebrow: "Connected to \(gatewayLabel)",
|
||||
title: "Your agents are ready",
|
||||
subtitle:
|
||||
"This phone stays dormant until the gateway needs it, then wakes, syncs, and goes back to sleep.",
|
||||
gatewayLabel: gatewayLabel,
|
||||
activeAgentName: self.appModel.activeAgentName,
|
||||
activeAgentBadge: agents.first(where: { $0.isActive })?.badge ?? "OC",
|
||||
activeAgentCaption: "Selected on this phone",
|
||||
agentCount: agents.count,
|
||||
agents: Array(agents.prefix(6)),
|
||||
footer: "The overview refreshes on reconnect and when the app returns to foreground.")
|
||||
case .connecting:
|
||||
return HomeCanvasPayload(
|
||||
gatewayState: "connecting",
|
||||
eyebrow: "Reconnecting",
|
||||
title: "OpenClaw is syncing back up",
|
||||
subtitle:
|
||||
"The gateway session is coming back online. "
|
||||
+ "Agent shortcuts should settle automatically in a moment.",
|
||||
gatewayLabel: gatewayLabel,
|
||||
activeAgentName: self.appModel.activeAgentName,
|
||||
activeAgentBadge: "OC",
|
||||
activeAgentCaption: "Gateway session in progress",
|
||||
agentCount: agents.count,
|
||||
agents: Array(agents.prefix(4)),
|
||||
footer: "If the gateway is reachable, reconnect should complete without intervention.")
|
||||
case .error, .disconnected:
|
||||
return HomeCanvasPayload(
|
||||
gatewayState: self.gatewayStatus == .error ? "error" : "offline",
|
||||
eyebrow: "Welcome to OpenClaw",
|
||||
title: "Your phone stays quiet until it is needed",
|
||||
subtitle:
|
||||
"Pair this device to your gateway to wake it only for real work, "
|
||||
+ "keep a live agent overview handy, and avoid battery-draining background loops.",
|
||||
gatewayLabel: gatewayLabel,
|
||||
activeAgentName: "Main",
|
||||
activeAgentBadge: "OC",
|
||||
activeAgentCaption: "Connect to load your agents",
|
||||
agentCount: agents.count,
|
||||
agents: Array(agents.prefix(4)),
|
||||
footer:
|
||||
"When connected, the gateway can wake the phone with a silent push "
|
||||
+ "instead of holding an always-on session.")
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveActiveAgentID() -> String {
|
||||
let selected = self.normalized(self.appModel.selectedAgentId) ?? ""
|
||||
if !selected.isEmpty {
|
||||
return selected
|
||||
}
|
||||
return self.resolveDefaultAgentID()
|
||||
}
|
||||
|
||||
private func resolveDefaultAgentID() -> String {
|
||||
self.normalized(self.appModel.gatewayDefaultAgentId) ?? ""
|
||||
}
|
||||
|
||||
private func homeCanvasAgents(activeAgentID: String) -> [HomeCanvasAgentCard] {
|
||||
let defaultAgentID = self.resolveDefaultAgentID()
|
||||
let cards = self.appModel.gatewayAgents.map { agent -> HomeCanvasAgentCard in
|
||||
let isActive = !activeAgentID.isEmpty && agent.id == activeAgentID
|
||||
let isDefault = !defaultAgentID.isEmpty && agent.id == defaultAgentID
|
||||
return HomeCanvasAgentCard(
|
||||
id: agent.id,
|
||||
name: self.homeCanvasName(for: agent),
|
||||
badge: self.homeCanvasBadge(for: agent),
|
||||
caption: isActive ? "Active on this phone" : (isDefault ? "Default agent" : "Ready"),
|
||||
isActive: isActive)
|
||||
}
|
||||
|
||||
return cards.sorted { lhs, rhs in
|
||||
if lhs.isActive != rhs.isActive {
|
||||
return lhs.isActive
|
||||
}
|
||||
return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
private func homeCanvasName(for agent: AgentSummary) -> String {
|
||||
self.normalized(agent.name) ?? agent.id
|
||||
}
|
||||
|
||||
private func homeCanvasBadge(for agent: AgentSummary) -> String {
|
||||
if let identity = agent.identity,
|
||||
let emoji = identity["emoji"]?.value as? String,
|
||||
let normalizedEmoji = self.normalized(emoji)
|
||||
{
|
||||
return normalizedEmoji
|
||||
}
|
||||
let words = self.homeCanvasName(for: agent)
|
||||
.split(whereSeparator: { $0.isWhitespace || $0 == "-" || $0 == "_" })
|
||||
.prefix(2)
|
||||
let initials = words.compactMap { $0.first }.map(String.init).joined()
|
||||
if !initials.isEmpty {
|
||||
return initials.uppercased()
|
||||
}
|
||||
return "OC"
|
||||
}
|
||||
|
||||
private func normalized(_ value: String?) -> String? {
|
||||
guard let value else { return nil }
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
|
||||
private func evaluateOnboardingPresentation(force: Bool) {
|
||||
if force {
|
||||
self.onboardingAllowSkip = true
|
||||
@@ -274,6 +426,28 @@ struct RootCanvas: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct HomeCanvasPayload: Codable {
|
||||
var gatewayState: String
|
||||
var eyebrow: String
|
||||
var title: String
|
||||
var subtitle: String
|
||||
var gatewayLabel: String
|
||||
var activeAgentName: String
|
||||
var activeAgentBadge: String
|
||||
var activeAgentCaption: String
|
||||
var agentCount: Int
|
||||
var agents: [HomeCanvasAgentCard]
|
||||
var footer: String
|
||||
}
|
||||
|
||||
private struct HomeCanvasAgentCard: Codable {
|
||||
var id: String
|
||||
var name: String
|
||||
var badge: String
|
||||
var caption: String
|
||||
var isActive: Bool
|
||||
}
|
||||
|
||||
private struct CanvasContent: View {
|
||||
@Environment(NodeAppModel.self) private var appModel
|
||||
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
|
||||
@@ -301,53 +475,33 @@ private struct CanvasContent: View {
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .topLeading) {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
StatusPill(
|
||||
gateway: self.gatewayStatus,
|
||||
voiceWakeEnabled: self.voiceWakeEnabled,
|
||||
activity: self.statusActivity,
|
||||
brighten: self.brightenButtons,
|
||||
onTap: {
|
||||
if self.gatewayStatus == .connected {
|
||||
self.showGatewayActions = true
|
||||
} else {
|
||||
self.openSettings()
|
||||
}
|
||||
})
|
||||
.layoutPriority(1)
|
||||
|
||||
Spacer(minLength: 8)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
OverlayButton(systemImage: "text.bubble.fill", brighten: self.brightenButtons) {
|
||||
self.openChat()
|
||||
}
|
||||
.accessibilityLabel("Chat")
|
||||
|
||||
if self.talkButtonEnabled {
|
||||
// Keep Talk mode near status controls while freeing right-side screen real estate.
|
||||
OverlayButton(
|
||||
systemImage: self.talkActive ? "waveform.circle.fill" : "waveform.circle",
|
||||
brighten: self.brightenButtons,
|
||||
tint: self.appModel.seamColor,
|
||||
isActive: self.talkActive)
|
||||
{
|
||||
let next = !self.talkActive
|
||||
self.talkEnabled = next
|
||||
self.appModel.setTalkEnabled(next)
|
||||
}
|
||||
.accessibilityLabel("Talk Mode")
|
||||
}
|
||||
|
||||
OverlayButton(systemImage: "gearshape.fill", brighten: self.brightenButtons) {
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
HomeToolbar(
|
||||
gateway: self.gatewayStatus,
|
||||
voiceWakeEnabled: self.voiceWakeEnabled,
|
||||
activity: self.statusActivity,
|
||||
brighten: self.brightenButtons,
|
||||
talkButtonEnabled: self.talkButtonEnabled,
|
||||
talkActive: self.talkActive,
|
||||
talkTint: self.appModel.seamColor,
|
||||
onStatusTap: {
|
||||
if self.gatewayStatus == .connected {
|
||||
self.showGatewayActions = true
|
||||
} else {
|
||||
self.openSettings()
|
||||
}
|
||||
.accessibilityLabel("Settings")
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.safeAreaPadding(.top, 10)
|
||||
},
|
||||
onChatTap: {
|
||||
self.openChat()
|
||||
},
|
||||
onTalkTap: {
|
||||
let next = !self.talkActive
|
||||
self.talkEnabled = next
|
||||
self.appModel.setTalkEnabled(next)
|
||||
},
|
||||
onSettingsTap: {
|
||||
self.openSettings()
|
||||
})
|
||||
}
|
||||
.overlay(alignment: .topLeading) {
|
||||
if let voiceWakeToastText, !voiceWakeToastText.isEmpty {
|
||||
|
||||
@@ -20,6 +20,7 @@ final class ScreenController {
|
||||
private var debugStatusEnabled: Bool = false
|
||||
private var debugStatusTitle: String?
|
||||
private var debugStatusSubtitle: String?
|
||||
private var homeCanvasStateJSON: String?
|
||||
|
||||
init() {
|
||||
self.reload()
|
||||
@@ -94,6 +95,26 @@ final class ScreenController {
|
||||
subtitle: self.debugStatusSubtitle)
|
||||
}
|
||||
|
||||
func updateHomeCanvasState(json: String?) {
|
||||
self.homeCanvasStateJSON = json
|
||||
self.applyHomeCanvasStateIfNeeded()
|
||||
}
|
||||
|
||||
func applyHomeCanvasStateIfNeeded() {
|
||||
guard let webView = self.activeWebView else { return }
|
||||
let payload = self.homeCanvasStateJSON ?? "null"
|
||||
let js = """
|
||||
(() => {
|
||||
try {
|
||||
const api = globalThis.__openclaw;
|
||||
if (!api || typeof api.renderHome !== 'function') return;
|
||||
api.renderHome(\(payload));
|
||||
} catch (_) {}
|
||||
})()
|
||||
"""
|
||||
webView.evaluateJavaScript(js) { _, _ in }
|
||||
}
|
||||
|
||||
func waitForA2UIReady(timeoutMs: Int) async -> Bool {
|
||||
let clock = ContinuousClock()
|
||||
let deadline = clock.now.advanced(by: .milliseconds(timeoutMs))
|
||||
@@ -191,6 +212,7 @@ final class ScreenController {
|
||||
self.activeWebView = webView
|
||||
self.reload()
|
||||
self.applyDebugStatusIfNeeded()
|
||||
self.applyHomeCanvasStateIfNeeded()
|
||||
}
|
||||
|
||||
func detachWebView(_ webView: WKWebView) {
|
||||
|
||||
@@ -161,6 +161,7 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
|
||||
func webView(_: WKWebView, didFinish _: WKNavigation?) {
|
||||
self.controller?.errorText = nil
|
||||
self.controller?.applyDebugStatusIfNeeded()
|
||||
self.controller?.applyHomeCanvasStateIfNeeded()
|
||||
}
|
||||
|
||||
func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) {
|
||||
|
||||
@@ -38,6 +38,7 @@ struct StatusPill: View {
|
||||
var gateway: GatewayState
|
||||
var voiceWakeEnabled: Bool
|
||||
var activity: Activity?
|
||||
var compact: Bool = false
|
||||
var brighten: Bool = false
|
||||
var onTap: () -> Void
|
||||
|
||||
@@ -45,11 +46,11 @@ struct StatusPill: View {
|
||||
|
||||
var body: some View {
|
||||
Button(action: self.onTap) {
|
||||
HStack(spacing: 10) {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: self.compact ? 8 : 10) {
|
||||
HStack(spacing: self.compact ? 6 : 8) {
|
||||
Circle()
|
||||
.fill(self.gateway.color)
|
||||
.frame(width: 9, height: 9)
|
||||
.frame(width: self.compact ? 8 : 9, height: self.compact ? 8 : 9)
|
||||
.scaleEffect(
|
||||
self.gateway == .connecting && !self.reduceMotion
|
||||
? (self.pulse ? 1.15 : 0.85)
|
||||
@@ -58,34 +59,38 @@ struct StatusPill: View {
|
||||
.opacity(self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.0 : 0.6) : 1.0)
|
||||
|
||||
Text(self.gateway.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font((self.compact ? Font.footnote : Font.subheadline).weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.frame(height: 14)
|
||||
.opacity(0.35)
|
||||
|
||||
if let activity {
|
||||
HStack(spacing: 6) {
|
||||
if !self.compact {
|
||||
Divider()
|
||||
.frame(height: 14)
|
||||
.opacity(0.35)
|
||||
}
|
||||
|
||||
HStack(spacing: self.compact ? 4 : 6) {
|
||||
Image(systemName: activity.systemImage)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font((self.compact ? Font.footnote : Font.subheadline).weight(.semibold))
|
||||
.foregroundStyle(activity.tint ?? .primary)
|
||||
Text(activity.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(1)
|
||||
if !self.compact {
|
||||
Text(activity.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
} else {
|
||||
Image(systemName: self.voiceWakeEnabled ? "mic.fill" : "mic.slash")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font((self.compact ? Font.footnote : Font.subheadline).weight(.semibold))
|
||||
.foregroundStyle(self.voiceWakeEnabled ? .primary : .secondary)
|
||||
.accessibilityLabel(self.voiceWakeEnabled ? "Voice Wake enabled" : "Voice Wake disabled")
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
}
|
||||
}
|
||||
.statusGlassCard(brighten: self.brighten, verticalPadding: 8)
|
||||
.statusGlassCard(brighten: self.brighten, verticalPadding: self.compact ? 6 : 8)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Connection Status")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Canvas</title>
|
||||
<title>OpenClaw</title>
|
||||
<script>
|
||||
(() => {
|
||||
try {
|
||||
@@ -15,99 +15,339 @@
|
||||
}
|
||||
if (/android/i.test(navigator.userAgent || '')) {
|
||||
document.documentElement.dataset.platform = 'android';
|
||||
} else {
|
||||
document.documentElement.dataset.platform = 'ios';
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body::before, body::after { animation: none !important; }
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #06070b;
|
||||
--panel: rgba(14, 17, 24, 0.74);
|
||||
--panel-strong: rgba(18, 23, 32, 0.86);
|
||||
--line: rgba(255, 255, 255, 0.1);
|
||||
--line-strong: rgba(255, 255, 255, 0.18);
|
||||
--text: rgba(255, 255, 255, 0.96);
|
||||
--muted: rgba(222, 229, 239, 0.72);
|
||||
--soft: rgba(222, 229, 239, 0.5);
|
||||
--accent: #8ec5ff;
|
||||
--accent-strong: #5b9dff;
|
||||
--accent-warm: #ff9159;
|
||||
--accent-rose: #ff5fa2;
|
||||
--state: #7d8ca3;
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
html,body { height:100%; margin:0; }
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, sans-serif;
|
||||
background:
|
||||
radial-gradient(1200px 900px at 15% 20%, rgba(42, 113, 255, 0.18), rgba(0,0,0,0) 55%),
|
||||
radial-gradient(900px 700px at 85% 30%, rgba(255, 0, 138, 0.14), rgba(0,0,0,0) 60%),
|
||||
radial-gradient(1000px 900px at 60% 90%, rgba(0, 209, 255, 0.10), rgba(0,0,0,0) 60%),
|
||||
#000;
|
||||
radial-gradient(900px 640px at 12% 10%, rgba(91, 157, 255, 0.36), rgba(0, 0, 0, 0) 58%),
|
||||
radial-gradient(840px 600px at 88% 16%, rgba(255, 95, 162, 0.24), rgba(0, 0, 0, 0) 62%),
|
||||
radial-gradient(960px 720px at 50% 100%, rgba(255, 145, 89, 0.18), rgba(0, 0, 0, 0) 60%),
|
||||
linear-gradient(180deg, #090b11 0%, #05060a 100%);
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
}
|
||||
:root[data-platform="android"] body {
|
||||
background:
|
||||
radial-gradient(1200px 900px at 15% 20%, rgba(42, 113, 255, 0.62), rgba(0,0,0,0) 55%),
|
||||
radial-gradient(900px 700px at 85% 30%, rgba(255, 0, 138, 0.52), rgba(0,0,0,0) 60%),
|
||||
radial-gradient(1000px 900px at 60% 90%, rgba(0, 209, 255, 0.48), rgba(0,0,0,0) 60%),
|
||||
#0b1328;
|
||||
}
|
||||
body::before {
|
||||
content:"";
|
||||
position: fixed;
|
||||
inset: -20%;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, rgba(255,255,255,0.03) 0, rgba(255,255,255,0.03) 1px,
|
||||
transparent 1px, transparent 48px),
|
||||
repeating-linear-gradient(90deg, rgba(255,255,255,0.03) 0, rgba(255,255,255,0.03) 1px,
|
||||
transparent 1px, transparent 48px);
|
||||
transform: translate3d(0,0,0) rotate(-7deg);
|
||||
will-change: transform, opacity;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
animation: openclaw-grid-drift 140s ease-in-out infinite alternate;
|
||||
}
|
||||
:root[data-platform="android"] body::before { opacity: 0.80; }
|
||||
|
||||
body::before,
|
||||
body::after {
|
||||
content:"";
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: -35%;
|
||||
background:
|
||||
radial-gradient(900px 700px at 30% 30%, rgba(42,113,255,0.16), rgba(0,0,0,0) 60%),
|
||||
radial-gradient(800px 650px at 70% 35%, rgba(255,0,138,0.12), rgba(0,0,0,0) 62%),
|
||||
radial-gradient(900px 800px at 55% 75%, rgba(0,209,255,0.10), rgba(0,0,0,0) 62%);
|
||||
filter: blur(28px);
|
||||
opacity: 0.52;
|
||||
will-change: transform, opacity;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform: translate3d(0,0,0);
|
||||
inset: -10%;
|
||||
pointer-events: none;
|
||||
animation: openclaw-glow-drift 110s ease-in-out infinite alternate;
|
||||
}
|
||||
:root[data-platform="android"] body::after { opacity: 0.85; }
|
||||
@supports (mix-blend-mode: screen) {
|
||||
body::after { mix-blend-mode: screen; }
|
||||
|
||||
body::before {
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.025) 0,
|
||||
rgba(255, 255, 255, 0.025) 1px,
|
||||
transparent 1px,
|
||||
transparent 52px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 255, 255, 0.025) 0,
|
||||
rgba(255, 255, 255, 0.025) 1px,
|
||||
transparent 1px,
|
||||
transparent 52px
|
||||
);
|
||||
opacity: 0.42;
|
||||
transform: rotate(-7deg);
|
||||
}
|
||||
@supports not (mix-blend-mode: screen) {
|
||||
body::after { opacity: 0.70; }
|
||||
}
|
||||
@keyframes openclaw-grid-drift {
|
||||
0% { transform: translate3d(-12px, 8px, 0) rotate(-7deg); opacity: 0.40; }
|
||||
50% { transform: translate3d( 10px,-7px, 0) rotate(-6.6deg); opacity: 0.56; }
|
||||
100% { transform: translate3d(-8px, 6px, 0) rotate(-7.2deg); opacity: 0.42; }
|
||||
}
|
||||
@keyframes openclaw-glow-drift {
|
||||
0% { transform: translate3d(-18px, 12px, 0) scale(1.02); opacity: 0.40; }
|
||||
50% { transform: translate3d( 14px,-10px, 0) scale(1.05); opacity: 0.52; }
|
||||
100% { transform: translate3d(-10px, 8px, 0) scale(1.03); opacity: 0.43; }
|
||||
|
||||
body::after {
|
||||
background:
|
||||
radial-gradient(700px 460px at 20% 18%, rgba(142, 197, 255, 0.18), rgba(0, 0, 0, 0) 62%),
|
||||
radial-gradient(720px 520px at 84% 20%, rgba(255, 95, 162, 0.14), rgba(0, 0, 0, 0) 66%),
|
||||
radial-gradient(860px 620px at 52% 88%, rgba(255, 145, 89, 0.14), rgba(0, 0, 0, 0) 64%);
|
||||
filter: blur(28px);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
body[data-state="connected"] { --state: #61d58b; }
|
||||
body[data-state="connecting"] { --state: #ffd05f; }
|
||||
body[data-state="error"] { --state: #ff6d6d; }
|
||||
body[data-state="offline"] { --state: #95a3b9; }
|
||||
|
||||
canvas {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display:block;
|
||||
width:100vw;
|
||||
height:100vh;
|
||||
touch-action: none;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
}
|
||||
:root[data-platform="android"] #openclaw-canvas {
|
||||
background:
|
||||
radial-gradient(1100px 800px at 20% 15%, rgba(42, 113, 255, 0.78), rgba(0,0,0,0) 58%),
|
||||
radial-gradient(900px 650px at 82% 28%, rgba(255, 0, 138, 0.66), rgba(0,0,0,0) 62%),
|
||||
radial-gradient(1000px 900px at 60% 88%, rgba(0, 209, 255, 0.58), rgba(0,0,0,0) 62%),
|
||||
#141c33;
|
||||
|
||||
#openclaw-home {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
padding: calc(var(--safe-top) + 22px) 18px calc(var(--safe-bottom) + 18px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(100%, 760px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(180deg, rgba(18, 24, 34, 0.86), rgba(10, 13, 19, 0.94));
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.42);
|
||||
padding: 22px 22px 18px;
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -30% auto auto -20%;
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
border-radius: 999px;
|
||||
background: radial-gradient(circle, rgba(142, 197, 255, 0.18), rgba(0, 0, 0, 0));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.eyebrow-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
background: var(--state);
|
||||
box-shadow: 0 0 18px color-mix(in srgb, var(--state) 68%, transparent);
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 18px 0 0;
|
||||
font-size: clamp(32px, 7vw, 52px);
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 14px 0 0;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: var(--muted);
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.meta-card,
|
||||
.agent-card {
|
||||
border-radius: 22px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.meta-card {
|
||||
padding: 16px 16px 15px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--soft);
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
margin-top: 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.meta-subtitle {
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.agent-focus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.agent-badge {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(142, 197, 255, 0.22), rgba(91, 157, 255, 0.1)),
|
||||
rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agent-focus .name {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.agent-focus .caption {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 16px 16px 14px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.section-count {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--soft);
|
||||
}
|
||||
|
||||
.agent-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
padding: 13px 13px 12px;
|
||||
}
|
||||
|
||||
.agent-card.active {
|
||||
background: var(--panel-strong);
|
||||
border-color: var(--line-strong);
|
||||
box-shadow: inset 0 0 0 1px rgba(142, 197, 255, 0.12);
|
||||
}
|
||||
|
||||
.agent-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.agent-row .badge {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agent-row .name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.agent-row .caption {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 18px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.12);
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
margin-top: 12px;
|
||||
color: var(--soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
#openclaw-status {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -115,41 +355,116 @@
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
padding-top: calc(20px + env(safe-area-inset-top, 0px));
|
||||
padding-top: calc(var(--safe-top) + 18px);
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#openclaw-status .card {
|
||||
text-align: center;
|
||||
padding: 16px 18px;
|
||||
border-radius: 14px;
|
||||
background: rgba(18, 18, 22, 0.42);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
box-shadow: 0 18px 60px rgba(0,0,0,0.55);
|
||||
background: rgba(18, 18, 22, 0.46);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
#openclaw-status .title {
|
||||
font: 600 20px -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, sans-serif;
|
||||
letter-spacing: 0.2px;
|
||||
color: rgba(255,255,255,0.92);
|
||||
text-shadow: 0 0 22px rgba(42, 113, 255, 0.35);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
#openclaw-status .subtitle {
|
||||
margin-top: 6px;
|
||||
font: 500 12px -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
|
||||
color: rgba(255,255,255,0.58);
|
||||
color: rgba(255, 255, 255, 0.58);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
#openclaw-home {
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
border-radius: 24px;
|
||||
padding: 18px 16px 16px;
|
||||
}
|
||||
|
||||
.hero-grid,
|
||||
.agent-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body::before,
|
||||
body::after {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body data-state="offline">
|
||||
<canvas id="openclaw-canvas"></canvas>
|
||||
<div id="openclaw-home">
|
||||
<div class="shell">
|
||||
<div class="hero">
|
||||
<div class="eyebrow">
|
||||
<span class="eyebrow-dot"></span>
|
||||
<span id="openclaw-home-eyebrow">Welcome to OpenClaw</span>
|
||||
</div>
|
||||
<h1 id="openclaw-home-title">Your phone stays quiet until it is needed</h1>
|
||||
<p id="openclaw-home-subtitle">
|
||||
Pair this device to your gateway to wake it only for real work, keep a live agent overview handy, and avoid battery-draining background loops.
|
||||
</p>
|
||||
|
||||
<div class="hero-grid">
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">Gateway</div>
|
||||
<div class="meta-value" id="openclaw-home-gateway">Gateway</div>
|
||||
<div class="meta-subtitle" id="openclaw-home-gateway-caption">Connect to load your agents</div>
|
||||
</div>
|
||||
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">Active Agent</div>
|
||||
<div class="agent-focus">
|
||||
<div class="agent-badge" id="openclaw-home-active-badge">OC</div>
|
||||
<div>
|
||||
<div class="name" id="openclaw-home-active-name">Main</div>
|
||||
<div class="caption" id="openclaw-home-active-caption">Connect to load your agents</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meta-card section">
|
||||
<div class="section-header">
|
||||
<div class="section-title">Live agents</div>
|
||||
<div class="section-count" id="openclaw-home-agent-count">0 agents</div>
|
||||
</div>
|
||||
<div class="agent-grid" id="openclaw-home-agent-grid"></div>
|
||||
<div class="footer-note" id="openclaw-home-footer">
|
||||
When connected, the gateway can wake the phone with a silent push instead of holding an always-on session.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="openclaw-status">
|
||||
<div class="card">
|
||||
<div class="title" id="openclaw-status-title">Ready</div>
|
||||
<div class="subtitle" id="openclaw-status-subtitle">Waiting for agent</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const canvas = document.getElementById('openclaw-canvas');
|
||||
@@ -157,6 +472,20 @@
|
||||
const statusEl = document.getElementById('openclaw-status');
|
||||
const titleEl = document.getElementById('openclaw-status-title');
|
||||
const subtitleEl = document.getElementById('openclaw-status-subtitle');
|
||||
const home = {
|
||||
root: document.getElementById('openclaw-home'),
|
||||
eyebrow: document.getElementById('openclaw-home-eyebrow'),
|
||||
title: document.getElementById('openclaw-home-title'),
|
||||
subtitle: document.getElementById('openclaw-home-subtitle'),
|
||||
gateway: document.getElementById('openclaw-home-gateway'),
|
||||
gatewayCaption: document.getElementById('openclaw-home-gateway-caption'),
|
||||
activeBadge: document.getElementById('openclaw-home-active-badge'),
|
||||
activeName: document.getElementById('openclaw-home-active-name'),
|
||||
activeCaption: document.getElementById('openclaw-home-active-caption'),
|
||||
agentCount: document.getElementById('openclaw-home-agent-count'),
|
||||
agentGrid: document.getElementById('openclaw-home-agent-grid'),
|
||||
footer: document.getElementById('openclaw-home-footer')
|
||||
};
|
||||
const debugStatusEnabledByQuery = (() => {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -172,54 +501,114 @@
|
||||
|
||||
function resize() {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = Math.max(1, Math.floor(window.innerWidth * dpr));
|
||||
const h = Math.max(1, Math.floor(window.innerHeight * dpr));
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const width = Math.max(1, Math.floor(window.innerWidth * dpr));
|
||||
const height = Math.max(1, Math.floor(window.innerHeight * dpr));
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
|
||||
function setDebugStatusEnabled(enabled) {
|
||||
debugStatusEnabled = !!enabled;
|
||||
if (!debugStatusEnabled) {
|
||||
statusEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(title, subtitle) {
|
||||
if (!debugStatusEnabled) return;
|
||||
if (!title && !subtitle) {
|
||||
statusEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
statusEl.style.display = 'flex';
|
||||
if (typeof title === 'string') titleEl.textContent = title;
|
||||
if (typeof subtitle === 'string') subtitleEl.textContent = subtitle;
|
||||
}
|
||||
|
||||
function clearChildren(node) {
|
||||
while (node.firstChild) node.removeChild(node.firstChild);
|
||||
}
|
||||
|
||||
function createAgentCard(agent) {
|
||||
const card = document.createElement('div');
|
||||
card.className = `agent-card${agent.isActive ? ' active' : ''}`;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'agent-row';
|
||||
|
||||
const badge = document.createElement('div');
|
||||
badge.className = 'badge';
|
||||
badge.textContent = agent.badge || 'OC';
|
||||
|
||||
const text = document.createElement('div');
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'name';
|
||||
name.textContent = agent.name || agent.id || 'Agent';
|
||||
|
||||
const caption = document.createElement('div');
|
||||
caption.className = 'caption';
|
||||
caption.textContent = agent.caption || 'Ready';
|
||||
|
||||
text.appendChild(name);
|
||||
text.appendChild(caption);
|
||||
row.appendChild(badge);
|
||||
row.appendChild(text);
|
||||
card.appendChild(row);
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderHome(state) {
|
||||
if (!state || typeof state !== 'object') return;
|
||||
|
||||
document.body.dataset.state = state.gatewayState || 'offline';
|
||||
home.root.style.display = 'flex';
|
||||
home.eyebrow.textContent = state.eyebrow || 'Welcome to OpenClaw';
|
||||
home.title.textContent = state.title || 'OpenClaw';
|
||||
home.subtitle.textContent = state.subtitle || '';
|
||||
home.gateway.textContent = state.gatewayLabel || 'Gateway';
|
||||
home.gatewayCaption.textContent = state.gatewayState === 'connected'
|
||||
? `${state.agentCount || 0} agent${state.agentCount === 1 ? '' : 's'} available`
|
||||
: (state.activeAgentCaption || 'Connect to load your agents');
|
||||
home.activeBadge.textContent = state.activeAgentBadge || 'OC';
|
||||
home.activeName.textContent = state.activeAgentName || 'Main';
|
||||
home.activeCaption.textContent = state.activeAgentCaption || '';
|
||||
home.agentCount.textContent = `${state.agentCount || 0} agent${state.agentCount === 1 ? '' : 's'}`;
|
||||
home.footer.textContent = state.footer || '';
|
||||
|
||||
clearChildren(home.agentGrid);
|
||||
const agents = Array.isArray(state.agents) ? state.agents : [];
|
||||
if (!agents.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'empty-state';
|
||||
empty.textContent = state.gatewayState === 'connected'
|
||||
? 'Your gateway is online. Agents will appear here as soon as the current scope reports them.'
|
||||
: 'Connect this phone to your gateway and the live agent overview will appear here.';
|
||||
home.agentGrid.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
agents.forEach((agent) => {
|
||||
home.agentGrid.appendChild(createAgentCard(agent));
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
const setDebugStatusEnabled = (enabled) => {
|
||||
debugStatusEnabled = !!enabled;
|
||||
if (!statusEl) return;
|
||||
if (!debugStatusEnabled) {
|
||||
statusEl.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
if (statusEl && !debugStatusEnabled) {
|
||||
if (!debugStatusEnabled) {
|
||||
statusEl.style.display = 'none';
|
||||
}
|
||||
|
||||
const api = {
|
||||
window.__openclaw = {
|
||||
canvas,
|
||||
ctx,
|
||||
setDebugStatusEnabled,
|
||||
setStatus: (title, subtitle) => {
|
||||
if (!statusEl || !debugStatusEnabled) return;
|
||||
if (!title && !subtitle) {
|
||||
statusEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
statusEl.style.display = 'flex';
|
||||
if (titleEl && typeof title === 'string') titleEl.textContent = title;
|
||||
if (subtitleEl && typeof subtitle === 'string') subtitleEl.textContent = subtitle;
|
||||
if (!debugStatusEnabled) {
|
||||
clearTimeout(window.__statusTimeout);
|
||||
window.__statusTimeout = setTimeout(() => {
|
||||
statusEl.style.display = 'none';
|
||||
}, 3000);
|
||||
} else {
|
||||
clearTimeout(window.__statusTimeout);
|
||||
}
|
||||
}
|
||||
setStatus,
|
||||
renderHome
|
||||
};
|
||||
window.__openclaw = api;
|
||||
})();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user