@elizaos/app-core 2.0.0-alpha.70 → 2.0.0-alpha.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -5
- package/src/App.tsx +7 -1
- package/src/api/client.ts +3 -0
- package/src/bridge/electrobun-rpc.ts +1 -0
- package/src/components/AvatarLoader.tsx +1 -0
- package/src/components/CharacterRoster.tsx +1 -0
- package/src/components/CompanionSceneHost.tsx +21 -12
- package/src/components/FlaminaGuide.tsx +2 -2
- package/src/components/OnboardingWizard.tsx +36 -7
- package/src/components/avatar/VrmEngine.ts +7 -0
- package/src/components/avatar/VrmViewer.tsx +2 -2
- package/src/components/onboarding/ConnectionStep.tsx +15 -6
- package/src/components/onboarding/IdentityStep.tsx +110 -62
- package/src/components/onboarding/OnboardingStepNav.tsx +4 -1
- package/src/config/branding.ts +2 -0
- package/src/hooks/useMiladyBar.ts +4 -4
- package/src/state/AppContext.tsx +50 -21
- package/src/state/types.ts +15 -0
- package/src/state/vrm.ts +58 -26
- package/src/styles/onboarding-game.css +226 -1
- package/test/app/cloud-login-lock.test.ts +4 -0
- package/test/app/connection-mode-persistence.test.ts +411 -0
- package/test/app/onboarding-finish-lock.test.ts +3 -0
- package/test/app/startup-backend-missing.e2e.test.ts +8 -0
- package/.turbo/turbo-build.log +0 -4
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elizaos/app-core",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.72",
|
|
4
4
|
"description": "Shared application core for Milady shells and white-label apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"@capacitor/haptics": "8.0.0",
|
|
69
69
|
"@capacitor/keyboard": "8.0.0",
|
|
70
70
|
"@capacitor/preferences": "^8.0.1",
|
|
71
|
-
"@elizaos/autonomous": "
|
|
72
|
-
"@elizaos/ui": "
|
|
71
|
+
"@elizaos/autonomous": "workspace:*",
|
|
72
|
+
"@elizaos/ui": "workspace:*",
|
|
73
73
|
"@sparkjsdev/spark": "^0.1.10",
|
|
74
74
|
"lucide-react": "^0.575.0",
|
|
75
75
|
"three": "^0.182.0",
|
|
@@ -86,6 +86,5 @@
|
|
|
86
86
|
"tailwindcss": "^4.1.18",
|
|
87
87
|
"typescript": "^5.9.3",
|
|
88
88
|
"vitest": "^4.0.18"
|
|
89
|
-
}
|
|
90
|
-
"gitHead": "db049debacc33857bcea596057f1284568a061ff"
|
|
89
|
+
}
|
|
91
90
|
}
|
package/src/App.tsx
CHANGED
|
@@ -219,8 +219,14 @@ export function App() {
|
|
|
219
219
|
shellMode === "native" &&
|
|
220
220
|
(isCharacterTab(effectiveTab) || isCharacterTab(tab));
|
|
221
221
|
const companionShellVisible = shellMode === "companion";
|
|
222
|
+
// Don't initialize the 3D scene while the system is still booting — this
|
|
223
|
+
// prevents VrmEngine's Three.js setup from blocking the JS thread and
|
|
224
|
+
// delaying WebSocket agent-status updates (which would freeze the loader).
|
|
222
225
|
const companionSceneActive =
|
|
223
|
-
COMPANION_ENABLED &&
|
|
226
|
+
COMPANION_ENABLED &&
|
|
227
|
+
!onboardingLoading &&
|
|
228
|
+
agentStatus?.state !== "starting" &&
|
|
229
|
+
(companionShellVisible || characterSceneVisible);
|
|
224
230
|
const contextMenu = useContextMenu();
|
|
225
231
|
|
|
226
232
|
useStreamPopoutNavigation(setTab);
|
package/src/api/client.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const CHARACTER_PRESET_META: Record<
|
|
|
24
24
|
"hehe~": { name: "Rin", avatarIndex: 5, voicePresetId: "gigi" },
|
|
25
25
|
"...": { name: "Ryu", avatarIndex: 6, voicePresetId: "daniel" },
|
|
26
26
|
"lmao kms": { name: "Satoshi", avatarIndex: 7, voicePresetId: "callum" },
|
|
27
|
+
"bruh": { name: "Yuki", avatarIndex: 8, voicePresetId: "echo" },
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
/* ── Types ────────────────────────────────────────────────────────────── */
|
|
@@ -156,6 +156,13 @@ function CompanionSceneSurface({
|
|
|
156
156
|
companionZoomHydratedRef.current = true;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// Lazy-mount VrmStage: only initialize the 3D engine once the scene is
|
|
160
|
+
// actually needed (first time active becomes true). This prevents the WebGL
|
|
161
|
+
// context and asset loads from firing in native/chat mode on startup.
|
|
162
|
+
const hasEverBeenActiveRef = useRef(active);
|
|
163
|
+
if (active) hasEverBeenActiveRef.current = true;
|
|
164
|
+
const shouldMountVrm = hasEverBeenActiveRef.current;
|
|
165
|
+
|
|
159
166
|
const setCompanionZoom = useCallback((value: number) => {
|
|
160
167
|
const nextZoom = clampCompanionZoom(value);
|
|
161
168
|
companionZoomRef.current = nextZoom;
|
|
@@ -447,18 +454,20 @@ function CompanionSceneSurface({
|
|
|
447
454
|
>
|
|
448
455
|
<div className="absolute inset-0 z-0 bg-cover opacity-60 bg-[radial-gradient(circle_at_10%_20%,rgba(255,255,255,0.03)_0%,transparent_40%),radial-gradient(circle_at_80%_80%,rgba(0,225,255,0.05)_0%,transparent_40%)] pointer-events-none" />
|
|
449
456
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
457
|
+
{shouldMountVrm && (
|
|
458
|
+
<VrmStage
|
|
459
|
+
active={active}
|
|
460
|
+
vrmPath={vrmPath}
|
|
461
|
+
worldUrl={worldUrl}
|
|
462
|
+
fallbackPreviewUrl={fallbackPreviewUrl}
|
|
463
|
+
preloadAvatars={preloadAvatars}
|
|
464
|
+
cameraProfile="companion"
|
|
465
|
+
onEngineReady={handleStageEngineReady}
|
|
466
|
+
onLayerEngineReady={handleStageLayerEngineReady}
|
|
467
|
+
playWaveOnAvatarChange
|
|
468
|
+
t={t}
|
|
469
|
+
/>
|
|
470
|
+
)}
|
|
462
471
|
|
|
463
472
|
<div
|
|
464
473
|
aria-hidden="true"
|
|
@@ -177,7 +177,7 @@ export function DeferredSetupChecklist({
|
|
|
177
177
|
</div>
|
|
178
178
|
|
|
179
179
|
<div className="space-y-2">
|
|
180
|
-
{onboardingDeferredTasks.map((task) => (
|
|
180
|
+
{(onboardingDeferredTasks as FlaminaGuideTopic[]).map((task) => (
|
|
181
181
|
<div
|
|
182
182
|
key={task}
|
|
183
183
|
className="flex flex-col gap-2 rounded-xl border border-border/50 bg-bg/50 px-3 py-3 md:flex-row md:items-center md:justify-between"
|
|
@@ -199,7 +199,7 @@ export function DeferredSetupChecklist({
|
|
|
199
199
|
<button
|
|
200
200
|
type="button"
|
|
201
201
|
className="rounded-full border border-border/60 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted transition-colors hover:text-txt"
|
|
202
|
-
onClick={() => markDone(task)}
|
|
202
|
+
onClick={() => markDone(task as FlaminaGuideTopic)}
|
|
203
203
|
>
|
|
204
204
|
Done
|
|
205
205
|
</button>
|
|
@@ -19,14 +19,19 @@ import { OnboardingStepNav } from "./onboarding/OnboardingStepNav";
|
|
|
19
19
|
import { PermissionsStep } from "./onboarding/PermissionsStep";
|
|
20
20
|
import { RpcStep } from "./onboarding/RpcStep";
|
|
21
21
|
|
|
22
|
+
const FORCE_VRM =
|
|
23
|
+
typeof window !== "undefined" &&
|
|
24
|
+
new URLSearchParams(window.location.search).get("test_force_vrm") === "1";
|
|
25
|
+
|
|
22
26
|
const DISABLE_ONBOARDING_VRM =
|
|
23
|
-
|
|
24
|
-
String(import.meta.env.VITE_E2E_DISABLE_VRM ?? "") === "
|
|
27
|
+
!FORCE_VRM &&
|
|
28
|
+
(String(import.meta.env.VITE_E2E_DISABLE_VRM ?? "").toLowerCase() === "true" ||
|
|
29
|
+
String(import.meta.env.VITE_E2E_DISABLE_VRM ?? "") === "1");
|
|
25
30
|
|
|
26
31
|
export function OnboardingWizard() {
|
|
27
32
|
const branding = useBranding();
|
|
28
33
|
const isEliza = branding.appName === "Eliza";
|
|
29
|
-
const disableVrm = DISABLE_ONBOARDING_VRM || isEliza;
|
|
34
|
+
const disableVrm = !FORCE_VRM && (DISABLE_ONBOARDING_VRM || isEliza);
|
|
30
35
|
const [revealStarted, setRevealStarted] = useState(disableVrm);
|
|
31
36
|
|
|
32
37
|
const {
|
|
@@ -36,6 +41,7 @@ export function OnboardingWizard() {
|
|
|
36
41
|
uiLanguage,
|
|
37
42
|
uiTheme,
|
|
38
43
|
setState,
|
|
44
|
+
handleOnboardingNext,
|
|
39
45
|
t,
|
|
40
46
|
} = useApp();
|
|
41
47
|
|
|
@@ -62,10 +68,25 @@ export function OnboardingWizard() {
|
|
|
62
68
|
};
|
|
63
69
|
}, [uiTheme]);
|
|
64
70
|
|
|
71
|
+
// Auto-advance past the wakeUp splash once the VRM reveal animation starts,
|
|
72
|
+
// or after a 4-second timeout to prevent getting stuck when VRM fails to load.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (onboardingStep !== "wakeUp") return;
|
|
75
|
+
if (revealStarted) {
|
|
76
|
+
handleOnboardingNext();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
setRevealStarted(true);
|
|
81
|
+
}, 4000);
|
|
82
|
+
return () => clearTimeout(timer);
|
|
83
|
+
}, [onboardingStep, revealStarted, handleOnboardingNext]);
|
|
84
|
+
|
|
65
85
|
function renderStep() {
|
|
66
86
|
switch (onboardingStep) {
|
|
67
87
|
case "identity":
|
|
68
|
-
|
|
88
|
+
// Rendered outside the panel in OnboardingWizard JSX
|
|
89
|
+
return null;
|
|
69
90
|
case "connection":
|
|
70
91
|
return <ConnectionStep />;
|
|
71
92
|
case "rpc":
|
|
@@ -108,6 +129,7 @@ export function OnboardingWizard() {
|
|
|
108
129
|
)}
|
|
109
130
|
|
|
110
131
|
<div
|
|
132
|
+
data-testid="onboarding-ui-overlay"
|
|
111
133
|
style={{
|
|
112
134
|
position: "absolute",
|
|
113
135
|
inset: 0,
|
|
@@ -210,9 +232,16 @@ export function OnboardingWizard() {
|
|
|
210
232
|
{/* ── Standard overlaid UI — step nav + content panel ── */}
|
|
211
233
|
<div className="onboarding-ui-overlay">
|
|
212
234
|
<OnboardingStepNav />
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
235
|
+
{onboardingStep === "identity" ? (
|
|
236
|
+
/* Identity step renders full-width at the bottom — no glass panel */
|
|
237
|
+
<div className="ob-identity-overlay">
|
|
238
|
+
<IdentityStep />
|
|
239
|
+
</div>
|
|
240
|
+
) : (
|
|
241
|
+
<OnboardingPanel step={onboardingStep}>
|
|
242
|
+
{renderStep()}
|
|
243
|
+
</OnboardingPanel>
|
|
244
|
+
)}
|
|
216
245
|
</div>
|
|
217
246
|
</div>
|
|
218
247
|
</div>
|
|
@@ -838,6 +838,9 @@ export class VrmEngine {
|
|
|
838
838
|
if (!this.scene || !this.renderer) return;
|
|
839
839
|
|
|
840
840
|
const { SparkRenderer } = await this.loadSparkModule();
|
|
841
|
+
// Re-check after async import — the engine may have been disposed during
|
|
842
|
+
// the await (e.g. React StrictMode double-mount).
|
|
843
|
+
if (!this.scene || !this.renderer) return;
|
|
841
844
|
const sparkRenderer = new SparkRenderer({
|
|
842
845
|
renderer: this.renderer as THREE.WebGLRenderer,
|
|
843
846
|
apertureAngle: 0.0,
|
|
@@ -1911,7 +1914,11 @@ export class VrmEngine {
|
|
|
1911
1914
|
}
|
|
1912
1915
|
|
|
1913
1916
|
await this.ensureSparkRenderer();
|
|
1917
|
+
// Re-check after async — the engine may have been disposed during the
|
|
1918
|
+
// await (e.g. React StrictMode double-mount or rapid navigation).
|
|
1919
|
+
if (!this.scene || this.loadingAborted || requestId !== this.worldLoadRequestId) return;
|
|
1914
1920
|
const spark = await this.loadSparkModule();
|
|
1921
|
+
if (!this.scene || this.loadingAborted || requestId !== this.worldLoadRequestId) return;
|
|
1915
1922
|
const { SplatMesh } = spark;
|
|
1916
1923
|
let worldAnchor = new THREE.Vector3(0, 0, 0);
|
|
1917
1924
|
let worldRevealRadius = 1;
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* the `mouthOpen` prop. Sized to fill its parent container.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { resolveAppAssetUrl } from "@elizaos/app-core/utils";
|
|
9
8
|
import { useEffect, useEffectEvent, useRef } from "react";
|
|
9
|
+
import { getVrmUrl } from "../../state/vrm";
|
|
10
10
|
import {
|
|
11
11
|
type CameraProfile,
|
|
12
12
|
type InteractionMode,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type VrmEngineState,
|
|
16
16
|
} from "./VrmEngine";
|
|
17
17
|
|
|
18
|
-
const DEFAULT_VRM_PATH =
|
|
18
|
+
const DEFAULT_VRM_PATH = getVrmUrl(1);
|
|
19
19
|
|
|
20
20
|
export type VrmViewerProps = {
|
|
21
21
|
/** When false the loaded scene stays resident but the render loop is paused */
|
|
@@ -9,6 +9,7 @@ import { isNative } from "@elizaos/app-core/platform";
|
|
|
9
9
|
import { getProviderLogo } from "@elizaos/app-core/providers";
|
|
10
10
|
import { useApp } from "@elizaos/app-core/state";
|
|
11
11
|
import { openExternalUrl } from "@elizaos/app-core/utils";
|
|
12
|
+
import { ONBOARDING_PROVIDER_CATALOG } from "@elizaos/autonomous/contracts/onboarding";
|
|
12
13
|
import { useEffect, useState } from "react";
|
|
13
14
|
|
|
14
15
|
function formatRequestError(err: unknown): string {
|
|
@@ -151,7 +152,13 @@ export function ConnectionStep() {
|
|
|
151
152
|
setState("onboardingOpenRouterModel", modelId);
|
|
152
153
|
};
|
|
153
154
|
|
|
154
|
-
|
|
155
|
+
// Use the static provider catalog shipped in the frontend bundle — no API
|
|
156
|
+
// call needed. Runtime overrides from `onboardingOptions` (if available)
|
|
157
|
+
// take precedence so the server can still augment the list when present.
|
|
158
|
+
const catalogProviders: ProviderOption[] =
|
|
159
|
+
(onboardingOptions?.providers as ProviderOption[] | undefined)?.length
|
|
160
|
+
? (onboardingOptions!.providers as ProviderOption[])
|
|
161
|
+
: ([...ONBOARDING_PROVIDER_CATALOG] as unknown as ProviderOption[]);
|
|
155
162
|
// Merge custom providers from branding (app-injected) with the catalog.
|
|
156
163
|
// Custom providers are appended; duplicates (by id) are skipped.
|
|
157
164
|
const customProviders = branding.customProviders ?? [];
|
|
@@ -293,17 +300,19 @@ export function ConnectionStep() {
|
|
|
293
300
|
}
|
|
294
301
|
};
|
|
295
302
|
|
|
296
|
-
// On native (mobile)
|
|
297
|
-
//
|
|
303
|
+
// On native (mobile) or when branding requires cloud-only, force sandbox mode
|
|
304
|
+
// — skip straight to Eliza Cloud login. These platforms cannot run a local
|
|
305
|
+
// backend, so cloud is the only option.
|
|
306
|
+
const forceCloud = isNative || branding.cloudOnly;
|
|
298
307
|
useEffect(() => {
|
|
299
|
-
if (
|
|
308
|
+
if (forceCloud && !onboardingRunMode) {
|
|
300
309
|
setState("onboardingRunMode", "cloud");
|
|
301
310
|
setState("onboardingCloudProvider", "elizacloud");
|
|
302
311
|
setState("onboardingProvider", "");
|
|
303
312
|
setState("onboardingApiKey", "");
|
|
304
313
|
setState("onboardingPrimaryModel", "");
|
|
305
314
|
}
|
|
306
|
-
}, [
|
|
315
|
+
}, [forceCloud, onboardingRunMode, setState]);
|
|
307
316
|
|
|
308
317
|
if (!showProviderSelection) {
|
|
309
318
|
if (!onboardingRunMode) {
|
|
@@ -319,7 +328,7 @@ export function ConnectionStep() {
|
|
|
319
328
|
{t("onboarding.hostingQuestion")}
|
|
320
329
|
</div>
|
|
321
330
|
<div className="onboarding-provider-grid">
|
|
322
|
-
{!isNative && (
|
|
331
|
+
{!isNative && !branding.cloudOnly && (
|
|
323
332
|
<button
|
|
324
333
|
type="button"
|
|
325
334
|
className="onboarding-provider-card onboarding-provider-card--recommended"
|
|
@@ -1,29 +1,36 @@
|
|
|
1
|
-
import { client } from "@elizaos/app-core/api";
|
|
2
1
|
import { useApp } from "@elizaos/app-core/state";
|
|
2
|
+
import { getVrmPreviewUrl } from "@elizaos/app-core/state";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import {
|
|
5
|
-
CharacterRoster,
|
|
6
5
|
CHARACTER_PRESET_META,
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
SLANT_CLIP,
|
|
7
|
+
INSET_CLIP,
|
|
9
8
|
} from "../CharacterRoster";
|
|
10
9
|
|
|
10
|
+
/* ── Hardcoded frontend presets — no server needed ─────────────── */
|
|
11
|
+
|
|
12
|
+
const FRONTEND_PRESETS = Object.entries(CHARACTER_PRESET_META).map(
|
|
13
|
+
([catchphrase, meta]) => ({
|
|
14
|
+
id: catchphrase,
|
|
15
|
+
name: meta.name,
|
|
16
|
+
avatarIndex: meta.avatarIndex,
|
|
17
|
+
voicePresetId: meta.voicePresetId,
|
|
18
|
+
catchphrase,
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
type PresetEntry = (typeof FRONTEND_PRESETS)[number];
|
|
23
|
+
|
|
11
24
|
export function IdentityStep() {
|
|
12
25
|
const {
|
|
13
|
-
onboardingOptions,
|
|
14
26
|
onboardingStyle,
|
|
15
27
|
handleOnboardingNext,
|
|
16
28
|
setState,
|
|
17
29
|
t,
|
|
18
30
|
} = useApp();
|
|
19
31
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const rosterEntries = useMemo(
|
|
24
|
-
() => resolveRosterEntries(styles).slice(0, 4),
|
|
25
|
-
[styles],
|
|
26
|
-
);
|
|
32
|
+
const entries = FRONTEND_PRESETS;
|
|
33
|
+
const selectedId = onboardingStyle || entries[0]?.catchphrase || "";
|
|
27
34
|
|
|
28
35
|
/* ── Import / restore state ─────────────────────────────────────── */
|
|
29
36
|
const [showImport, setShowImport] = useState(false);
|
|
@@ -34,6 +41,22 @@ export function IdentityStep() {
|
|
|
34
41
|
const [importSuccess, setImportSuccess] = useState<string | null>(null);
|
|
35
42
|
const importBusyRef = useRef(false);
|
|
36
43
|
|
|
44
|
+
const handleSelect = useCallback(
|
|
45
|
+
(entry: PresetEntry) => {
|
|
46
|
+
setState("onboardingStyle", entry.catchphrase);
|
|
47
|
+
setState("onboardingName", entry.name);
|
|
48
|
+
setState("selectedVrmIndex", entry.avatarIndex);
|
|
49
|
+
},
|
|
50
|
+
[setState],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Auto-select the first one if nothing is selected yet
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!onboardingStyle && entries.length > 0) {
|
|
56
|
+
handleSelect(entries[0]);
|
|
57
|
+
}
|
|
58
|
+
}, [onboardingStyle, entries, handleSelect]);
|
|
59
|
+
|
|
37
60
|
const handleImportAgent = useCallback(async () => {
|
|
38
61
|
if (importBusyRef.current || importBusy) return;
|
|
39
62
|
if (!importFile) {
|
|
@@ -49,6 +72,8 @@ export function IdentityStep() {
|
|
|
49
72
|
setImportBusy(true);
|
|
50
73
|
setImportError(null);
|
|
51
74
|
setImportSuccess(null);
|
|
75
|
+
// Dynamic import to avoid hard dependency on client when server is absent
|
|
76
|
+
const { client } = await import("@elizaos/app-core/api");
|
|
52
77
|
const fileBuffer = await importFile.arrayBuffer();
|
|
53
78
|
const result = await client.importAgent(importPassword, fileBuffer);
|
|
54
79
|
const counts = result.counts;
|
|
@@ -75,25 +100,6 @@ export function IdentityStep() {
|
|
|
75
100
|
}
|
|
76
101
|
}, [importBusy, importFile, importPassword, t]);
|
|
77
102
|
|
|
78
|
-
const handleSelect = useCallback(
|
|
79
|
-
(entry: CharacterRosterEntry) => {
|
|
80
|
-
setState("onboardingStyle", entry.id);
|
|
81
|
-
const meta = CHARACTER_PRESET_META[entry.id];
|
|
82
|
-
if (meta) {
|
|
83
|
-
setState("onboardingName", meta.name);
|
|
84
|
-
setState("selectedVrmIndex", meta.avatarIndex);
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
[setState],
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
// Auto-select the first one if nothing is selected yet
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
if (!onboardingStyle && rosterEntries.length > 0) {
|
|
93
|
-
handleSelect(rosterEntries[0]);
|
|
94
|
-
}
|
|
95
|
-
}, [onboardingStyle, rosterEntries, handleSelect]);
|
|
96
|
-
|
|
97
103
|
/* ── Import UI ──────────────────────────────────────────────────── */
|
|
98
104
|
if (showImport) {
|
|
99
105
|
return (
|
|
@@ -166,38 +172,80 @@ export function IdentityStep() {
|
|
|
166
172
|
);
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
/* ──
|
|
175
|
+
/* ── Overwatch-style character select — full-width bottom bar ──── */
|
|
176
|
+
const selected = entries.find((e) => e.catchphrase === selectedId);
|
|
177
|
+
|
|
170
178
|
return (
|
|
171
|
-
<div className="
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
179
|
+
<div className="ob-identity">
|
|
180
|
+
{/* Selected character info — floats above the roster */}
|
|
181
|
+
<div className="ob-identity-info">
|
|
182
|
+
<div className="ob-identity-name">{selected?.name ?? ""}</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* ── Roster bar ── */}
|
|
186
|
+
<div className="ob-identity-roster">
|
|
187
|
+
{entries.map((entry) => {
|
|
188
|
+
const isSelected = selectedId === entry.catchphrase;
|
|
189
|
+
return (
|
|
190
|
+
<button
|
|
191
|
+
key={entry.catchphrase}
|
|
192
|
+
type="button"
|
|
193
|
+
className={`ob-identity-card ${isSelected ? "ob-identity-card--active" : ""}`}
|
|
194
|
+
onClick={() => handleSelect(entry)}
|
|
195
|
+
data-testid={`onboarding-preset-${entry.catchphrase}`}
|
|
196
|
+
>
|
|
197
|
+
<div
|
|
198
|
+
className={`ob-identity-card-frame ${isSelected ? "ob-identity-card-frame--active" : ""}`}
|
|
199
|
+
style={{ clipPath: SLANT_CLIP }}
|
|
200
|
+
>
|
|
201
|
+
<div
|
|
202
|
+
className="ob-identity-card-inner"
|
|
203
|
+
style={{ clipPath: SLANT_CLIP }}
|
|
204
|
+
>
|
|
205
|
+
{isSelected && (
|
|
206
|
+
<div
|
|
207
|
+
className="pointer-events-none absolute -inset-3 bg-yellow-300/15 blur-xl"
|
|
208
|
+
style={{ clipPath: SLANT_CLIP }}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
<img
|
|
212
|
+
src={getVrmPreviewUrl(entry.avatarIndex)}
|
|
213
|
+
alt={entry.name}
|
|
214
|
+
draggable={false}
|
|
215
|
+
className={`ob-identity-card-img ${isSelected ? "ob-identity-card-img--active" : ""}`}
|
|
216
|
+
/>
|
|
217
|
+
<div className="ob-identity-card-label">
|
|
218
|
+
<div
|
|
219
|
+
className={`ob-identity-card-name ${isSelected ? "ob-identity-card-name--active" : ""}`}
|
|
220
|
+
style={{ clipPath: INSET_CLIP }}
|
|
221
|
+
>
|
|
222
|
+
{entry.name}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</button>
|
|
228
|
+
);
|
|
229
|
+
})}
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* ── Actions row ── */}
|
|
233
|
+
<div className="ob-identity-actions">
|
|
234
|
+
<button
|
|
235
|
+
className="onboarding-confirm-btn"
|
|
236
|
+
onClick={() => handleOnboardingNext()}
|
|
237
|
+
type="button"
|
|
238
|
+
>
|
|
239
|
+
Continue
|
|
240
|
+
</button>
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
onClick={() => setShowImport(true)}
|
|
244
|
+
className="ob-identity-restore"
|
|
245
|
+
>
|
|
246
|
+
{t("onboarding.restoreFromBackup")}
|
|
247
|
+
</button>
|
|
248
|
+
</div>
|
|
201
249
|
</div>
|
|
202
250
|
);
|
|
203
251
|
}
|
|
@@ -6,11 +6,14 @@ export function OnboardingStepNav() {
|
|
|
6
6
|
const branding = useBranding();
|
|
7
7
|
|
|
8
8
|
const isEliza = branding.appName === "Eliza";
|
|
9
|
+
const isCloudOnly = !!branding.cloudOnly;
|
|
9
10
|
const activeSteps = isEliza
|
|
10
11
|
? ONBOARDING_STEPS.filter(
|
|
11
12
|
(s) => s.id === "connection" || s.id === "activate",
|
|
12
13
|
)
|
|
13
|
-
:
|
|
14
|
+
: isCloudOnly
|
|
15
|
+
? ONBOARDING_STEPS.filter((s) => s.id !== "wakeUp")
|
|
16
|
+
: ONBOARDING_STEPS;
|
|
14
17
|
|
|
15
18
|
const currentIndex = activeSteps.findIndex((s) => s.id === onboardingStep);
|
|
16
19
|
|
package/src/config/branding.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface BrandingConfig {
|
|
|
43
43
|
packageScope: string;
|
|
44
44
|
/** Custom providers injected by the app into the onboarding flow */
|
|
45
45
|
customProviders?: CustomProviderOption[];
|
|
46
|
+
/** When true, the app requires Eliza Cloud — local backend mode is disabled. */
|
|
47
|
+
cloudOnly?: boolean;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export const DEFAULT_BRANDING: BrandingConfig = {
|
|
@@ -18,10 +18,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
18
18
|
import {
|
|
19
19
|
type DetectedProvider,
|
|
20
20
|
invokeDesktopBridgeRequest,
|
|
21
|
-
scanAndValidateProviderCredentials,
|
|
22
21
|
scanProviderCredentials,
|
|
23
22
|
subscribeDesktopBridgeEvent,
|
|
24
23
|
} from "../bridge/electrobun-rpc";
|
|
24
|
+
import type { Tab } from "../navigation";
|
|
25
25
|
import { isDesktopPlatform } from "../platform";
|
|
26
26
|
import { useApp } from "../state";
|
|
27
27
|
|
|
@@ -423,8 +423,8 @@ export function useMiladyBar() {
|
|
|
423
423
|
// Validated scan — for background refresh and manual refresh
|
|
424
424
|
const runValidatedScan = useCallback(() => {
|
|
425
425
|
if (!isDesktopPlatform()) return;
|
|
426
|
-
|
|
427
|
-
.then((results) => {
|
|
426
|
+
scanProviderCredentials()
|
|
427
|
+
.then((results: DetectedProvider[]) => {
|
|
428
428
|
setScannedProviders(results);
|
|
429
429
|
setLastRefreshAt(Date.now());
|
|
430
430
|
setNow(Date.now());
|
|
@@ -514,7 +514,7 @@ export function useMiladyBar() {
|
|
|
514
514
|
}
|
|
515
515
|
if (itemId.startsWith("navigate-")) {
|
|
516
516
|
const target = itemId.replace("navigate-", "");
|
|
517
|
-
setTab?.(target);
|
|
517
|
+
setTab?.(target as Tab);
|
|
518
518
|
void invokeDesktopBridgeRequest({
|
|
519
519
|
rpcMethod: "desktopShowWindow",
|
|
520
520
|
ipcChannel: "desktop:showWindow",
|