@elizaos/app-core 2.0.0-alpha.69 → 2.0.0-alpha.71
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 -4
- package/src/api/client.ts +134 -0
- package/src/bridge/electrobun-rpc.ts +1 -0
- package/src/components/CharacterRoster.tsx +7 -25
- package/src/components/CloudSourceControls.tsx +3 -6
- package/src/components/CompanionSceneHost.tsx +13 -0
- package/src/components/ConfigPageView.tsx +8 -11
- package/src/components/FlaminaGuide.tsx +2 -2
- package/src/components/VrmStage.tsx +14 -7
- package/src/components/avatar/VrmEngine.ts +7 -0
- package/src/components/avatar/VrmViewer.tsx +2 -2
- package/src/components/onboarding/ConnectionStep.tsx +37 -5
- package/src/components/onboarding/OnboardingStepNav.tsx +4 -1
- package/src/config/branding.ts +26 -3
- package/src/hooks/useMiladyBar.ts +4 -4
- package/src/i18n/locales/en.json +23 -23
- package/src/i18n/locales/es.json +23 -23
- package/src/i18n/locales/ko.json +23 -23
- package/src/i18n/locales/pt.json +23 -23
- package/src/i18n/locales/zh-CN.json +23 -23
- package/src/providers/index.ts +10 -1
- package/src/state/AppContext.tsx +231 -18
- package/src/state/internal.ts +4 -0
- package/src/state/persistence.ts +59 -0
- package/src/state/types.ts +15 -0
- package/src/state/vrm.ts +58 -26
- 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/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.71",
|
|
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": "2.0.0-alpha.
|
|
72
|
-
"@elizaos/ui": "2.0.0-alpha.
|
|
71
|
+
"@elizaos/autonomous": "2.0.0-alpha.71",
|
|
72
|
+
"@elizaos/ui": "2.0.0-alpha.71",
|
|
73
73
|
"@sparkjsdev/spark": "^0.1.10",
|
|
74
74
|
"lucide-react": "^0.575.0",
|
|
75
75
|
"three": "^0.182.0",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"typescript": "^5.9.3",
|
|
88
88
|
"vitest": "^4.0.18"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "c150a8cf4bffddd7e934f3a6073a092ca1cd6520"
|
|
91
91
|
}
|
package/src/api/client.ts
CHANGED
|
@@ -2087,6 +2087,10 @@ export class MiladyClient {
|
|
|
2087
2087
|
}
|
|
2088
2088
|
}
|
|
2089
2089
|
|
|
2090
|
+
getBaseUrl(): string {
|
|
2091
|
+
return this.baseUrl;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2090
2094
|
setBaseUrl(baseUrl: string | null): void {
|
|
2091
2095
|
const normalized = baseUrl?.trim().replace(/\/+$/, "") || "";
|
|
2092
2096
|
this._explicitBase = normalized.length > 0;
|
|
@@ -5519,7 +5523,137 @@ export class MiladyClient {
|
|
|
5519
5523
|
body: JSON.stringify({ settings }),
|
|
5520
5524
|
});
|
|
5521
5525
|
}
|
|
5526
|
+
|
|
5527
|
+
// ── Direct Eliza Cloud Auth (no local backend required) ─────────────
|
|
5528
|
+
|
|
5529
|
+
/**
|
|
5530
|
+
* Initiate a direct login to Eliza Cloud without going through a local agent.
|
|
5531
|
+
* Used in sandbox mode when no local backend exists yet.
|
|
5532
|
+
*/
|
|
5533
|
+
async cloudLoginDirect(
|
|
5534
|
+
cloudApiBase: string,
|
|
5535
|
+
): Promise<{ ok: boolean; browserUrl?: string; sessionId?: string; error?: string }> {
|
|
5536
|
+
const res = await fetch(`${cloudApiBase}/api/v1/auth/login`, {
|
|
5537
|
+
method: "POST",
|
|
5538
|
+
headers: { "Content-Type": "application/json" },
|
|
5539
|
+
});
|
|
5540
|
+
if (!res.ok) {
|
|
5541
|
+
return { ok: false, error: `Login failed (${res.status})` };
|
|
5542
|
+
}
|
|
5543
|
+
return res.json();
|
|
5544
|
+
}
|
|
5545
|
+
|
|
5546
|
+
/**
|
|
5547
|
+
* Poll a direct Eliza Cloud login session for authentication status.
|
|
5548
|
+
*/
|
|
5549
|
+
async cloudLoginPollDirect(
|
|
5550
|
+
cloudApiBase: string,
|
|
5551
|
+
sessionId: string,
|
|
5552
|
+
): Promise<{
|
|
5553
|
+
status: "pending" | "authenticated" | "expired" | "error";
|
|
5554
|
+
token?: string;
|
|
5555
|
+
userId?: string;
|
|
5556
|
+
error?: string;
|
|
5557
|
+
}> {
|
|
5558
|
+
const res = await fetch(
|
|
5559
|
+
`${cloudApiBase}/api/v1/auth/login/status?sessionId=${encodeURIComponent(sessionId)}`,
|
|
5560
|
+
);
|
|
5561
|
+
if (!res.ok) {
|
|
5562
|
+
return { status: "error", error: `Poll failed (${res.status})` };
|
|
5563
|
+
}
|
|
5564
|
+
return res.json();
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
// ── Eliza Cloud Sandbox Provisioning ───────────────────────────────
|
|
5568
|
+
|
|
5569
|
+
/**
|
|
5570
|
+
* Create a sandbox agent on Eliza Cloud and provision it.
|
|
5571
|
+
* Returns the bridge URL when provisioning completes.
|
|
5572
|
+
*
|
|
5573
|
+
* Flow:
|
|
5574
|
+
* 1. POST /api/v1/milady/agents — create agent record
|
|
5575
|
+
* 2. POST /api/v1/milady/agents/{id}/provision — start async provisioning
|
|
5576
|
+
* 3. Poll GET /api/v1/jobs/{jobId} until completed
|
|
5577
|
+
* 4. Return bridgeUrl from job result
|
|
5578
|
+
*/
|
|
5579
|
+
async provisionCloudSandbox(options: {
|
|
5580
|
+
cloudApiBase: string;
|
|
5581
|
+
authToken: string;
|
|
5582
|
+
name: string;
|
|
5583
|
+
bio?: string[];
|
|
5584
|
+
onProgress?: (status: string, detail?: string) => void;
|
|
5585
|
+
}): Promise<{ bridgeUrl: string; agentId: string }> {
|
|
5586
|
+
const { cloudApiBase, authToken, name, bio, onProgress } = options;
|
|
5587
|
+
const headers: Record<string, string> = {
|
|
5588
|
+
"Content-Type": "application/json",
|
|
5589
|
+
Authorization: `Bearer ${authToken}`,
|
|
5590
|
+
};
|
|
5591
|
+
|
|
5592
|
+
onProgress?.("creating", "Creating agent...");
|
|
5593
|
+
|
|
5594
|
+
// Step 1: Create agent
|
|
5595
|
+
const createRes = await fetch(`${cloudApiBase}/api/v1/milady/agents`, {
|
|
5596
|
+
method: "POST",
|
|
5597
|
+
headers,
|
|
5598
|
+
body: JSON.stringify({ name, bio }),
|
|
5599
|
+
});
|
|
5600
|
+
if (!createRes.ok) {
|
|
5601
|
+
const err = await createRes.text().catch(() => "Unknown error");
|
|
5602
|
+
throw new Error(`Failed to create cloud agent: ${err}`);
|
|
5603
|
+
}
|
|
5604
|
+
const createData = (await createRes.json()) as { id: string };
|
|
5605
|
+
const agentId = createData.id;
|
|
5606
|
+
|
|
5607
|
+
onProgress?.("provisioning", "Provisioning sandbox environment...");
|
|
5608
|
+
|
|
5609
|
+
// Step 2: Start provisioning
|
|
5610
|
+
const provisionRes = await fetch(
|
|
5611
|
+
`${cloudApiBase}/api/v1/milady/agents/${agentId}/provision`,
|
|
5612
|
+
{ method: "POST", headers },
|
|
5613
|
+
);
|
|
5614
|
+
if (!provisionRes.ok) {
|
|
5615
|
+
const err = await provisionRes.text().catch(() => "Unknown error");
|
|
5616
|
+
throw new Error(`Failed to start provisioning: ${err}`);
|
|
5617
|
+
}
|
|
5618
|
+
const provisionData = (await provisionRes.json()) as { jobId: string };
|
|
5619
|
+
const jobId = provisionData.jobId;
|
|
5620
|
+
|
|
5621
|
+
// Step 3: Poll job status
|
|
5622
|
+
const deadline = Date.now() + 120_000; // 2 minute timeout
|
|
5623
|
+
while (Date.now() < deadline) {
|
|
5624
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
5625
|
+
|
|
5626
|
+
const jobRes = await fetch(`${cloudApiBase}/api/v1/jobs/${jobId}`, {
|
|
5627
|
+
headers,
|
|
5628
|
+
});
|
|
5629
|
+
if (!jobRes.ok) continue;
|
|
5630
|
+
|
|
5631
|
+
const jobData = (await jobRes.json()) as {
|
|
5632
|
+
status: string;
|
|
5633
|
+
result?: { bridgeUrl?: string };
|
|
5634
|
+
error?: string;
|
|
5635
|
+
};
|
|
5636
|
+
|
|
5637
|
+
if (jobData.status === "completed" && jobData.result?.bridgeUrl) {
|
|
5638
|
+
onProgress?.("ready", "Sandbox ready!");
|
|
5639
|
+
return { bridgeUrl: jobData.result.bridgeUrl, agentId };
|
|
5640
|
+
}
|
|
5641
|
+
|
|
5642
|
+
if (jobData.status === "failed") {
|
|
5643
|
+
throw new Error(
|
|
5644
|
+
`Provisioning failed: ${jobData.error ?? "Unknown error"}`,
|
|
5645
|
+
);
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
onProgress?.("provisioning", `Status: ${jobData.status}...`);
|
|
5649
|
+
}
|
|
5650
|
+
|
|
5651
|
+
throw new Error("Provisioning timed out after 2 minutes");
|
|
5652
|
+
}
|
|
5522
5653
|
}
|
|
5523
5654
|
|
|
5655
|
+
/** @deprecated Use MiladyClient directly. */
|
|
5656
|
+
export type ElizaClient = MiladyClient;
|
|
5657
|
+
|
|
5524
5658
|
// Singleton
|
|
5525
5659
|
export const client = new MiladyClient();
|
|
@@ -90,7 +90,7 @@ export function CharacterRoster({
|
|
|
90
90
|
|
|
91
91
|
return (
|
|
92
92
|
<div
|
|
93
|
-
className="
|
|
93
|
+
className="ce-roster"
|
|
94
94
|
data-testid={`${testIdPrefix}-roster-grid`}
|
|
95
95
|
>
|
|
96
96
|
{entries.map((entry) => {
|
|
@@ -100,26 +100,16 @@ export function CharacterRoster({
|
|
|
100
100
|
<button
|
|
101
101
|
key={entry.id}
|
|
102
102
|
type="button"
|
|
103
|
-
className={`
|
|
104
|
-
isSelected
|
|
105
|
-
? "z-100 scale-[1.00] opacity-100"
|
|
106
|
-
: "scale-[1.00] opacity-70 hover:scale-[1.00] hover:opacity-100"
|
|
107
|
-
}`}
|
|
103
|
+
className={`ce-roster-card ${isSelected ? "ce-roster-card--active" : ""}`}
|
|
108
104
|
onClick={() => onSelect(entry)}
|
|
109
105
|
data-testid={`${testIdPrefix}-preset-${entry.id}`}
|
|
110
106
|
>
|
|
111
107
|
<div
|
|
112
|
-
className={`
|
|
113
|
-
isSelected
|
|
114
|
-
? "bg-yellow-400 shadow-[0_0_28px_rgba(250,204,21,0.32)]"
|
|
115
|
-
: useWhiteBorders
|
|
116
|
-
? "bg-white/10 hover:bg-white/35"
|
|
117
|
-
: "bg-border/20 hover:bg-border/60"
|
|
118
|
-
}`}
|
|
108
|
+
className={`ce-roster-card-frame ${isSelected ? "ce-roster-card-frame--active" : ""}`}
|
|
119
109
|
style={{ clipPath: SLANT_CLIP }}
|
|
120
110
|
>
|
|
121
111
|
<div
|
|
122
|
-
className="
|
|
112
|
+
className="ce-roster-card-inner"
|
|
123
113
|
style={{ clipPath: SLANT_CLIP }}
|
|
124
114
|
>
|
|
125
115
|
{isSelected && (
|
|
@@ -132,19 +122,11 @@ export function CharacterRoster({
|
|
|
132
122
|
src={getVrmPreviewUrl(entry.avatarIndex)}
|
|
133
123
|
alt={entry.name}
|
|
134
124
|
draggable={false}
|
|
135
|
-
className={`
|
|
136
|
-
isSelected
|
|
137
|
-
? "scale-[1.04]"
|
|
138
|
-
: "scale-100 group-hover:scale-[1.02]"
|
|
139
|
-
}`}
|
|
125
|
+
className={`ce-roster-card-img ${isSelected ? "ce-roster-card-img--active" : ""}`}
|
|
140
126
|
/>
|
|
141
|
-
<div className="
|
|
127
|
+
<div className="ce-roster-card-label">
|
|
142
128
|
<div
|
|
143
|
-
className={`
|
|
144
|
-
isSelected
|
|
145
|
-
? "bg-black/78 shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]"
|
|
146
|
-
: "bg-black/62"
|
|
147
|
-
}`}
|
|
129
|
+
className={`ce-roster-card-name ${isSelected ? "ce-roster-card-name--active" : ""}`}
|
|
148
130
|
style={{ clipPath: INSET_CLIP }}
|
|
149
131
|
>
|
|
150
132
|
{entry.name}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Button } from "@elizaos/ui";
|
|
2
|
-
import { useBranding } from "../config/branding";
|
|
3
2
|
import { useApp } from "../state";
|
|
4
3
|
|
|
5
4
|
export type CloudSourceMode = "cloud" | "own-key";
|
|
@@ -7,7 +6,7 @@ export type CloudSourceMode = "cloud" | "own-key";
|
|
|
7
6
|
export function CloudSourceModeToggle({
|
|
8
7
|
mode,
|
|
9
8
|
onChange,
|
|
10
|
-
cloudLabel,
|
|
9
|
+
cloudLabel = "Eliza Cloud",
|
|
11
10
|
ownKeyLabel = "Own API Key",
|
|
12
11
|
}: {
|
|
13
12
|
mode: CloudSourceMode;
|
|
@@ -15,8 +14,7 @@ export function CloudSourceModeToggle({
|
|
|
15
14
|
cloudLabel?: string;
|
|
16
15
|
ownKeyLabel?: string;
|
|
17
16
|
}) {
|
|
18
|
-
const
|
|
19
|
-
const resolvedCloudLabel = cloudLabel ?? branding.cloudName;
|
|
17
|
+
const resolvedCloudLabel = cloudLabel;
|
|
20
18
|
return (
|
|
21
19
|
<div className="inline-flex overflow-hidden rounded-lg border border-[var(--border)] bg-[var(--card)] shadow-sm">
|
|
22
20
|
<Button
|
|
@@ -59,9 +57,8 @@ export function CloudConnectionStatus({
|
|
|
59
57
|
disconnectedText: string;
|
|
60
58
|
}) {
|
|
61
59
|
const { t } = useApp();
|
|
62
|
-
const branding = useBranding();
|
|
63
60
|
const resolvedConnectedText =
|
|
64
|
-
connectedText ??
|
|
61
|
+
connectedText ?? "Connected to Eliza Cloud";
|
|
65
62
|
return (
|
|
66
63
|
<div className="flex items-center justify-between py-2.5 px-3 border border-[var(--border)] bg-[var(--bg-muted)]">
|
|
67
64
|
{connected ? (
|
|
@@ -415,6 +415,19 @@ function CompanionSceneSurface({
|
|
|
415
415
|
});
|
|
416
416
|
}, [tab]);
|
|
417
417
|
|
|
418
|
+
/* ── Preload all VRM files into browser cache for instant character swaps ── */
|
|
419
|
+
const preloadedRef = useRef(false);
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
if (preloadedRef.current || preloadAvatars.length === 0) return;
|
|
422
|
+
preloadedRef.current = true;
|
|
423
|
+
for (const entry of preloadAvatars) {
|
|
424
|
+
// Fire-and-forget fetch to warm browser cache; low priority.
|
|
425
|
+
void fetch(entry.vrmPath, { priority: "low" } as RequestInit).catch(
|
|
426
|
+
() => {},
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}, [preloadAvatars]);
|
|
430
|
+
|
|
418
431
|
return (
|
|
419
432
|
<div
|
|
420
433
|
ref={rootRef}
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
defaultRegistry,
|
|
18
18
|
type JsonSchemaObject,
|
|
19
19
|
} from "../config";
|
|
20
|
-
import { DEFAULT_BRANDING, useBranding } from "../config/branding";
|
|
21
20
|
import { useApp } from "../state";
|
|
22
21
|
import type { ConfigUiHint } from "../types";
|
|
23
22
|
import {
|
|
@@ -64,13 +63,12 @@ function CloudRpcStatus({
|
|
|
64
63
|
onLogin,
|
|
65
64
|
}: CloudRpcStatusProps) {
|
|
66
65
|
const { t, setState, setTab } = useApp();
|
|
67
|
-
const branding = useBranding();
|
|
68
66
|
if (connected) {
|
|
69
67
|
return (
|
|
70
68
|
<div className="flex items-center gap-2 text-xs">
|
|
71
69
|
<span className="inline-block w-2 h-2 rounded-full bg-[var(--ok,#16a34a)]" />
|
|
72
70
|
<span className="font-semibold">
|
|
73
|
-
|
|
71
|
+
Connected to Eliza Cloud
|
|
74
72
|
</span>
|
|
75
73
|
{credits !== null && (
|
|
76
74
|
<span className="text-[var(--muted)] ml-auto">
|
|
@@ -107,7 +105,7 @@ function CloudRpcStatus({
|
|
|
107
105
|
<div className="flex items-center gap-2 text-xs">
|
|
108
106
|
<span className="inline-block w-2 h-2 rounded-full bg-[var(--muted)]" />
|
|
109
107
|
<span className="text-[var(--muted)]">
|
|
110
|
-
|
|
108
|
+
Requires Eliza Cloud
|
|
111
109
|
</span>
|
|
112
110
|
</div>
|
|
113
111
|
<button
|
|
@@ -197,7 +195,6 @@ function RpcConfigSection<T extends string>({
|
|
|
197
195
|
cloud,
|
|
198
196
|
containerClassName,
|
|
199
197
|
}: RpcSectionProps<T>) {
|
|
200
|
-
const branding = useBranding();
|
|
201
198
|
const rpcConfig = buildRpcRendererConfig(
|
|
202
199
|
selectedProvider,
|
|
203
200
|
providerConfigs,
|
|
@@ -217,7 +214,7 @@ function RpcConfigSection<T extends string>({
|
|
|
217
214
|
(key: string) => {
|
|
218
215
|
// hack to get t function without breaking hook rules
|
|
219
216
|
return key === "elizaclouddashboard.ElizaCloud"
|
|
220
|
-
?
|
|
217
|
+
? "Eliza Cloud"
|
|
221
218
|
: key;
|
|
222
219
|
},
|
|
223
220
|
)}
|
|
@@ -294,27 +291,27 @@ const CLOUD_SERVICE_DEFS: {
|
|
|
294
291
|
{
|
|
295
292
|
key: "inference",
|
|
296
293
|
label: "Model Inference",
|
|
297
|
-
description: `Use
|
|
294
|
+
description: `Use Eliza Cloud for LLM calls. Turn off to use your own API keys (Anthropic, OpenAI, etc.)`,
|
|
298
295
|
},
|
|
299
296
|
{
|
|
300
297
|
key: "rpc",
|
|
301
298
|
label: "Blockchain RPC",
|
|
302
|
-
description: `Use
|
|
299
|
+
description: `Use Eliza Cloud RPC endpoints for EVM, BSC, and Solana`,
|
|
303
300
|
},
|
|
304
301
|
{
|
|
305
302
|
key: "media",
|
|
306
303
|
label: "Media Generation",
|
|
307
|
-
description: `Use
|
|
304
|
+
description: `Use Eliza Cloud for image, video, audio, and vision`,
|
|
308
305
|
},
|
|
309
306
|
{
|
|
310
307
|
key: "tts",
|
|
311
308
|
label: "Text-to-Speech",
|
|
312
|
-
description: `Use
|
|
309
|
+
description: `Use Eliza Cloud for TTS voice synthesis`,
|
|
313
310
|
},
|
|
314
311
|
{
|
|
315
312
|
key: "embeddings",
|
|
316
313
|
label: "Embeddings",
|
|
317
|
-
description: `Use
|
|
314
|
+
description: `Use Eliza Cloud for text embedding generation`,
|
|
318
315
|
},
|
|
319
316
|
];
|
|
320
317
|
|
|
@@ -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>
|
|
@@ -83,6 +83,8 @@ export const VrmStage = memo(function VrmStage({
|
|
|
83
83
|
const [loaderFading, setLoaderFading] = useState(false);
|
|
84
84
|
const [loaderHidden, setLoaderHidden] = useState(false);
|
|
85
85
|
const loaderFadingStartedRef = useRef(false);
|
|
86
|
+
/** After the first successful VRM load, suppress the loader on subsequent swaps. */
|
|
87
|
+
const hasLoadedFirstVrmRef = useRef(false);
|
|
86
88
|
|
|
87
89
|
const chatAvatarVoice = useChatAvatarVoiceState();
|
|
88
90
|
|
|
@@ -142,6 +144,7 @@ export const VrmStage = memo(function VrmStage({
|
|
|
142
144
|
if (state.vrmLoaded) {
|
|
143
145
|
setVrmLoaded(true);
|
|
144
146
|
setShowVrmFallback(false);
|
|
147
|
+
hasLoadedFirstVrmRef.current = true;
|
|
145
148
|
if (!loaderFadingStartedRef.current) {
|
|
146
149
|
loaderFadingStartedRef.current = true;
|
|
147
150
|
setLoaderFading(true);
|
|
@@ -173,13 +176,17 @@ export const VrmStage = memo(function VrmStage({
|
|
|
173
176
|
if (vrmPath === prevVrmPathRef.current && hasMountedRef.current) return;
|
|
174
177
|
prevVrmPathRef.current = vrmPath;
|
|
175
178
|
if (hasMountedRef.current) {
|
|
176
|
-
// Avatar changed — reset loading state but NOT the world
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
// Avatar changed — reset loading state but NOT the world.
|
|
180
|
+
// After the first successful VRM load, keep the loader hidden so
|
|
181
|
+
// subsequent character swaps feel instant (no flash of loading bar).
|
|
182
|
+
if (!hasLoadedFirstVrmRef.current) {
|
|
183
|
+
setVrmLoaded(false);
|
|
184
|
+
setShowVrmFallback(false);
|
|
185
|
+
setLoadingProgress(undefined);
|
|
186
|
+
setLoaderFading(false);
|
|
187
|
+
setLoaderHidden(false);
|
|
188
|
+
loaderFadingStartedRef.current = false;
|
|
189
|
+
}
|
|
183
190
|
}
|
|
184
191
|
hasMountedRef.current = true;
|
|
185
192
|
}, [vrmPath]);
|
|
@@ -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 */
|
|
@@ -4,11 +4,12 @@ import type {
|
|
|
4
4
|
ProviderOption,
|
|
5
5
|
} from "@elizaos/app-core/api";
|
|
6
6
|
import { client } from "@elizaos/app-core/api";
|
|
7
|
+
import { useBranding } from "@elizaos/app-core/config";
|
|
7
8
|
import { isNative } from "@elizaos/app-core/platform";
|
|
8
9
|
import { getProviderLogo } from "@elizaos/app-core/providers";
|
|
9
10
|
import { useApp } from "@elizaos/app-core/state";
|
|
10
11
|
import { openExternalUrl } from "@elizaos/app-core/utils";
|
|
11
|
-
import { useState } from "react";
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
12
13
|
|
|
13
14
|
function formatRequestError(err: unknown): string {
|
|
14
15
|
if (err instanceof Error) {
|
|
@@ -46,6 +47,8 @@ export function ConnectionStep() {
|
|
|
46
47
|
t,
|
|
47
48
|
} = useApp();
|
|
48
49
|
|
|
50
|
+
const branding = useBranding();
|
|
51
|
+
|
|
49
52
|
const [openaiOAuthStarted, setOpenaiOAuthStarted] = useState(false);
|
|
50
53
|
const [openaiCallbackUrl, setOpenaiCallbackUrl] = useState("");
|
|
51
54
|
const [openaiConnected, setOpenaiConnected] = useState(false);
|
|
@@ -148,7 +151,22 @@ export function ConnectionStep() {
|
|
|
148
151
|
setState("onboardingOpenRouterModel", modelId);
|
|
149
152
|
};
|
|
150
153
|
|
|
151
|
-
const
|
|
154
|
+
const catalogProviders = onboardingOptions?.providers ?? [];
|
|
155
|
+
// Merge custom providers from branding (app-injected) with the catalog.
|
|
156
|
+
// Custom providers are appended; duplicates (by id) are skipped.
|
|
157
|
+
const customProviders = branding.customProviders ?? [];
|
|
158
|
+
const catalogIds = new Set(catalogProviders.map((p: ProviderOption) => p.id));
|
|
159
|
+
const providers = [
|
|
160
|
+
...catalogProviders,
|
|
161
|
+
...customProviders.filter((cp) => !catalogIds.has(cp.id as never)),
|
|
162
|
+
] as ProviderOption[];
|
|
163
|
+
// Build a map of custom provider logos for getProviderLogo lookups.
|
|
164
|
+
const customLogoMap = new Map(
|
|
165
|
+
customProviders
|
|
166
|
+
.filter((cp) => cp.logoDark || cp.logoLight)
|
|
167
|
+
.map((cp) => [cp.id, { logoDark: cp.logoDark, logoLight: cp.logoLight }]),
|
|
168
|
+
);
|
|
169
|
+
const getCustomLogo = (id: string) => customLogoMap.get(id);
|
|
152
170
|
const elizaCloudReady =
|
|
153
171
|
elizaCloudConnected ||
|
|
154
172
|
(onboardingRunMode === "cloud" &&
|
|
@@ -275,6 +293,20 @@ export function ConnectionStep() {
|
|
|
275
293
|
}
|
|
276
294
|
};
|
|
277
295
|
|
|
296
|
+
// On native (mobile) or when branding requires cloud-only, force sandbox mode
|
|
297
|
+
// — skip straight to Eliza Cloud login. These platforms cannot run a local
|
|
298
|
+
// backend, so cloud is the only option.
|
|
299
|
+
const forceCloud = isNative || branding.cloudOnly;
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (forceCloud && !onboardingRunMode) {
|
|
302
|
+
setState("onboardingRunMode", "cloud");
|
|
303
|
+
setState("onboardingCloudProvider", "elizacloud");
|
|
304
|
+
setState("onboardingProvider", "");
|
|
305
|
+
setState("onboardingApiKey", "");
|
|
306
|
+
setState("onboardingPrimaryModel", "");
|
|
307
|
+
}
|
|
308
|
+
}, [forceCloud, onboardingRunMode, setState]);
|
|
309
|
+
|
|
278
310
|
if (!showProviderSelection) {
|
|
279
311
|
if (!onboardingRunMode) {
|
|
280
312
|
return (
|
|
@@ -289,7 +321,7 @@ export function ConnectionStep() {
|
|
|
289
321
|
{t("onboarding.hostingQuestion")}
|
|
290
322
|
</div>
|
|
291
323
|
<div className="onboarding-provider-grid">
|
|
292
|
-
{!isNative && (
|
|
324
|
+
{!isNative && !branding.cloudOnly && (
|
|
293
325
|
<button
|
|
294
326
|
type="button"
|
|
295
327
|
className="onboarding-provider-card onboarding-provider-card--recommended"
|
|
@@ -730,7 +762,7 @@ export function ConnectionStep() {
|
|
|
730
762
|
onClick={() => handleProviderSelect(p.id)}
|
|
731
763
|
>
|
|
732
764
|
<img
|
|
733
|
-
src={getProviderLogo(p.id, false)}
|
|
765
|
+
src={getProviderLogo(p.id, false, getCustomLogo(p.id))}
|
|
734
766
|
alt={display.name}
|
|
735
767
|
className="onboarding-provider-icon"
|
|
736
768
|
/>
|
|
@@ -795,7 +827,7 @@ export function ConnectionStep() {
|
|
|
795
827
|
>
|
|
796
828
|
{selectedProvider && (
|
|
797
829
|
<img
|
|
798
|
-
src={getProviderLogo(selectedProvider.id, false)}
|
|
830
|
+
src={getProviderLogo(selectedProvider.id, false, getCustomLogo(selectedProvider.id))}
|
|
799
831
|
alt={selectedDisplay.name}
|
|
800
832
|
className="onboarding-provider-icon"
|
|
801
833
|
style={{ width: "1.5rem", height: "1.5rem" }}
|
|
@@ -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
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Custom provider that apps can inject into the onboarding flow.
|
|
5
|
+
* Uses `string` for id/family so apps aren't restricted to the built-in union.
|
|
6
|
+
*/
|
|
7
|
+
export interface CustomProviderOption {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
envKey: string | null;
|
|
11
|
+
pluginName: string;
|
|
12
|
+
keyPrefix: string | null;
|
|
13
|
+
description: string;
|
|
14
|
+
family: string;
|
|
15
|
+
authMode: "api-key" | "cloud" | "credentials" | "local" | "subscription";
|
|
16
|
+
group: "cloud" | "local" | "subscription";
|
|
17
|
+
order: number;
|
|
18
|
+
recommended?: boolean;
|
|
19
|
+
/** Dark-mode logo path (e.g. "/logos/my-provider.png") */
|
|
20
|
+
logoDark?: string;
|
|
21
|
+
/** Light-mode logo path */
|
|
22
|
+
logoLight?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
3
25
|
export interface BrandingConfig {
|
|
4
26
|
/** Product name shown in UI ("Eliza" | "Milady") */
|
|
5
27
|
appName: string;
|
|
6
|
-
/** Cloud service name ("Eliza Cloud" | "Milady Cloud") */
|
|
7
|
-
cloudName: string;
|
|
8
28
|
/** GitHub org ("elizaos" | "milady-ai") */
|
|
9
29
|
orgName: string;
|
|
10
30
|
/** GitHub repo name ("eliza" | "milady") */
|
|
@@ -21,11 +41,14 @@ export interface BrandingConfig {
|
|
|
21
41
|
fileExtension: string;
|
|
22
42
|
/** npm package scope ("elizaos" | "miladyai") */
|
|
23
43
|
packageScope: string;
|
|
44
|
+
/** Custom providers injected by the app into the onboarding flow */
|
|
45
|
+
customProviders?: CustomProviderOption[];
|
|
46
|
+
/** When true, the app requires Eliza Cloud — local backend mode is disabled. */
|
|
47
|
+
cloudOnly?: boolean;
|
|
24
48
|
}
|
|
25
49
|
|
|
26
50
|
export const DEFAULT_BRANDING: BrandingConfig = {
|
|
27
51
|
appName: "Eliza",
|
|
28
|
-
cloudName: "Eliza Cloud",
|
|
29
52
|
orgName: "elizaos",
|
|
30
53
|
repoName: "eliza",
|
|
31
54
|
docsUrl: "https://docs.elizaos.ai",
|