@elizaos/app-core 2.0.0-alpha.68 → 2.0.0-alpha.69
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/actions/character.ts +197 -15
- package/src/components/CharacterView.tsx +90 -45
- package/src/components/FlaminaGuide.test.tsx +61 -0
- package/src/components/FlaminaGuide.tsx +212 -0
- package/src/components/MiladyBarSettings.tsx +872 -0
- package/src/components/onboarding/ConnectionStep.tsx +3 -4
- package/src/events/index.ts +8 -0
- package/src/hooks/useMiladyBar.ts +594 -0
- package/src/shell-params.test.ts +48 -0
- package/src/shell-params.ts +36 -0
- package/src/state/AppContext.tsx +266 -8
- package/test/app/app-context-autonomy-events.test.ts +82 -1
- package/test/app/character-customization.e2e.test.ts +61 -7
- package/test/app/character-save-journey.test.ts +1245 -0
- package/test/app/chat-journey.test.ts +1075 -0
- package/test/app/memory-monitor.test.ts +7 -8
- package/test/app/milady-bar-regression.test.tsx +519 -0
- package/test/app/milady-bar-settings.test.tsx +1056 -0
- package/test/app/milady-bar.test.tsx +462 -251
- package/test/app/onboarding-e2e-journey.test.ts +1409 -0
- package/test/app/onboarding-finish-lock.test.ts +2 -2
- package/test/app/pages-navigation-smoke.e2e.test.ts +9 -1
- package/test/app/restart-banner.test.tsx +13 -5
- package/test/app/shell-mode-switching.e2e.test.ts +1 -0
- package/test/app/startup-chat.e2e.test.ts +2 -0
- package/test/app/startup-onboarding.e2e.test.ts +103 -9
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.69",
|
|
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.69",
|
|
72
|
+
"@elizaos/ui": "2.0.0-alpha.69",
|
|
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": "774937c9cbefd3d95fc181ca40295bb4fd5a7d3b"
|
|
91
91
|
}
|
package/src/actions/character.ts
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import type { CharacterData, ElizaClient } from "../api/client";
|
|
8
8
|
|
|
9
|
+
type MessageExampleGroup = {
|
|
10
|
+
examples: Array<{ name: string; content: { text: string } }>;
|
|
11
|
+
};
|
|
12
|
+
|
|
9
13
|
export interface CharacterActionContext {
|
|
10
14
|
client: ElizaClient;
|
|
11
15
|
setCharacterData: (data: CharacterData | null) => void;
|
|
@@ -51,6 +55,165 @@ export async function loadCharacter(
|
|
|
51
55
|
ctx.setCharacterLoading(false);
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
function extractLikelyJson(input: string): string {
|
|
59
|
+
const trimmed = input.trim();
|
|
60
|
+
if (!trimmed) return trimmed;
|
|
61
|
+
|
|
62
|
+
const withoutFences = trimmed
|
|
63
|
+
.replace(/^```(?:json)?\s*/i, "")
|
|
64
|
+
.replace(/\s*```$/i, "")
|
|
65
|
+
.trim();
|
|
66
|
+
if (withoutFences.startsWith("{") || withoutFences.startsWith("[")) {
|
|
67
|
+
return withoutFences;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const firstBracket = withoutFences.indexOf("[");
|
|
71
|
+
const firstBrace = withoutFences.indexOf("{");
|
|
72
|
+
const starts = [firstBracket, firstBrace].filter((index) => index >= 0);
|
|
73
|
+
if (starts.length === 0) return withoutFences;
|
|
74
|
+
|
|
75
|
+
const start = Math.min(...starts);
|
|
76
|
+
const opener = withoutFences[start];
|
|
77
|
+
const closer = opener === "[" ? "]" : "}";
|
|
78
|
+
const end = withoutFences.lastIndexOf(closer);
|
|
79
|
+
if (end <= start) return withoutFences;
|
|
80
|
+
|
|
81
|
+
return withoutFences.slice(start, end + 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeSpeakerName(
|
|
85
|
+
rawName: unknown,
|
|
86
|
+
fallbackAgentName: string,
|
|
87
|
+
options: { fallbackMissingSpeaker?: boolean } = {},
|
|
88
|
+
): string {
|
|
89
|
+
const fallbackMissingSpeaker = options.fallbackMissingSpeaker ?? true;
|
|
90
|
+
if (typeof rawName === "string" && rawName.trim()) {
|
|
91
|
+
const trimmed = rawName.trim();
|
|
92
|
+
const normalized = trimmed.toLowerCase();
|
|
93
|
+
if (
|
|
94
|
+
normalized === "assistant" ||
|
|
95
|
+
normalized === "agent" ||
|
|
96
|
+
normalized === "ai" ||
|
|
97
|
+
normalized === "model" ||
|
|
98
|
+
normalized === "{{agentname}}"
|
|
99
|
+
) {
|
|
100
|
+
return fallbackAgentName;
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
normalized === "user" ||
|
|
104
|
+
normalized === "human" ||
|
|
105
|
+
normalized === "{{user}}" ||
|
|
106
|
+
normalized === "customer"
|
|
107
|
+
) {
|
|
108
|
+
return "{{user1}}";
|
|
109
|
+
}
|
|
110
|
+
return trimmed;
|
|
111
|
+
}
|
|
112
|
+
return fallbackMissingSpeaker ? fallbackAgentName : "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeMessageText(raw: unknown): string {
|
|
116
|
+
if (typeof raw === "string") return raw.trim();
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeConversation(
|
|
121
|
+
conversation: unknown,
|
|
122
|
+
fallbackAgentName: string,
|
|
123
|
+
options: { fallbackMissingSpeaker?: boolean } = {},
|
|
124
|
+
): MessageExampleGroup | null {
|
|
125
|
+
const rawExamples = Array.isArray(conversation)
|
|
126
|
+
? conversation
|
|
127
|
+
: conversation &&
|
|
128
|
+
typeof conversation === "object" &&
|
|
129
|
+
Array.isArray(
|
|
130
|
+
(conversation as { examples?: unknown[] }).examples,
|
|
131
|
+
)
|
|
132
|
+
? (conversation as { examples: unknown[] }).examples
|
|
133
|
+
: null;
|
|
134
|
+
|
|
135
|
+
if (!rawExamples) return null;
|
|
136
|
+
|
|
137
|
+
const examples = rawExamples
|
|
138
|
+
.map((message) => {
|
|
139
|
+
const record =
|
|
140
|
+
message && typeof message === "object"
|
|
141
|
+
? (message as Record<string, unknown>)
|
|
142
|
+
: null;
|
|
143
|
+
if (!record) return null;
|
|
144
|
+
|
|
145
|
+
const content =
|
|
146
|
+
record.content && typeof record.content === "object"
|
|
147
|
+
? (record.content as Record<string, unknown>)
|
|
148
|
+
: null;
|
|
149
|
+
const text = normalizeMessageText(
|
|
150
|
+
content?.text ?? record.text ?? record.message ?? record.content,
|
|
151
|
+
);
|
|
152
|
+
if (!text) return null;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
name: normalizeSpeakerName(
|
|
156
|
+
record.name ?? record.user ?? record.speaker ?? record.role,
|
|
157
|
+
fallbackAgentName,
|
|
158
|
+
options,
|
|
159
|
+
),
|
|
160
|
+
content: { text },
|
|
161
|
+
};
|
|
162
|
+
})
|
|
163
|
+
.filter(
|
|
164
|
+
(
|
|
165
|
+
message,
|
|
166
|
+
): message is { name: string; content: { text: string } } =>
|
|
167
|
+
Boolean(message?.name && message.content.text),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (examples.length === 0) return null;
|
|
171
|
+
return { examples };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function normalizeGeneratedMessageExamples(
|
|
175
|
+
input: unknown,
|
|
176
|
+
fallbackAgentName = "Agent",
|
|
177
|
+
options: { fallbackMissingSpeaker?: boolean } = {},
|
|
178
|
+
): MessageExampleGroup[] {
|
|
179
|
+
let parsed = input;
|
|
180
|
+
|
|
181
|
+
if (typeof input === "string") {
|
|
182
|
+
const candidate = extractLikelyJson(input);
|
|
183
|
+
try {
|
|
184
|
+
parsed = JSON.parse(candidate);
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const source =
|
|
191
|
+
parsed &&
|
|
192
|
+
typeof parsed === "object" &&
|
|
193
|
+
Array.isArray(
|
|
194
|
+
(parsed as { messageExamples?: unknown[] }).messageExamples,
|
|
195
|
+
)
|
|
196
|
+
? (parsed as { messageExamples: unknown[] }).messageExamples
|
|
197
|
+
: parsed;
|
|
198
|
+
|
|
199
|
+
if (!Array.isArray(source)) return [];
|
|
200
|
+
|
|
201
|
+
const groups =
|
|
202
|
+
source.length > 0 &&
|
|
203
|
+
source.every(
|
|
204
|
+
(entry) =>
|
|
205
|
+
entry && typeof entry === "object" && Array.isArray((entry as { examples?: unknown[] }).examples),
|
|
206
|
+
)
|
|
207
|
+
? source
|
|
208
|
+
: source.every((entry) => Array.isArray(entry))
|
|
209
|
+
? source
|
|
210
|
+
: [source];
|
|
211
|
+
|
|
212
|
+
return groups
|
|
213
|
+
.map((group) => normalizeConversation(group, fallbackAgentName, options))
|
|
214
|
+
.filter((group): group is MessageExampleGroup => Boolean(group));
|
|
215
|
+
}
|
|
216
|
+
|
|
54
217
|
export function prepareDraftForSave(
|
|
55
218
|
draft: CharacterData,
|
|
56
219
|
): Record<string, unknown> {
|
|
@@ -91,24 +254,43 @@ export function prepareDraftForSave(
|
|
|
91
254
|
);
|
|
92
255
|
if (postExamples.length > 0) result.postExamples = postExamples;
|
|
93
256
|
|
|
94
|
-
if (
|
|
95
|
-
Array.isArray(draft.messageExamples) &&
|
|
96
|
-
draft.messageExamples.length > 0
|
|
97
|
-
) {
|
|
257
|
+
if (draft.messageExamples != null) {
|
|
98
258
|
// Strip extra fields from content (schema is .strict() — only text + actions allowed)
|
|
99
|
-
const cleaned =
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
259
|
+
const cleaned = normalizeGeneratedMessageExamples(
|
|
260
|
+
draft.messageExamples,
|
|
261
|
+
draft.name?.trim() || "Agent",
|
|
262
|
+
{ fallbackMissingSpeaker: false },
|
|
263
|
+
).map((group, groupIndex) => ({
|
|
264
|
+
examples: group.examples
|
|
265
|
+
.map((msg) => {
|
|
266
|
+
const originalGroup =
|
|
267
|
+
Array.isArray(draft.messageExamples) &&
|
|
268
|
+
draft.messageExamples[groupIndex] &&
|
|
269
|
+
typeof draft.messageExamples[groupIndex] === "object"
|
|
270
|
+
? (draft.messageExamples[groupIndex] as {
|
|
271
|
+
examples?: Array<{
|
|
272
|
+
name?: string;
|
|
273
|
+
content?: { text?: string; actions?: string[] };
|
|
274
|
+
}>;
|
|
275
|
+
})
|
|
276
|
+
: null;
|
|
277
|
+
const originalMessage = originalGroup?.examples?.find(
|
|
278
|
+
(candidate) =>
|
|
279
|
+
candidate?.name?.trim() === msg.name &&
|
|
280
|
+
candidate?.content?.text?.trim() === msg.content.text,
|
|
281
|
+
);
|
|
282
|
+
return {
|
|
283
|
+
name: msg.name.trim(),
|
|
105
284
|
content: {
|
|
106
|
-
text: msg.content.text,
|
|
107
|
-
...(
|
|
285
|
+
text: msg.content.text.trim(),
|
|
286
|
+
...(originalMessage?.content?.actions
|
|
287
|
+
? { actions: originalMessage.content.actions }
|
|
288
|
+
: {}),
|
|
108
289
|
},
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
290
|
+
};
|
|
291
|
+
})
|
|
292
|
+
.filter((msg) => msg.name && msg.content.text),
|
|
293
|
+
}));
|
|
112
294
|
if (cleaned.length > 0) result.messageExamples = cleaned;
|
|
113
295
|
}
|
|
114
296
|
|
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
dispatchWindowEvent,
|
|
14
14
|
VOICE_CONFIG_UPDATED_EVENT,
|
|
15
15
|
} from "@elizaos/app-core/events";
|
|
16
|
+
import { COMPANION_ENABLED } from "@elizaos/app-core/navigation";
|
|
17
|
+
import { normalizeGeneratedMessageExamples } from "../actions/character";
|
|
16
18
|
import { useApp } from "@elizaos/app-core/state";
|
|
17
19
|
import {
|
|
18
20
|
CharacterRoster,
|
|
19
21
|
type CharacterRosterEntry,
|
|
20
|
-
CHARACTER_PRESET_META,
|
|
21
22
|
resolveRosterEntries,
|
|
22
23
|
} from "./CharacterRoster";
|
|
23
24
|
import {
|
|
@@ -389,7 +390,7 @@ export function CharacterView({
|
|
|
389
390
|
}, [onboardingOptions?.styles]);
|
|
390
391
|
|
|
391
392
|
const characterRoster = resolveRosterEntries(rosterStyles);
|
|
392
|
-
const visibleCharacterRoster = characterRoster
|
|
393
|
+
const visibleCharacterRoster = characterRoster;
|
|
393
394
|
const currentCharacter = hasCharacterContent(characterDraft)
|
|
394
395
|
? characterDraft
|
|
395
396
|
: characterData;
|
|
@@ -533,12 +534,12 @@ export function CharacterView({
|
|
|
533
534
|
dispatchWindowEvent(VOICE_CONFIG_UPDATED_EVENT, normalizedVoiceConfig);
|
|
534
535
|
}, [voiceConfig]);
|
|
535
536
|
|
|
536
|
-
const d = characterDraft;
|
|
537
|
+
const d = characterDraft ?? ({} as Record<string, unknown>);
|
|
537
538
|
const bioText =
|
|
538
539
|
typeof d.bio === "string"
|
|
539
540
|
? d.bio
|
|
540
541
|
: Array.isArray(d.bio)
|
|
541
|
-
? d.bio.join("\n")
|
|
542
|
+
? (d.bio as string[]).join("\n")
|
|
542
543
|
: "";
|
|
543
544
|
|
|
544
545
|
const getCharContext = useCallback(
|
|
@@ -598,23 +599,18 @@ export function CharacterView({
|
|
|
598
599
|
/* raw text fallback */
|
|
599
600
|
}
|
|
600
601
|
} else if (field === "chatExamples") {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
);
|
|
614
|
-
handleFieldEdit("messageExamples", formatted);
|
|
615
|
-
}
|
|
616
|
-
} catch {
|
|
617
|
-
/* raw text fallback */
|
|
602
|
+
const formatted = normalizeGeneratedMessageExamples(
|
|
603
|
+
generated,
|
|
604
|
+
d.name?.trim() || "Agent",
|
|
605
|
+
);
|
|
606
|
+
if (formatted.length > 0) {
|
|
607
|
+
setState("characterSaveError", null);
|
|
608
|
+
handleFieldEdit("messageExamples", formatted);
|
|
609
|
+
} else {
|
|
610
|
+
setState(
|
|
611
|
+
"characterSaveError",
|
|
612
|
+
"Generated chat examples could not be parsed. Try again.",
|
|
613
|
+
);
|
|
618
614
|
}
|
|
619
615
|
} else if (field === "postExamples") {
|
|
620
616
|
try {
|
|
@@ -633,8 +629,15 @@ export function CharacterView({
|
|
|
633
629
|
/* raw text fallback */
|
|
634
630
|
}
|
|
635
631
|
}
|
|
636
|
-
} catch {
|
|
637
|
-
|
|
632
|
+
} catch (err) {
|
|
633
|
+
if (field === "chatExamples") {
|
|
634
|
+
setState(
|
|
635
|
+
"characterSaveError",
|
|
636
|
+
err instanceof Error
|
|
637
|
+
? `Failed to generate chat examples: ${err.message}`
|
|
638
|
+
: "Failed to generate chat examples.",
|
|
639
|
+
);
|
|
640
|
+
}
|
|
638
641
|
}
|
|
639
642
|
setGenerating(null);
|
|
640
643
|
},
|
|
@@ -644,6 +647,7 @@ export function CharacterView({
|
|
|
644
647
|
handleFieldEdit,
|
|
645
648
|
handleStyleEdit,
|
|
646
649
|
handleCharacterArrayInput,
|
|
650
|
+
setState,
|
|
647
651
|
],
|
|
648
652
|
);
|
|
649
653
|
|
|
@@ -802,19 +806,44 @@ export function CharacterView({
|
|
|
802
806
|
]);
|
|
803
807
|
|
|
804
808
|
const handleSaveAll = useCallback(async () => {
|
|
805
|
-
|
|
809
|
+
const trimmedName =
|
|
810
|
+
typeof characterDraft?.name === "string" ? characterDraft.name.trim() : "";
|
|
811
|
+
if (!trimmedName) {
|
|
812
|
+
setVoiceSaveError(null);
|
|
813
|
+
setState("characterSaveSuccess", null);
|
|
814
|
+
setState("characterSaveError", "Character name is required before saving.");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
806
818
|
setVoiceSaveError(null);
|
|
819
|
+
try {
|
|
820
|
+
await handleSaveCharacter();
|
|
821
|
+
} catch {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
setVoiceSaving(true);
|
|
826
|
+
let voiceSaveFailed = false;
|
|
807
827
|
try {
|
|
808
828
|
await persistVoiceConfig();
|
|
809
829
|
} catch (err) {
|
|
830
|
+
voiceSaveFailed = true;
|
|
810
831
|
setVoiceSaveError(
|
|
811
832
|
err instanceof Error ? err.message : "Failed to save voice settings.",
|
|
812
833
|
);
|
|
813
834
|
} finally {
|
|
814
835
|
setVoiceSaving(false);
|
|
815
836
|
}
|
|
816
|
-
|
|
817
|
-
|
|
837
|
+
if (!voiceSaveFailed) {
|
|
838
|
+
setTab(COMPANION_ENABLED ? "companion" : "chat");
|
|
839
|
+
}
|
|
840
|
+
}, [
|
|
841
|
+
characterDraft?.name,
|
|
842
|
+
handleSaveCharacter,
|
|
843
|
+
persistVoiceConfig,
|
|
844
|
+
setState,
|
|
845
|
+
setTab,
|
|
846
|
+
]);
|
|
818
847
|
|
|
819
848
|
/* ── Helpers ────────────────────────────────────────────────────── */
|
|
820
849
|
const cardCls = sceneOverlay
|
|
@@ -839,8 +868,8 @@ export function CharacterView({
|
|
|
839
868
|
const notebookShellCls =
|
|
840
869
|
"relative isolate flex h-[34.75rem] w-full max-w-[29rem] overflow-visible";
|
|
841
870
|
const notebookFrameCls = sceneOverlay
|
|
842
|
-
? "relative flex h-full w-full overflow-hidden rounded-[1.75rem] border border-[#2f261b]/20 bg-[#f8f0df]/70 shadow-[0_24px_60px_rgba(0,0,0,0.34),inset_0_1px_0_rgba(255,255,255,0.5)]"
|
|
843
|
-
: "relative flex h-full w-full overflow-hidden rounded-[1.75rem] border border-[#2f261b]/14 bg-[#f8f0df] shadow-[0_26px_60px_rgba(36,28,18,0.2),inset_0_1px_0_rgba(255,255,255,0.58)]";
|
|
871
|
+
? "relative flex h-full w-full overflow-hidden rounded-[1.75rem] border border-[#2f261b]/20 bg-[#f8f0df]/70 dark:border-white/10 dark:bg-[#1c1c1e]/90 shadow-[0_24px_60px_rgba(0,0,0,0.34),inset_0_1px_0_rgba(255,255,255,0.5)]"
|
|
872
|
+
: "relative flex h-full w-full overflow-hidden rounded-[1.75rem] border border-[#2f261b]/14 bg-[#f8f0df] dark:border-white/10 dark:bg-[#1c1c1e] shadow-[0_26px_60px_rgba(36,28,18,0.2),inset_0_1px_0_rgba(255,255,255,0.58)]";
|
|
844
873
|
|
|
845
874
|
const handleCustomVrmUpload = useCallback(
|
|
846
875
|
(file: File) => {
|
|
@@ -872,7 +901,11 @@ export function CharacterView({
|
|
|
872
901
|
PREMADE_VOICES.find((preset) => preset.id === selectedVoicePresetId) ??
|
|
873
902
|
null;
|
|
874
903
|
const voiceSelectValue = selectedVoicePresetId ?? null;
|
|
875
|
-
const combinedSaveError =
|
|
904
|
+
const combinedSaveError = characterSaveError;
|
|
905
|
+
const saveAdvisory =
|
|
906
|
+
characterSaveSuccess && voiceSaveError
|
|
907
|
+
? `Character saved. Voice settings still need attention: ${voiceSaveError}`
|
|
908
|
+
: null;
|
|
876
909
|
const customizationActionLabel = customOverridesEnabled
|
|
877
910
|
? t("characterview.backToCharacterSelect")
|
|
878
911
|
: t("characterview.customize");
|
|
@@ -995,16 +1028,16 @@ export function CharacterView({
|
|
|
995
1028
|
className={notebookShellCls}
|
|
996
1029
|
data-testid="character-notebook"
|
|
997
1030
|
>
|
|
998
|
-
<div className="pointer-events-none absolute inset-x-5 bottom-[-1.2rem] top-6 rounded-[1.9rem] bg-[linear-gradient(180deg,rgba(35,27,18,0.12)_0%,rgba(16,12,9,0.32)_100%)] blur-xl" />
|
|
999
|
-
<div className="pointer-events-none absolute inset-[0.35rem] translate-x-2 translate-y-2 rounded-[1.9rem] border border-[#d7c5a4]/50 bg-[linear-gradient(180deg,rgba(251,246,235,0.86)_0%,rgba(236,224,198,0.7)_100%)] shadow-[0_12px_26px_rgba(62,44,21,0.12)]" />
|
|
1031
|
+
<div className="pointer-events-none absolute inset-x-5 bottom-[-1.2rem] top-6 rounded-[1.9rem] bg-[linear-gradient(180deg,rgba(35,27,18,0.12)_0%,rgba(16,12,9,0.32)_100%)] dark:bg-[linear-gradient(180deg,rgba(0,0,0,0.3)_0%,rgba(0,0,0,0.5)_100%)] blur-xl" />
|
|
1032
|
+
<div className="pointer-events-none absolute inset-[0.35rem] translate-x-2 translate-y-2 rounded-[1.9rem] border border-[#d7c5a4]/50 bg-[linear-gradient(180deg,rgba(251,246,235,0.86)_0%,rgba(236,224,198,0.7)_100%)] dark:border-white/10 dark:bg-[linear-gradient(180deg,rgba(38,38,40,0.9)_0%,rgba(28,28,30,0.8)_100%)] shadow-[0_12px_26px_rgba(62,44,21,0.12)]" />
|
|
1000
1033
|
<div className="pointer-events-none absolute bottom-[-0.95rem] left-8 h-14 w-4 rounded-b-sm bg-[linear-gradient(180deg,#ddb45e_0%,#be8530_100%)] [clip-path:polygon(0_0,100%_0,100%_78%,50%_100%,0_78%)] shadow-[0_10px_20px_rgba(141,97,34,0.28)]" />
|
|
1001
1034
|
<div className={notebookFrameCls}>
|
|
1002
|
-
<div className="pointer-events-none absolute left-4 top-4 h-2 w-2 rounded-full border border-[#d8c295]/70 bg-[#fff9eb]/85 shadow-[0_0_0_2px_rgba(150,116,61,0.08)]" />
|
|
1003
|
-
<div className="pointer-events-none absolute bottom-4 left-4 h-2 w-2 rounded-full border border-[#d8c295]/70 bg-[#fff9eb]/85 shadow-[0_0_0_2px_rgba(150,116,61,0.08)]" />
|
|
1035
|
+
<div className="pointer-events-none absolute left-4 top-4 h-2 w-2 rounded-full border border-[#d8c295]/70 bg-[#fff9eb]/85 shadow-[0_0_0_2px_rgba(150,116,61,0.08)] dark:border-white/20 dark:bg-white/10 dark:shadow-[0_0_0_2px_rgba(255,255,255,0.05)]" />
|
|
1036
|
+
<div className="pointer-events-none absolute bottom-4 left-4 h-2 w-2 rounded-full border border-[#d8c295]/70 bg-[#fff9eb]/85 shadow-[0_0_0_2px_rgba(150,116,61,0.08)] dark:border-white/20 dark:bg-white/10 dark:shadow-[0_0_0_2px_rgba(255,255,255,0.05)]" />
|
|
1004
1037
|
<div className="pointer-events-none absolute right-4 top-4 h-2 w-2 rounded-full border border-white/10 bg-white/10 shadow-[0_0_0_2px_rgba(255,255,255,0.03)]" />
|
|
1005
1038
|
<div className="pointer-events-none absolute bottom-4 right-4 h-2 w-2 rounded-full border border-white/10 bg-white/10 shadow-[0_0_0_2px_rgba(255,255,255,0.03)]" />
|
|
1006
|
-
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-14 bg-[linear-gradient(180deg,rgba(0,0,0,0)_0%,rgba(104,77,39,0.07)_100%)]" />
|
|
1007
|
-
<div className="pointer-events-none absolute bottom-3 right-[4.95rem] top-3 w-[0.7rem] rounded-full bg-[linear-gradient(90deg,rgba(116,92,55,0.16)_0%,rgba(255,255,255,0.5)_45%,rgba(112,88,52,0.2)_100%)] shadow-[inset_0_0_7px_rgba(89,67,37,0.16),0_0_18px_rgba(255,246,220,0.16)]" />
|
|
1039
|
+
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-14 bg-[linear-gradient(180deg,rgba(0,0,0,0)_0%,rgba(104,77,39,0.07)_100%)] dark:bg-[linear-gradient(180deg,rgba(0,0,0,0)_0%,rgba(0,0,0,0.15)_100%)]" />
|
|
1040
|
+
<div className="pointer-events-none absolute bottom-3 right-[4.95rem] top-3 w-[0.7rem] rounded-full bg-[linear-gradient(90deg,rgba(116,92,55,0.16)_0%,rgba(255,255,255,0.5)_45%,rgba(112,88,52,0.2)_100%)] shadow-[inset_0_0_7px_rgba(89,67,37,0.16),0_0_18px_rgba(255,246,220,0.16)] dark:bg-[linear-gradient(90deg,rgba(255,255,255,0.05)_0%,rgba(255,255,255,0.15)_45%,rgba(255,255,255,0.05)_100%)] dark:shadow-[inset_0_0_7px_rgba(0,0,0,0.3),0_0_18px_rgba(255,255,255,0.05)]" />
|
|
1008
1041
|
{/* ── Book page (left) ── */}
|
|
1009
1042
|
<div
|
|
1010
1043
|
className={`${bookPageCls} flex flex-1 flex-col rounded-l-[1.75rem] border-r border-[#cdbb98]/60 text-[#1e2329] dark:border-white/[0.08] dark:text-[hsl(40,10%,84%)]`}
|
|
@@ -1411,13 +1444,18 @@ export function CharacterView({
|
|
|
1411
1444
|
)}
|
|
1412
1445
|
|
|
1413
1446
|
<div className={`${sectionCls} relative z-10 ${sceneOverlay ? "character-action-bar" : ""}`}>
|
|
1414
|
-
{(characterSaveSuccess || combinedSaveError) && (
|
|
1447
|
+
{(characterSaveSuccess || combinedSaveError || saveAdvisory) && (
|
|
1415
1448
|
<div className="mb-3 flex flex-wrap items-center justify-center gap-2">
|
|
1416
1449
|
{characterSaveSuccess && (
|
|
1417
1450
|
<span className="rounded-lg border border-green-400/20 bg-green-400/10 px-3 py-1.5 text-xs font-bold text-green-400">
|
|
1418
1451
|
{characterSaveSuccess}
|
|
1419
1452
|
</span>
|
|
1420
1453
|
)}
|
|
1454
|
+
{saveAdvisory && (
|
|
1455
|
+
<span className="rounded-lg border border-amber-400/20 bg-amber-400/10 px-3 py-1.5 text-xs font-medium text-amber-300">
|
|
1456
|
+
{saveAdvisory}
|
|
1457
|
+
</span>
|
|
1458
|
+
)}
|
|
1421
1459
|
{combinedSaveError && (
|
|
1422
1460
|
<span className="rounded-lg border border-danger/20 bg-danger/10 px-3 py-1.5 text-xs font-medium text-danger">
|
|
1423
1461
|
{combinedSaveError}
|
|
@@ -1426,8 +1464,8 @@ export function CharacterView({
|
|
|
1426
1464
|
</div>
|
|
1427
1465
|
)}
|
|
1428
1466
|
|
|
1429
|
-
<div className="
|
|
1430
|
-
<div className="flex items-center justify-
|
|
1467
|
+
<div className="flex min-h-10 flex-row items-center justify-between gap-2">
|
|
1468
|
+
<div className="flex items-center justify-start">
|
|
1431
1469
|
<div
|
|
1432
1470
|
className="flex min-w-0 items-center gap-2"
|
|
1433
1471
|
data-testid="character-voice-picker"
|
|
@@ -1466,7 +1504,7 @@ export function CharacterView({
|
|
|
1466
1504
|
}}
|
|
1467
1505
|
placeholder={t("characterview.selectAVoice")}
|
|
1468
1506
|
menuPlacement="top"
|
|
1469
|
-
className="w-[
|
|
1507
|
+
className="min-w-0 w-[7rem] sm:w-[11rem]"
|
|
1470
1508
|
triggerClassName="h-8 rounded-full border-border/50 bg-bg/65 px-4 py-0 text-[11px] shadow-inner backdrop-blur-sm"
|
|
1471
1509
|
menuClassName="border-border/60 bg-bg/92 shadow-2xl backdrop-blur-md"
|
|
1472
1510
|
/>
|
|
@@ -1495,25 +1533,32 @@ export function CharacterView({
|
|
|
1495
1533
|
</div>
|
|
1496
1534
|
</div>
|
|
1497
1535
|
|
|
1498
|
-
<div className="flex items-center
|
|
1536
|
+
<div className="flex items-center">
|
|
1499
1537
|
<Button
|
|
1500
1538
|
size="lg"
|
|
1501
|
-
className="
|
|
1539
|
+
className="ce-save-btn"
|
|
1502
1540
|
disabled={characterSaving || voiceSaving}
|
|
1503
1541
|
onClick={() => void handleSaveAll()}
|
|
1504
1542
|
>
|
|
1505
|
-
{characterSaving || voiceSaving ?
|
|
1543
|
+
{characterSaving || voiceSaving ? (
|
|
1544
|
+
"saving..."
|
|
1545
|
+
) : (
|
|
1546
|
+
<>
|
|
1547
|
+
<span className="hidden sm:inline">Save Character</span>
|
|
1548
|
+
<span className="sm:hidden">Save</span>
|
|
1549
|
+
</>
|
|
1550
|
+
)}
|
|
1506
1551
|
</Button>
|
|
1507
1552
|
</div>
|
|
1508
1553
|
|
|
1509
|
-
<div className="flex items-center justify-
|
|
1554
|
+
<div className="flex items-center justify-end">
|
|
1510
1555
|
<Button
|
|
1511
1556
|
type="button"
|
|
1512
1557
|
variant={customOverridesEnabled ? "outline" : "default"}
|
|
1513
1558
|
size="sm"
|
|
1514
|
-
className={`
|
|
1559
|
+
className={`ce-customize-btn ${customOverridesEnabled
|
|
1515
1560
|
? "border-border/40 bg-bg/40 text-txt"
|
|
1516
|
-
: "
|
|
1561
|
+
: ""
|
|
1517
1562
|
}`}
|
|
1518
1563
|
onClick={() =>
|
|
1519
1564
|
handleCustomOverridesChange(!customOverridesEnabled)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from "react";
|
|
3
|
+
import TestRenderer from "react-test-renderer";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { FlaminaGuideCard } from "./FlaminaGuide";
|
|
6
|
+
|
|
7
|
+
function textOf(node: TestRenderer.ReactTestInstance): string {
|
|
8
|
+
return node.children
|
|
9
|
+
.map((child) => (typeof child === "string" ? child : textOf(child)))
|
|
10
|
+
.join("");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("FlaminaGuideCard", () => {
|
|
14
|
+
it("explains provider impact on character behavior", () => {
|
|
15
|
+
const tree = TestRenderer.create(
|
|
16
|
+
React.createElement(FlaminaGuideCard, { topic: "provider" }),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const renderedText = textOf(tree.root);
|
|
20
|
+
|
|
21
|
+
expect(renderedText).toContain("reasons");
|
|
22
|
+
expect(renderedText).toContain("latency");
|
|
23
|
+
expect(renderedText).toContain("output quality");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("explains rpc impact on external capabilities", () => {
|
|
27
|
+
const tree = TestRenderer.create(
|
|
28
|
+
React.createElement(FlaminaGuideCard, { topic: "rpc" }),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const renderedText = textOf(tree.root);
|
|
32
|
+
|
|
33
|
+
expect(renderedText).toContain("wallets");
|
|
34
|
+
expect(renderedText).toContain("chains");
|
|
35
|
+
expect(renderedText).toContain("external execution");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("explains permissions impact on local access", () => {
|
|
39
|
+
const tree = TestRenderer.create(
|
|
40
|
+
React.createElement(FlaminaGuideCard, { topic: "permissions" }),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const renderedText = textOf(tree.root);
|
|
44
|
+
|
|
45
|
+
expect(renderedText).toContain("see");
|
|
46
|
+
expect(renderedText).toContain("control");
|
|
47
|
+
expect(renderedText).toContain("locally");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("explains voice impact on presentation", () => {
|
|
51
|
+
const tree = TestRenderer.create(
|
|
52
|
+
React.createElement(FlaminaGuideCard, { topic: "voice" }),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const renderedText = textOf(tree.root);
|
|
56
|
+
|
|
57
|
+
expect(renderedText).toContain("sounds");
|
|
58
|
+
expect(renderedText).toContain("spoken interactions");
|
|
59
|
+
expect(renderedText).toContain("saved");
|
|
60
|
+
});
|
|
61
|
+
});
|