mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 13:51:37 +00:00
* feat(macos): mascot moods, randomized idle behavior, and onboarding Easter eggs * feat(apps): opt About window and iOS hero marks into mascot Easter eggs * chore(i18n): sync native app i18n inventory
133 lines
4.3 KiB
Swift
133 lines
4.3 KiB
Swift
import OpenClawChatUI
|
|
import OpenClawIPC
|
|
import SwiftUI
|
|
|
|
/// Onboarding hero mascot with the openclaw.ai hero treatment: the animated
|
|
/// mascot plus its coral silhouette glow (drop-shadow at ~10% of size).
|
|
/// Interactive: it reacts to clicks and its eyes follow the pointer.
|
|
struct GlowingOpenClawIcon: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
let size: CGFloat
|
|
let mood: OpenClawMascotMood
|
|
|
|
init(size: CGFloat = 148, mood: OpenClawMascotMood = .idle) {
|
|
self.size = size
|
|
self.mood = mood
|
|
}
|
|
|
|
var body: some View {
|
|
OpenClawMascotView(mood: self.mood, interactive: true)
|
|
.frame(width: self.size, height: self.size)
|
|
.shadow(
|
|
color: OpenClawMascotView.heroGlowColor(for: self.colorScheme),
|
|
radius: self.size * 0.1)
|
|
}
|
|
}
|
|
|
|
extension OnboardingView {
|
|
/// Onboarding page classes the mascot reacts to.
|
|
enum MascotPage {
|
|
case welcome
|
|
case connection
|
|
case cli
|
|
case ai
|
|
case permissions
|
|
case chat
|
|
case ready
|
|
}
|
|
|
|
/// Flow state the mascot mood is derived from.
|
|
struct MascotMoodSnapshot {
|
|
var page: MascotPage
|
|
var installingCLI = false
|
|
var cliInstalled = false
|
|
var cliStatusKnown = false
|
|
var aiPhase: OnboardingAISetupModel.Phase = .idle
|
|
var aiBusy = false
|
|
var aiFailed = false
|
|
var remoteProbeState: RemoteOnboardingProbeState = .idle
|
|
var allPermissionsGranted = false
|
|
}
|
|
|
|
/// The hero mascot mirrors what setup is doing: curious while choosing,
|
|
/// thinking while work is in flight, sad on failures, celebrating once
|
|
/// the AI answers and on the final page.
|
|
var mascotMood: OpenClawMascotMood {
|
|
Self.mascotMood(for: MascotMoodSnapshot(
|
|
page: self.mascotPage,
|
|
installingCLI: self.installingCLI,
|
|
cliInstalled: self.cliInstalled,
|
|
cliStatusKnown: self.cliStatusKnown,
|
|
aiPhase: self.aiSetup.phase,
|
|
aiBusy: self.aiSetup.isBusy,
|
|
aiFailed: Self.aiSetupLooksFailed(self.aiSetup),
|
|
remoteProbeState: self.remoteProbeState,
|
|
allPermissionsGranted: Capability.importanceOrdered
|
|
.allSatisfy { self.permissionMonitor.status[$0] ?? false }))
|
|
}
|
|
|
|
private var mascotPage: MascotPage {
|
|
switch self.activePageIndex {
|
|
case self.connectionPageIndex: .connection
|
|
case self.cliPageIndex: .cli
|
|
case self.aiPageIndex: .ai
|
|
case self.permissionsPageIndex: .permissions
|
|
case self.onboardingChatPageIndex: .chat
|
|
case self.readyPageIndex: .ready
|
|
default: .welcome
|
|
}
|
|
}
|
|
|
|
static func aiSetupLooksFailed(_ aiSetup: OnboardingAISetupModel) -> Bool {
|
|
guard !aiSetup.connected else { return false }
|
|
let candidateFailed = aiSetup.statuses.values.contains { status in
|
|
if case .failed = status { return true }
|
|
return false
|
|
}
|
|
return aiSetup.detectError != nil ||
|
|
aiSetup.exhaustedAutoCandidates ||
|
|
aiSetup.manualError != nil ||
|
|
candidateFailed
|
|
}
|
|
|
|
static func mascotMood(for snapshot: MascotMoodSnapshot) -> OpenClawMascotMood {
|
|
switch snapshot.page {
|
|
case .welcome:
|
|
.idle
|
|
case .connection:
|
|
switch snapshot.remoteProbeState {
|
|
case .checking: .thinking
|
|
case .failed: .sad
|
|
case .ok: .happy
|
|
case .idle: .curious
|
|
}
|
|
case .cli:
|
|
if snapshot.cliInstalled {
|
|
.happy
|
|
} else if snapshot.cliStatusKnown, !snapshot.installingCLI {
|
|
// Mirrors the page's install-failed card.
|
|
.sad
|
|
} else {
|
|
.thinking
|
|
}
|
|
case .ai:
|
|
if snapshot.aiPhase == .connected {
|
|
.celebrating
|
|
} else if snapshot.aiBusy {
|
|
.thinking
|
|
} else if snapshot.aiFailed {
|
|
.sad
|
|
} else {
|
|
.curious
|
|
}
|
|
case .permissions:
|
|
snapshot.allPermissionsGranted ? .happy : .curious
|
|
case .chat:
|
|
.attentive
|
|
case .ready:
|
|
.celebrating
|
|
}
|
|
}
|
|
}
|