@elizaos/app-core 2.0.0-alpha.13 → 2.0.0-alpha.15

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.
Files changed (125) hide show
  1. package/.turbo/turbo-build.log +0 -1
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +26 -12
  4. package/dist/api/client.d.ts +3 -3
  5. package/dist/api/client.d.ts.map +1 -1
  6. package/dist/api/client.js +24 -13
  7. package/dist/bridge/plugin-bridge.d.ts.map +1 -1
  8. package/dist/components/AvatarLoader.d.ts +3 -1
  9. package/dist/components/AvatarLoader.d.ts.map +1 -1
  10. package/dist/components/AvatarLoader.js +4 -1
  11. package/dist/components/BscTradePanel.d.ts +1 -1
  12. package/dist/components/BscTradePanel.d.ts.map +1 -1
  13. package/dist/components/CharacterView.d.ts.map +1 -1
  14. package/dist/components/CharacterView.js +50 -15
  15. package/dist/components/ChatView.js +1 -1
  16. package/dist/components/ConfigPageView.d.ts.map +1 -1
  17. package/dist/components/ConfigPageView.js +9 -8
  18. package/dist/components/ConversationsSidebar.js +1 -1
  19. package/dist/components/CustomActionEditor.js +1 -1
  20. package/dist/components/FineTuningView.d.ts.map +1 -1
  21. package/dist/components/FineTuningView.js +2 -2
  22. package/dist/components/GlobalEmoteOverlay.d.ts.map +1 -1
  23. package/dist/components/GlobalEmoteOverlay.js +1 -1
  24. package/dist/components/HeartbeatsView.js +1 -1
  25. package/dist/components/LoadingScreen.d.ts.map +1 -1
  26. package/dist/components/LoadingScreen.js +38 -7
  27. package/dist/components/OnboardingWizard.js +1 -1
  28. package/dist/components/PluginsView.d.ts.map +1 -1
  29. package/dist/components/PluginsView.js +4 -1
  30. package/dist/components/ShellOverlays.js +1 -1
  31. package/dist/components/VoiceConfigView.d.ts.map +1 -1
  32. package/dist/components/VoiceConfigView.js +4 -2
  33. package/dist/components/avatar/VrmEngine.d.ts +2 -0
  34. package/dist/components/avatar/VrmEngine.d.ts.map +1 -1
  35. package/dist/components/avatar/VrmEngine.js +14 -6
  36. package/dist/components/companion/CompanionSceneHost.d.ts +1 -1
  37. package/dist/components/companion/CompanionSceneHost.d.ts.map +1 -1
  38. package/dist/components/companion/CompanionSceneHost.js +1 -1
  39. package/dist/components/index.d.ts +10 -10
  40. package/dist/components/index.d.ts.map +1 -1
  41. package/dist/components/index.js +10 -10
  42. package/dist/components/inventory/TokensTable.js +2 -2
  43. package/dist/components/onboarding/IdentityStep.d.ts.map +1 -1
  44. package/dist/components/onboarding/IdentityStep.js +1 -1
  45. package/dist/components/shared/ShellHeaderControls.js +1 -1
  46. package/dist/config/config-field.d.ts.map +1 -1
  47. package/dist/config/config-field.js +7 -8
  48. package/dist/config/index.d.ts +1 -1
  49. package/dist/config/index.d.ts.map +1 -1
  50. package/dist/config/index.js +1 -1
  51. package/dist/hooks/useVoiceChat.d.ts.map +1 -1
  52. package/dist/hooks/useVoiceChat.js +3 -1
  53. package/dist/i18n/locales/en.json +1192 -1192
  54. package/dist/i18n/locales/es.json +1192 -1192
  55. package/dist/i18n/locales/ko.json +1192 -1192
  56. package/dist/i18n/locales/pt.json +1192 -1192
  57. package/dist/i18n/locales/zh-CN.json +1192 -1192
  58. package/dist/package.json +181 -0
  59. package/dist/platform/lifo.d.ts.map +1 -1
  60. package/dist/platform/lifo.js +4 -1
  61. package/dist/state/AppContext.d.ts.map +1 -1
  62. package/dist/state/AppContext.js +23 -6
  63. package/dist/state/internal.d.ts +1 -1
  64. package/dist/state/internal.d.ts.map +1 -1
  65. package/dist/state/internal.js +1 -1
  66. package/dist/state/parsers.d.ts.map +1 -1
  67. package/dist/state/parsers.js +3 -2
  68. package/dist/state/persistence.js +1 -1
  69. package/dist/styles/anime.css +6324 -0
  70. package/dist/styles/base.css +196 -0
  71. package/dist/styles/onboarding-game.css +738 -0
  72. package/dist/styles/styles.css +2087 -0
  73. package/dist/styles/xterm.css +241 -0
  74. package/package.json +4 -4
  75. package/src/App.tsx +35 -14
  76. package/src/ambient.d.ts +5 -5
  77. package/src/api/client.ts +36 -23
  78. package/src/bridge/plugin-bridge.ts +1 -1
  79. package/src/components/AvatarLoader.tsx +6 -0
  80. package/src/components/BscTradePanel.tsx +1 -1
  81. package/src/components/CharacterView.tsx +536 -367
  82. package/src/components/ChatView.tsx +3 -3
  83. package/src/components/ConfigPageView.tsx +9 -8
  84. package/src/components/ConversationsSidebar.tsx +1 -1
  85. package/src/components/CustomActionEditor.tsx +6 -6
  86. package/src/components/FineTuningView.tsx +6 -3
  87. package/src/components/GlobalEmoteOverlay.tsx +1 -4
  88. package/src/components/HeartbeatsView.tsx +1 -1
  89. package/src/components/InventoryView.tsx +2 -2
  90. package/src/components/LoadingScreen.tsx +39 -6
  91. package/src/components/OnboardingWizard.tsx +1 -1
  92. package/src/components/PluginsView.tsx +6 -0
  93. package/src/components/ShellOverlays.tsx +1 -1
  94. package/src/components/VoiceConfigView.tsx +4 -5
  95. package/src/components/avatar/VrmEngine.ts +25 -7
  96. package/src/components/companion/CompanionSceneHost.tsx +5 -1
  97. package/src/components/index.ts +10 -10
  98. package/src/components/inventory/TokensTable.tsx +2 -2
  99. package/src/components/onboarding/IdentityStep.tsx +9 -13
  100. package/src/components/shared/ShellHeaderControls.tsx +1 -1
  101. package/src/config/config-field.tsx +7 -8
  102. package/src/config/index.ts +3 -3
  103. package/src/hooks/useVoiceChat.ts +5 -3
  104. package/src/platform/lifo.ts +14 -4
  105. package/src/state/AppContext.tsx +24 -1
  106. package/src/state/internal.ts +6 -6
  107. package/src/state/parsers.ts +4 -3
  108. package/src/state/persistence.ts +1 -1
  109. package/test/app/MessageContent.test.tsx +42 -0
  110. package/test/app/bug-report-modal.test.tsx +3 -3
  111. package/test/app/chat-view.test.tsx +3 -3
  112. package/test/app/cloud-login-lock.test.ts +3 -2
  113. package/test/app/custom-actions-smoke.test.ts +3 -3
  114. package/test/app/onboarding-language.test.tsx +3 -3
  115. package/test/app/pages-navigation-smoke.e2e.test.ts +13 -8
  116. package/test/app/plugin-bridge.test.ts +1 -1
  117. package/test/app/provider-dropdown-default.test.tsx +2 -4
  118. package/test/app/restart-banner.test.tsx +3 -3
  119. package/test/app/shell-mode-switching.e2e.test.ts +6 -6
  120. package/test/app/shell-mode-tab-memory.test.tsx +1 -1
  121. package/test/app/startup-chat.e2e.test.ts +3 -3
  122. package/test/app/triggers-view.e2e.test.ts +3 -2
  123. package/test/app/wallet-api-save-lock.test.ts +2 -1
  124. package/test/utils/assistant-text.test.ts +64 -0
  125. package/test/utils/streaming-text.test.ts +89 -0
@@ -8,7 +8,6 @@ import {
8
8
  type StylePreset,
9
9
  type VoiceConfig,
10
10
  } from "@elizaos/app-core/api";
11
- import { AvatarSelector } from "./AvatarSelector";
12
11
  import {
13
12
  dispatchWindowEvent,
14
13
  VOICE_CONFIG_UPDATED_EVENT,
@@ -20,8 +19,17 @@ import {
20
19
  type VoicePreset,
21
20
  } from "@elizaos/app-core/voice";
22
21
  import { Button, Input, Textarea, ThemedSelect } from "@elizaos/ui";
23
- import { BookOpen, FileText, Lock, LockOpen, Palette, Volume2, VolumeX } from "lucide-react";
22
+ import {
23
+ BookOpen,
24
+ FileText,
25
+ Lock,
26
+ LockOpen,
27
+ Palette,
28
+ Volume2,
29
+ VolumeX,
30
+ } from "lucide-react";
24
31
  import { useCallback, useEffect, useState } from "react";
32
+ import { AvatarSelector } from "./AvatarSelector";
25
33
 
26
34
  const DEFAULT_ELEVEN_FAST_MODEL = "eleven_flash_v2_5";
27
35
 
@@ -856,9 +864,17 @@ export function CharacterView({
856
864
  const scrollPaneCls =
857
865
  "min-h-0 overflow-y-auto pr-1 [scrollbar-gutter:stable] custom-scrollbar";
858
866
 
859
- const SIDEBAR_TABS: { key: ActiveSection; icon: typeof FileText; labelKey: string }[] = [
867
+ const SIDEBAR_TABS: {
868
+ key: ActiveSection;
869
+ icon: typeof FileText;
870
+ labelKey: string;
871
+ }[] = [
860
872
  { key: "aboutMe", icon: FileText, labelKey: "characterview.aboutMe" },
861
- { key: "directions", icon: BookOpen, labelKey: "characterview.directionsAndThing" },
873
+ {
874
+ key: "directions",
875
+ icon: BookOpen,
876
+ labelKey: "characterview.directionsAndThing",
877
+ },
862
878
  { key: "styleRules", icon: Palette, labelKey: "characterview.StyleRules" },
863
879
  ];
864
880
 
@@ -1101,10 +1117,7 @@ export function CharacterView({
1101
1117
  {customOverridesEnabled && (
1102
1118
  <div className="mt-3 flex justify-end">
1103
1119
  {/* ── Book container ── */}
1104
- <div
1105
- className={notebookShellCls}
1106
- data-testid="character-notebook"
1107
- >
1120
+ <div className={notebookShellCls} data-testid="character-notebook">
1108
1121
  <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" />
1109
1122
  <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)]" />
1110
1123
  <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)]" />
@@ -1115,410 +1128,564 @@ export function CharacterView({
1115
1128
  <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)]" />
1116
1129
  <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%)]" />
1117
1130
  <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)]" />
1118
- {/* ── Book page (left) ── */}
1119
- <div
1120
- 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%)]`}
1121
- >
1122
- <div className="pointer-events-none absolute inset-[0.7rem] rounded-[1.25rem] border border-[#efe3cc]/85 shadow-[inset_0_0_0_1px_rgba(207,184,141,0.24)] dark:border-white/5 dark:shadow-[inset_0_0_0_1px_rgba(255,255,255,0.03)]" />
1123
- <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.72)_0%,rgba(255,255,255,0)_36%),linear-gradient(180deg,rgba(255,255,255,0.14)_0%,rgba(255,255,255,0)_24%),repeating-linear-gradient(0deg,rgba(168,139,87,0.035)_0px,rgba(168,139,87,0.035)_1px,transparent_1px,transparent_11px)] opacity-85 dark:opacity-25" />
1124
- <div className="pointer-events-none absolute bottom-0 right-0 top-0 w-5 bg-[linear-gradient(90deg,rgba(123,98,61,0)_0%,rgba(123,98,61,0.08)_55%,rgba(255,255,255,0.14)_100%)] dark:bg-[linear-gradient(90deg,rgba(255,255,255,0)_0%,rgba(255,255,255,0.03)_60%,rgba(255,255,255,0.08)_100%)]" />
1125
- {/* Mode toggle: Core / Examples */}
1126
- <div className="relative z-10 flex items-end gap-2 bg-[linear-gradient(180deg,rgba(255,255,255,0.26)_0%,rgba(255,255,255,0)_100%)] px-5 pb-3 pt-4 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.06)_0%,rgba(255,255,255,0)_100%)]">
1127
- <button
1128
- type="button"
1129
- className={`relative h-9 rounded-b-md rounded-t-[1rem] border px-3.5 text-[10px] font-bold uppercase tracking-[0.18em] transition-all ${
1130
- customizeStep === "core"
1131
- ? "border-[#c59b47] bg-[linear-gradient(180deg,#f4cf79_0%,#d69f45_100%)] text-[#2d2418] shadow-[0_10px_18px_rgba(137,97,36,0.22)]"
1132
- : "border-[#d4c6ad]/90 bg-[linear-gradient(180deg,rgba(255,252,245,0.96)_0%,rgba(243,235,222,0.82)_100%)] text-[#7b7365] shadow-[inset_0_1px_0_rgba(255,255,255,0.75)] hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.98)_0%,rgba(248,240,226,0.9)_100%)] dark:border-white/10 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.1)_0%,rgba(255,255,255,0.04)_100%)] dark:text-muted dark:shadow-none dark:hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.14)_0%,rgba(255,255,255,0.05)_100%)]"
1133
- }`}
1134
- onClick={() => setCustomizeStep("core")}
1135
- >
1136
- {t("characterview.core")}
1137
- </button>
1138
- {activeSection !== "styleRules" && (
1131
+ {/* ── Book page (left) ── */}
1132
+ <div
1133
+ 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%)]`}
1134
+ >
1135
+ <div className="pointer-events-none absolute inset-[0.7rem] rounded-[1.25rem] border border-[#efe3cc]/85 shadow-[inset_0_0_0_1px_rgba(207,184,141,0.24)] dark:border-white/5 dark:shadow-[inset_0_0_0_1px_rgba(255,255,255,0.03)]" />
1136
+ <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.72)_0%,rgba(255,255,255,0)_36%),linear-gradient(180deg,rgba(255,255,255,0.14)_0%,rgba(255,255,255,0)_24%),repeating-linear-gradient(0deg,rgba(168,139,87,0.035)_0px,rgba(168,139,87,0.035)_1px,transparent_1px,transparent_11px)] opacity-85 dark:opacity-25" />
1137
+ <div className="pointer-events-none absolute bottom-0 right-0 top-0 w-5 bg-[linear-gradient(90deg,rgba(123,98,61,0)_0%,rgba(123,98,61,0.08)_55%,rgba(255,255,255,0.14)_100%)] dark:bg-[linear-gradient(90deg,rgba(255,255,255,0)_0%,rgba(255,255,255,0.03)_60%,rgba(255,255,255,0.08)_100%)]" />
1138
+ {/* Mode toggle: Core / Examples */}
1139
+ <div className="relative z-10 flex items-end gap-2 bg-[linear-gradient(180deg,rgba(255,255,255,0.26)_0%,rgba(255,255,255,0)_100%)] px-5 pb-3 pt-4 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.06)_0%,rgba(255,255,255,0)_100%)]">
1139
1140
  <button
1140
1141
  type="button"
1141
1142
  className={`relative h-9 rounded-b-md rounded-t-[1rem] border px-3.5 text-[10px] font-bold uppercase tracking-[0.18em] transition-all ${
1142
- customizeStep === "examples"
1143
+ customizeStep === "core"
1143
1144
  ? "border-[#c59b47] bg-[linear-gradient(180deg,#f4cf79_0%,#d69f45_100%)] text-[#2d2418] shadow-[0_10px_18px_rgba(137,97,36,0.22)]"
1144
1145
  : "border-[#d4c6ad]/90 bg-[linear-gradient(180deg,rgba(255,252,245,0.96)_0%,rgba(243,235,222,0.82)_100%)] text-[#7b7365] shadow-[inset_0_1px_0_rgba(255,255,255,0.75)] hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.98)_0%,rgba(248,240,226,0.9)_100%)] dark:border-white/10 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.1)_0%,rgba(255,255,255,0.04)_100%)] dark:text-muted dark:shadow-none dark:hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.14)_0%,rgba(255,255,255,0.05)_100%)]"
1145
1146
  }`}
1146
- onClick={() => setCustomizeStep("examples")}
1147
+ onClick={() => setCustomizeStep("core")}
1147
1148
  >
1148
- {t("characterview.examples")}
1149
+ {t("characterview.core")}
1149
1150
  </button>
1150
- )}
1151
- <div className="pointer-events-none absolute inset-x-4 bottom-0 h-px bg-[linear-gradient(90deg,rgba(209,193,165,0)_0%,rgba(209,193,165,0.92)_10%,rgba(209,193,165,0.92)_90%,rgba(209,193,165,0)_100%)] dark:bg-[linear-gradient(90deg,rgba(255,255,255,0)_0%,rgba(255,255,255,0.08)_10%,rgba(255,255,255,0.08)_90%,rgba(255,255,255,0)_100%)]" />
1152
- </div>
1151
+ {activeSection !== "styleRules" && (
1152
+ <button
1153
+ type="button"
1154
+ className={`relative h-9 rounded-b-md rounded-t-[1rem] border px-3.5 text-[10px] font-bold uppercase tracking-[0.18em] transition-all ${
1155
+ customizeStep === "examples"
1156
+ ? "border-[#c59b47] bg-[linear-gradient(180deg,#f4cf79_0%,#d69f45_100%)] text-[#2d2418] shadow-[0_10px_18px_rgba(137,97,36,0.22)]"
1157
+ : "border-[#d4c6ad]/90 bg-[linear-gradient(180deg,rgba(255,252,245,0.96)_0%,rgba(243,235,222,0.82)_100%)] text-[#7b7365] shadow-[inset_0_1px_0_rgba(255,255,255,0.75)] hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.98)_0%,rgba(248,240,226,0.9)_100%)] dark:border-white/10 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.1)_0%,rgba(255,255,255,0.04)_100%)] dark:text-muted dark:shadow-none dark:hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.14)_0%,rgba(255,255,255,0.05)_100%)]"
1158
+ }`}
1159
+ onClick={() => setCustomizeStep("examples")}
1160
+ >
1161
+ {t("characterview.examples")}
1162
+ </button>
1163
+ )}
1164
+ <div className="pointer-events-none absolute inset-x-4 bottom-0 h-px bg-[linear-gradient(90deg,rgba(209,193,165,0)_0%,rgba(209,193,165,0.92)_10%,rgba(209,193,165,0.92)_90%,rgba(209,193,165,0)_100%)] dark:bg-[linear-gradient(90deg,rgba(255,255,255,0)_0%,rgba(255,255,255,0.08)_10%,rgba(255,255,255,0.08)_90%,rgba(255,255,255,0)_100%)]" />
1165
+ </div>
1153
1166
 
1154
- {/* Content area */}
1155
- <div className="relative z-10 flex flex-1 flex-col overflow-y-auto p-5 custom-scrollbar" role="tabpanel" aria-labelledby={`notebook-tab-${activeSection}`}>
1156
- {/* ── About Me (Core) ── */}
1157
- {activeSection === "aboutMe" && customizeStep === "core" && (
1158
- <div className="flex flex-1 flex-col gap-3" data-testid="character-core-editor">
1159
- {/* Name */}
1160
- <div className="flex flex-col gap-1.5">
1161
- <span className="text-xs font-medium text-[#6d737a] dark:text-muted">Name</span>
1162
- <Input
1163
- type="text"
1164
- value={d.name ?? ""}
1165
- placeholder="Agent name"
1166
- onChange={(e) => handleFieldEdit("name", e.target.value)}
1167
- className="h-8 rounded-lg border-[#d6d3c6] dark:border-border/40 bg-white/60 dark:bg-white/5 text-sm text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:border-accent focus-visible:ring-accent/50"
1168
- />
1169
- </div>
1167
+ {/* Content area */}
1168
+ <div
1169
+ className="relative z-10 flex flex-1 flex-col overflow-y-auto p-5 custom-scrollbar"
1170
+ role="tabpanel"
1171
+ aria-labelledby={`notebook-tab-${activeSection}`}
1172
+ >
1173
+ {/* ── About Me (Core) ── */}
1174
+ {activeSection === "aboutMe" && customizeStep === "core" && (
1175
+ <div
1176
+ className="flex flex-1 flex-col gap-3"
1177
+ data-testid="character-core-editor"
1178
+ >
1179
+ {/* Name */}
1180
+ <div className="flex flex-col gap-1.5">
1181
+ <span className="text-xs font-medium text-[#6d737a] dark:text-muted">
1182
+ Name
1183
+ </span>
1184
+ <Input
1185
+ type="text"
1186
+ value={d.name ?? ""}
1187
+ placeholder="Agent name"
1188
+ onChange={(e) =>
1189
+ handleFieldEdit("name", e.target.value)
1190
+ }
1191
+ className="h-8 rounded-lg border-[#d6d3c6] dark:border-border/40 bg-white/60 dark:bg-white/5 text-sm text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:border-accent focus-visible:ring-accent/50"
1192
+ />
1193
+ </div>
1170
1194
 
1171
- {/* Avatar */}
1172
- <div className="flex flex-col gap-1.5">
1173
- <span className="text-xs font-medium text-[#6d737a] dark:text-muted">Avatar</span>
1174
- <AvatarSelector
1175
- selected={selectedVrmIndex}
1176
- onSelect={(index: number) => setState("selectedVrmIndex", index)}
1177
- onUpload={handleCustomVrmUpload}
1178
- showUpload={true}
1179
- fullWidth={true}
1180
- />
1181
- </div>
1195
+ {/* Avatar */}
1196
+ <div className="flex flex-col gap-1.5">
1197
+ <span className="text-xs font-medium text-[#6d737a] dark:text-muted">
1198
+ Avatar
1199
+ </span>
1200
+ <AvatarSelector
1201
+ selected={selectedVrmIndex}
1202
+ onSelect={(index: number) =>
1203
+ setState("selectedVrmIndex", index)
1204
+ }
1205
+ onUpload={handleCustomVrmUpload}
1206
+ showUpload={true}
1207
+ fullWidth={true}
1208
+ />
1209
+ </div>
1182
1210
 
1183
- <div className="flex items-center justify-between">
1184
- <span className="text-xs font-medium text-[#6d737a] dark:text-muted">{t("characterview.aboutMe")}</span>
1185
- <Button
1186
- variant="ghost"
1187
- size="sm"
1188
- className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1189
- onClick={() => void handleGenerate("bio")}
1190
- disabled={generating === "bio"}
1191
- >
1192
- {generating === "bio" ? "generating..." : "regenerate"}
1193
- </Button>
1211
+ <div className="flex items-center justify-between">
1212
+ <span className="text-xs font-medium text-[#6d737a] dark:text-muted">
1213
+ {t("characterview.aboutMe")}
1214
+ </span>
1215
+ <Button
1216
+ variant="ghost"
1217
+ size="sm"
1218
+ className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1219
+ onClick={() => void handleGenerate("bio")}
1220
+ disabled={generating === "bio"}
1221
+ >
1222
+ {generating === "bio"
1223
+ ? "generating..."
1224
+ : "regenerate"}
1225
+ </Button>
1226
+ </div>
1227
+ <Textarea
1228
+ value={bioText}
1229
+ placeholder={t("characterview.describeWhoYourAg")}
1230
+ onChange={(e) => handleFieldEdit("bio", e.target.value)}
1231
+ className="flex-1 resize-none overflow-y-auto rounded-lg border-[#d6d3c6] dark:border-border/40 bg-white/60 dark:bg-white/5 p-3 font-mono text-xs leading-relaxed text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:border-accent focus-visible:ring-accent/50"
1232
+ />
1194
1233
  </div>
1195
- <Textarea
1196
- value={bioText}
1197
- placeholder={t("characterview.describeWhoYourAg")}
1198
- onChange={(e) => handleFieldEdit("bio", e.target.value)}
1199
- className="flex-1 resize-none overflow-y-auto rounded-lg border-[#d6d3c6] dark:border-border/40 bg-white/60 dark:bg-white/5 p-3 font-mono text-xs leading-relaxed text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:border-accent focus-visible:ring-accent/50"
1200
- />
1201
- </div>
1202
- )}
1234
+ )}
1235
+
1236
+ {/* ── Directions (Core) ── */}
1237
+ {activeSection === "directions" &&
1238
+ customizeStep === "core" && (
1239
+ <div className="flex flex-1 flex-col gap-3">
1240
+ <div className="flex items-center justify-between">
1241
+ <span className="text-xs font-medium text-[#6d737a] dark:text-muted">
1242
+ {t("characterview.directionsAndThing")}
1243
+ </span>
1244
+ <Button
1245
+ variant="ghost"
1246
+ size="sm"
1247
+ className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1248
+ onClick={() => void handleGenerate("system")}
1249
+ disabled={generating === "system"}
1250
+ >
1251
+ {generating === "system"
1252
+ ? "generating..."
1253
+ : "regenerate"}
1254
+ </Button>
1255
+ </div>
1256
+ <Textarea
1257
+ value={d.system ?? ""}
1258
+ maxLength={10000}
1259
+ placeholder={t("characterview.writeInFirstPerso")}
1260
+ onChange={(e) =>
1261
+ handleFieldEdit("system", e.target.value)
1262
+ }
1263
+ className="flex-1 resize-none overflow-y-auto rounded-lg border-[#d6d3c6] dark:border-border/40 bg-white/60 dark:bg-white/5 p-3 font-mono text-xs leading-relaxed text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:border-accent focus-visible:ring-accent/50"
1264
+ />
1265
+ </div>
1266
+ )}
1203
1267
 
1204
- {/* ── Directions (Core) ── */}
1205
- {activeSection === "directions" && customizeStep === "core" && (
1206
- <div className="flex flex-1 flex-col gap-3">
1207
- <div className="flex items-center justify-between">
1208
- <span className="text-xs font-medium text-[#6d737a] dark:text-muted">{t("characterview.directionsAndThing")}</span>
1209
- <Button
1210
- variant="ghost"
1211
- size="sm"
1212
- className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1213
- onClick={() => void handleGenerate("system")}
1214
- disabled={generating === "system"}
1268
+ {/* ── Style Rules (Core) ── */}
1269
+ {activeSection === "styleRules" &&
1270
+ customizeStep === "core" && (
1271
+ <div
1272
+ className="flex flex-col gap-3"
1273
+ data-testid="character-style-editor"
1215
1274
  >
1216
- {generating === "system" ? "generating..." : "regenerate"}
1217
- </Button>
1218
- </div>
1219
- <Textarea
1220
- value={d.system ?? ""}
1221
- maxLength={10000}
1222
- placeholder={t("characterview.writeInFirstPerso")}
1223
- onChange={(e) => handleFieldEdit("system", e.target.value)}
1224
- className="flex-1 resize-none overflow-y-auto rounded-lg border-[#d6d3c6] dark:border-border/40 bg-white/60 dark:bg-white/5 p-3 font-mono text-xs leading-relaxed text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:border-accent focus-visible:ring-accent/50"
1225
- />
1226
- </div>
1227
- )}
1275
+ <div className="flex items-center justify-between">
1276
+ <span className="text-xs font-medium text-[#6d737a] dark:text-muted">
1277
+ {t("characterview.StyleRules")}
1278
+ </span>
1279
+ <Button
1280
+ variant="ghost"
1281
+ size="sm"
1282
+ className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1283
+ onClick={() =>
1284
+ void handleGenerate("style", "replace")
1285
+ }
1286
+ disabled={generating === "style"}
1287
+ >
1288
+ {generating === "style"
1289
+ ? "generating..."
1290
+ : "regenerate"}
1291
+ </Button>
1292
+ </div>
1293
+ {STYLE_SECTION_KEYS.map((key) => {
1294
+ const items = d.style?.[key] ?? [];
1295
+ return (
1296
+ <div
1297
+ key={key}
1298
+ className="flex flex-col gap-1.5"
1299
+ data-testid={`style-section-${key}`}
1300
+ >
1301
+ <div className="flex items-center gap-2 border-b border-[#d6d3c6]/60 dark:border-border/30 pb-1">
1302
+ <span className="text-[10px] font-bold uppercase tracking-widest text-[#6d737a] dark:text-muted">
1303
+ {key}
1304
+ </span>
1305
+ <span className="text-[9px] font-medium uppercase tracking-wider text-[#9ca3af] dark:text-muted/60">
1306
+ {items.length} rule
1307
+ {items.length === 1 ? "" : "s"}
1308
+ </span>
1309
+ </div>
1310
+ <div className={`${scrollPaneCls} max-h-[12rem]`}>
1311
+ <div className="flex flex-col gap-1">
1312
+ {items.length > 0 ? (
1313
+ items.map((item, index) => (
1314
+ <div
1315
+ key={getStyleEntryRenderKey(
1316
+ key,
1317
+ items,
1318
+ item,
1319
+ index,
1320
+ )}
1321
+ className="group flex items-start gap-2 rounded-md border border-[#d6d3c6]/40 dark:border-border/20 bg-white/50 dark:bg-white/3 px-2.5 py-2"
1322
+ data-testid={`style-entry-${key}-${index}`}
1323
+ >
1324
+ <span className="mt-0.5 shrink-0 text-[10px] font-bold text-accent">
1325
+ {index + 1}
1326
+ </span>
1327
+ <Textarea
1328
+ value={
1329
+ styleEntryDrafts[key]?.[index] ??
1330
+ item
1331
+ }
1332
+ rows={1}
1333
+ onChange={(e) =>
1334
+ handleStyleEntryDraftChange(
1335
+ key,
1336
+ index,
1337
+ e.target.value,
1338
+ )
1339
+ }
1340
+ onBlur={() =>
1341
+ handleCommitStyleEntry(key, index)
1342
+ }
1343
+ className="min-h-[2rem] min-w-0 flex-1 resize-none rounded border-0 bg-transparent p-0 font-mono text-xs leading-relaxed text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:ring-0"
1344
+ data-testid={`style-entry-editor-${key}-${index}`}
1345
+ />
1346
+ <button
1347
+ type="button"
1348
+ className="mt-0.5 shrink-0 text-[#9ca3af] opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-500"
1349
+ onClick={() =>
1350
+ handleRemoveStyleEntry(key, index)
1351
+ }
1352
+ title={t("characterview.remove")}
1353
+ >
1354
+ <svg
1355
+ width="10"
1356
+ height="10"
1357
+ viewBox="0 0 10 10"
1358
+ fill="none"
1359
+ stroke="currentColor"
1360
+ strokeWidth="1.5"
1361
+ strokeLinecap="round"
1362
+ aria-label={t(
1363
+ "characterview.remove",
1364
+ )}
1365
+ >
1366
+ <path d="M2 2l6 6M8 2l-6 6" />
1367
+ </svg>
1368
+ </button>
1369
+ </div>
1370
+ ))
1371
+ ) : (
1372
+ <div className="rounded-md border border-dashed border-[#d6d3c6]/60 dark:border-border/30 px-3 py-2 text-[11px] text-[#9ca3af] dark:text-muted">
1373
+ {STYLE_SECTION_EMPTY_STATES[key]}
1374
+ </div>
1375
+ )}
1376
+ </div>
1377
+ </div>
1378
+ <div className="flex items-center gap-2">
1379
+ <Input
1380
+ type="text"
1381
+ value={pendingStyleEntries[key]}
1382
+ placeholder={STYLE_SECTION_PLACEHOLDERS[key]}
1383
+ onChange={(e) =>
1384
+ handlePendingStyleEntryChange(
1385
+ key,
1386
+ e.target.value,
1387
+ )
1388
+ }
1389
+ onKeyDown={(e) => {
1390
+ if (e.key === "Enter") {
1391
+ e.preventDefault();
1392
+ handleAddStyleEntry(key);
1393
+ }
1394
+ }}
1395
+ className="h-7 min-w-0 flex-1 rounded-md border-[#d6d3c6]/60 dark:border-border/30 bg-white/50 dark:bg-white/5 text-xs text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:border-accent focus-visible:ring-accent/50"
1396
+ data-testid={`style-entry-input-${key}`}
1397
+ />
1398
+ <Button
1399
+ variant="ghost"
1400
+ size="sm"
1401
+ className="h-7 shrink-0 rounded-md px-2 text-[10px] font-bold text-accent"
1402
+ onClick={() => handleAddStyleEntry(key)}
1403
+ disabled={!pendingStyleEntries[key].trim()}
1404
+ >
1405
+ + add
1406
+ </Button>
1407
+ </div>
1408
+ </div>
1409
+ );
1410
+ })}
1411
+ </div>
1412
+ )}
1228
1413
 
1229
- {/* ── Style Rules (Core) ── */}
1230
- {activeSection === "styleRules" && customizeStep === "core" && (
1231
- <div className="flex flex-col gap-3" data-testid="character-style-editor">
1232
- <div className="flex items-center justify-between">
1233
- <span className="text-xs font-medium text-[#6d737a] dark:text-muted">{t("characterview.StyleRules")}</span>
1234
- <Button
1235
- variant="ghost"
1236
- size="sm"
1237
- className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1238
- onClick={() => void handleGenerate("style", "replace")}
1239
- disabled={generating === "style"}
1414
+ {/* ── Chat Examples (About Me + Examples mode) ── */}
1415
+ {activeSection === "aboutMe" &&
1416
+ customizeStep === "examples" && (
1417
+ <div
1418
+ className="flex flex-col gap-2"
1419
+ data-testid="character-chat-examples-card"
1240
1420
  >
1241
- {generating === "style" ? "generating..." : "regenerate"}
1242
- </Button>
1243
- </div>
1244
- {STYLE_SECTION_KEYS.map((key) => {
1245
- const items = d.style?.[key] ?? [];
1246
- return (
1247
- <div
1248
- key={key}
1249
- className="flex flex-col gap-1.5"
1250
- data-testid={`style-section-${key}`}
1251
- >
1252
- <div className="flex items-center gap-2 border-b border-[#d6d3c6]/60 dark:border-border/30 pb-1">
1253
- <span className="text-[10px] font-bold uppercase tracking-widest text-[#6d737a] dark:text-muted">{key}</span>
1254
- <span className="text-[9px] font-medium uppercase tracking-wider text-[#9ca3af] dark:text-muted/60">
1255
- {items.length} rule{items.length === 1 ? "" : "s"}
1256
- </span>
1257
- </div>
1258
- <div className={`${scrollPaneCls} max-h-[12rem]`}>
1259
- <div className="flex flex-col gap-1">
1260
- {items.length > 0 ? (
1261
- items.map((item, index) => (
1421
+ <div className="flex items-center justify-between">
1422
+ <span className="text-xs font-medium text-[#6d737a] dark:text-muted">
1423
+ {t("characterview.chatExamples")}
1424
+ </span>
1425
+ <Button
1426
+ variant="ghost"
1427
+ size="sm"
1428
+ className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1429
+ onClick={() =>
1430
+ void handleGenerate("chatExamples", "replace")
1431
+ }
1432
+ disabled={generating === "chatExamples"}
1433
+ >
1434
+ {generating === "chatExamples"
1435
+ ? "generating..."
1436
+ : "generate"}
1437
+ </Button>
1438
+ </div>
1439
+ <div className={`${scrollPaneCls} flex flex-col gap-2`}>
1440
+ {(d.messageExamples ?? []).map((convo, ci) => (
1441
+ <div
1442
+ key={convo.examples
1443
+ .map(
1444
+ (msg) =>
1445
+ `${msg.name}:${msg.content?.text ?? ""}`,
1446
+ )
1447
+ .join("|")}
1448
+ className="rounded-lg border border-[#d6d3c6]/40 dark:border-border/30 p-2.5"
1449
+ >
1450
+ <div className="mb-2 flex items-center justify-between">
1451
+ <span className="text-[9px] font-bold uppercase tracking-widest text-[#9ca3af] dark:text-muted/60">
1452
+ {t("characterview.conversation")} {ci + 1}
1453
+ </span>
1454
+ <button
1455
+ type="button"
1456
+ className="text-[#9ca3af] hover:text-red-500 transition-colors"
1457
+ onClick={() => {
1458
+ const updated = [
1459
+ ...(d.messageExamples ?? []),
1460
+ ];
1461
+ updated.splice(ci, 1);
1462
+ handleFieldEdit("messageExamples", updated);
1463
+ }}
1464
+ >
1465
+ <svg
1466
+ width="10"
1467
+ height="10"
1468
+ viewBox="0 0 10 10"
1469
+ fill="none"
1470
+ stroke="currentColor"
1471
+ strokeWidth="1.5"
1472
+ strokeLinecap="round"
1473
+ aria-label={t("characterview.remove")}
1474
+ >
1475
+ <path d="M2 2l6 6M8 2l-6 6" />
1476
+ </svg>
1477
+ </button>
1478
+ </div>
1479
+ <div className="flex flex-col gap-1.5">
1480
+ {convo.examples.map((msg, mi) => (
1262
1481
  <div
1263
- key={getStyleEntryRenderKey(key, items, item, index)}
1264
- className="group flex items-start gap-2 rounded-md border border-[#d6d3c6]/40 dark:border-border/20 bg-white/50 dark:bg-white/3 px-2.5 py-2"
1265
- data-testid={`style-entry-${key}-${index}`}
1482
+ key={`${msg.name}:${msg.content?.text ?? ""}`}
1483
+ className="flex items-center gap-2"
1266
1484
  >
1267
- <span className="mt-0.5 shrink-0 text-[10px] font-bold text-accent">
1268
- {index + 1}
1485
+ <span
1486
+ className={`w-10 shrink-0 text-right text-[9px] font-bold uppercase tracking-wider ${msg.name === "{{user1}}" ? "text-[#9ca3af] dark:text-muted" : "text-accent"}`}
1487
+ >
1488
+ {msg.name === "{{user1}}"
1489
+ ? "user"
1490
+ : "agent"}
1269
1491
  </span>
1270
- <Textarea
1271
- value={styleEntryDrafts[key]?.[index] ?? item}
1272
- rows={1}
1273
- onChange={(e) => handleStyleEntryDraftChange(key, index, e.target.value)}
1274
- onBlur={() => handleCommitStyleEntry(key, index)}
1275
- className="min-h-[2rem] min-w-0 flex-1 resize-none rounded border-0 bg-transparent p-0 font-mono text-xs leading-relaxed text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:ring-0"
1276
- data-testid={`style-entry-editor-${key}-${index}`}
1492
+ <input
1493
+ type="text"
1494
+ value={msg.content?.text ?? ""}
1495
+ onChange={(e) => {
1496
+ const updated = [
1497
+ ...(d.messageExamples ?? []),
1498
+ ];
1499
+ const convoClone = {
1500
+ examples: [...updated[ci].examples],
1501
+ };
1502
+ convoClone.examples[mi] = {
1503
+ ...convoClone.examples[mi],
1504
+ content: { text: e.target.value },
1505
+ };
1506
+ updated[ci] = convoClone;
1507
+ handleFieldEdit(
1508
+ "messageExamples",
1509
+ updated,
1510
+ );
1511
+ }}
1512
+ className="h-7 flex-1 rounded-md border border-[#d6d3c6]/40 dark:border-border/30 bg-white/60 dark:bg-white/5 px-2 font-mono text-[11px] text-[#1e2329] dark:text-[hsl(40,10%,84%)] outline-none focus:border-accent"
1277
1513
  />
1278
- <button
1279
- type="button"
1280
- className="mt-0.5 shrink-0 text-[#9ca3af] opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-500"
1281
- onClick={() => handleRemoveStyleEntry(key, index)}
1282
- title={t("characterview.remove")}
1283
- >
1284
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="M2 2l6 6M8 2l-6 6" /></svg>
1285
- </button>
1286
1514
  </div>
1287
- ))
1288
- ) : (
1289
- <div className="rounded-md border border-dashed border-[#d6d3c6]/60 dark:border-border/30 px-3 py-2 text-[11px] text-[#9ca3af] dark:text-muted">
1290
- {STYLE_SECTION_EMPTY_STATES[key]}
1291
- </div>
1292
- )}
1515
+ ))}
1516
+ </div>
1293
1517
  </div>
1294
- </div>
1295
- <div className="flex items-center gap-2">
1296
- <Input
1297
- type="text"
1298
- value={pendingStyleEntries[key]}
1299
- placeholder={STYLE_SECTION_PLACEHOLDERS[key]}
1300
- onChange={(e) => handlePendingStyleEntryChange(key, e.target.value)}
1301
- onKeyDown={(e) => {
1302
- if (e.key === "Enter") {
1303
- e.preventDefault();
1304
- handleAddStyleEntry(key);
1305
- }
1306
- }}
1307
- className="h-7 min-w-0 flex-1 rounded-md border-[#d6d3c6]/60 dark:border-border/30 bg-white/50 dark:bg-white/5 text-xs text-[#1e2329] dark:text-[hsl(40,10%,84%)] focus-visible:border-accent focus-visible:ring-accent/50"
1308
- data-testid={`style-entry-input-${key}`}
1309
- />
1310
- <Button
1311
- variant="ghost"
1312
- size="sm"
1313
- className="h-7 shrink-0 rounded-md px-2 text-[10px] font-bold text-accent"
1314
- onClick={() => handleAddStyleEntry(key)}
1315
- disabled={!pendingStyleEntries[key].trim()}
1316
- >
1317
- + add
1318
- </Button>
1319
- </div>
1518
+ ))}
1519
+ {(d.messageExamples ?? []).length === 0 && (
1520
+ <div className="rounded-md border border-dashed border-[#d6d3c6]/60 dark:border-border/30 py-2 text-center text-[11px] text-[#9ca3af] dark:text-muted">
1521
+ {t("characterview.noChatExamplesYet")}
1522
+ </div>
1523
+ )}
1320
1524
  </div>
1321
- );
1322
- })}
1323
- </div>
1324
- )}
1525
+ </div>
1526
+ )}
1325
1527
 
1326
- {/* ── Chat Examples (About Me + Examples mode) ── */}
1327
- {activeSection === "aboutMe" && customizeStep === "examples" && (
1328
- <div className="flex flex-col gap-2" data-testid="character-chat-examples-card">
1329
- <div className="flex items-center justify-between">
1330
- <span className="text-xs font-medium text-[#6d737a] dark:text-muted">{t("characterview.chatExamples")}</span>
1331
- <Button
1332
- variant="ghost"
1333
- size="sm"
1334
- className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1335
- onClick={() => void handleGenerate("chatExamples", "replace")}
1336
- disabled={generating === "chatExamples"}
1528
+ {/* ── Post Examples (Directions + Examples mode) ── */}
1529
+ {activeSection === "directions" &&
1530
+ customizeStep === "examples" && (
1531
+ <div
1532
+ className="flex flex-col gap-2"
1533
+ data-testid="character-post-examples-card"
1337
1534
  >
1338
- {generating === "chatExamples" ? "generating..." : "generate"}
1339
- </Button>
1340
- </div>
1341
- <div className={`${scrollPaneCls} flex flex-col gap-2`}>
1342
- {(d.messageExamples ?? []).map((convo, ci) => (
1535
+ <div className="flex items-center justify-between">
1536
+ <span className="text-xs font-medium text-[#6d737a] dark:text-muted">
1537
+ {t("characterview.postExamples")}
1538
+ </span>
1539
+ <Button
1540
+ variant="ghost"
1541
+ size="sm"
1542
+ className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1543
+ onClick={() =>
1544
+ void handleGenerate("postExamples", "replace")
1545
+ }
1546
+ disabled={generating === "postExamples"}
1547
+ >
1548
+ {generating === "postExamples"
1549
+ ? "generating..."
1550
+ : "generate"}
1551
+ </Button>
1552
+ </div>
1343
1553
  <div
1344
- key={convo.examples.map((msg) => `${msg.name}:${msg.content?.text ?? ""}`).join("|")}
1345
- className="rounded-lg border border-[#d6d3c6]/40 dark:border-border/30 p-2.5"
1554
+ className={`${scrollPaneCls} flex flex-col gap-1.5`}
1346
1555
  >
1347
- <div className="mb-2 flex items-center justify-between">
1348
- <span className="text-[9px] font-bold uppercase tracking-widest text-[#9ca3af] dark:text-muted/60">
1349
- {t("characterview.conversation")} {ci + 1}
1350
- </span>
1351
- <button
1352
- type="button"
1353
- className="text-[#9ca3af] hover:text-red-500 transition-colors"
1354
- onClick={() => {
1355
- const updated = [...(d.messageExamples ?? [])];
1356
- updated.splice(ci, 1);
1357
- handleFieldEdit("messageExamples", updated);
1358
- }}
1359
- >
1360
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="M2 2l6 6M8 2l-6 6" /></svg>
1361
- </button>
1362
- </div>
1363
- <div className="flex flex-col gap-1.5">
1364
- {convo.examples.map((msg, mi) => (
1365
- <div key={`${msg.name}:${msg.content?.text ?? ""}`} className="flex items-center gap-2">
1366
- <span className={`w-10 shrink-0 text-right text-[9px] font-bold uppercase tracking-wider ${msg.name === "{{user1}}" ? "text-[#9ca3af] dark:text-muted" : "text-accent"}`}>
1367
- {msg.name === "{{user1}}" ? "user" : "agent"}
1368
- </span>
1556
+ {(d.postExamples ?? []).map(
1557
+ (post: string, pi: number) => (
1558
+ <div
1559
+ key={post || `post-${pi}`}
1560
+ className="group flex items-center gap-1.5"
1561
+ >
1369
1562
  <input
1370
1563
  type="text"
1371
- value={msg.content?.text ?? ""}
1564
+ value={post}
1372
1565
  onChange={(e) => {
1373
- const updated = [...(d.messageExamples ?? [])];
1374
- const convoClone = { examples: [...updated[ci].examples] };
1375
- convoClone.examples[mi] = { ...convoClone.examples[mi], content: { text: e.target.value } };
1376
- updated[ci] = convoClone;
1377
- handleFieldEdit("messageExamples", updated);
1566
+ const updated = [...(d.postExamples ?? [])];
1567
+ updated[pi] = e.target.value;
1568
+ handleFieldEdit("postExamples", updated);
1378
1569
  }}
1379
1570
  className="h-7 flex-1 rounded-md border border-[#d6d3c6]/40 dark:border-border/30 bg-white/60 dark:bg-white/5 px-2 font-mono text-[11px] text-[#1e2329] dark:text-[hsl(40,10%,84%)] outline-none focus:border-accent"
1380
1571
  />
1572
+ <button
1573
+ type="button"
1574
+ className="shrink-0 text-[#9ca3af] opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-500"
1575
+ onClick={() => {
1576
+ const updated = [...(d.postExamples ?? [])];
1577
+ updated.splice(pi, 1);
1578
+ handleFieldEdit("postExamples", updated);
1579
+ }}
1580
+ >
1581
+ <svg
1582
+ width="10"
1583
+ height="10"
1584
+ viewBox="0 0 10 10"
1585
+ fill="none"
1586
+ stroke="currentColor"
1587
+ strokeWidth="1.5"
1588
+ strokeLinecap="round"
1589
+ aria-label={t("characterview.remove")}
1590
+ >
1591
+ <path d="M2 2l6 6M8 2l-6 6" />
1592
+ </svg>
1593
+ </button>
1381
1594
  </div>
1382
- ))}
1383
- </div>
1384
- </div>
1385
- ))}
1386
- {(d.messageExamples ?? []).length === 0 && (
1387
- <div className="rounded-md border border-dashed border-[#d6d3c6]/60 dark:border-border/30 py-2 text-center text-[11px] text-[#9ca3af] dark:text-muted">
1388
- {t("characterview.noChatExamplesYet")}
1595
+ ),
1596
+ )}
1597
+ {(d.postExamples ?? []).length === 0 && (
1598
+ <div className="rounded-md border border-dashed border-[#d6d3c6]/60 dark:border-border/30 py-2 text-center text-[11px] text-[#9ca3af] dark:text-muted">
1599
+ {t("characterview.noPostExamplesYet")}
1600
+ </div>
1601
+ )}
1389
1602
  </div>
1390
- )}
1391
- </div>
1392
- </div>
1393
- )}
1394
-
1395
- {/* ── Post Examples (Directions + Examples mode) ── */}
1396
- {activeSection === "directions" && customizeStep === "examples" && (
1397
- <div className="flex flex-col gap-2" data-testid="character-post-examples-card">
1398
- <div className="flex items-center justify-between">
1399
- <span className="text-xs font-medium text-[#6d737a] dark:text-muted">{t("characterview.postExamples")}</span>
1400
- <Button
1401
- variant="ghost"
1402
- size="sm"
1403
- className="h-6 rounded-md px-2 text-[10px] font-bold text-accent"
1404
- onClick={() => void handleGenerate("postExamples", "replace")}
1405
- disabled={generating === "postExamples"}
1406
- >
1407
- {generating === "postExamples" ? "generating..." : "generate"}
1408
- </Button>
1409
- </div>
1410
- <div className={`${scrollPaneCls} flex flex-col gap-1.5`}>
1411
- {(d.postExamples ?? []).map((post: string, pi: number) => (
1412
- <div key={post || `post-${pi}`} className="group flex items-center gap-1.5">
1413
- <input
1414
- type="text"
1415
- value={post}
1416
- onChange={(e) => {
1417
- const updated = [...(d.postExamples ?? [])];
1418
- updated[pi] = e.target.value;
1419
- handleFieldEdit("postExamples", updated);
1420
- }}
1421
- className="h-7 flex-1 rounded-md border border-[#d6d3c6]/40 dark:border-border/30 bg-white/60 dark:bg-white/5 px-2 font-mono text-[11px] text-[#1e2329] dark:text-[hsl(40,10%,84%)] outline-none focus:border-accent"
1422
- />
1603
+ <div className="border-t border-[#d6d3c6]/40 dark:border-border/30 pt-2">
1423
1604
  <button
1424
1605
  type="button"
1425
- className="shrink-0 text-[#9ca3af] opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-500"
1606
+ className="text-[10px] font-bold text-accent hover:underline"
1426
1607
  onClick={() => {
1427
- const updated = [...(d.postExamples ?? [])];
1428
- updated.splice(pi, 1);
1608
+ const updated = [...(d.postExamples ?? []), ""];
1429
1609
  handleFieldEdit("postExamples", updated);
1430
1610
  }}
1431
1611
  >
1432
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="M2 2l6 6M8 2l-6 6" /></svg>
1612
+ {t("characterview.AddPost")}
1433
1613
  </button>
1434
1614
  </div>
1435
- ))}
1436
- {(d.postExamples ?? []).length === 0 && (
1437
- <div className="rounded-md border border-dashed border-[#d6d3c6]/60 dark:border-border/30 py-2 text-center text-[11px] text-[#9ca3af] dark:text-muted">
1438
- {t("characterview.noPostExamplesYet")}
1439
- </div>
1440
- )}
1441
- </div>
1442
- <div className="border-t border-[#d6d3c6]/40 dark:border-border/30 pt-2">
1443
- <button
1444
- type="button"
1445
- className="text-[10px] font-bold text-accent hover:underline"
1446
- onClick={() => {
1447
- const updated = [...(d.postExamples ?? []), ""];
1448
- handleFieldEdit("postExamples", updated);
1449
- }}
1450
- >
1451
- {t("characterview.AddPost")}
1452
- </button>
1453
- </div>
1454
- </div>
1455
- )}
1615
+ </div>
1616
+ )}
1617
+ </div>
1456
1618
  </div>
1457
- </div>
1458
1619
 
1459
- {/* ── Sidebar (right) ── */}
1460
- <div
1461
- className={`${bookSidebarCls} flex w-20 flex-col items-center rounded-r-[1.75rem] border-l border-white/[0.08] py-3`}
1462
- role="tablist"
1463
- aria-orientation="vertical"
1464
- >
1465
- <div className="pointer-events-none absolute inset-x-2 top-2 h-10 rounded-full bg-[linear-gradient(180deg,rgba(255,255,255,0.12)_0%,rgba(255,255,255,0)_100%)] blur-sm" />
1466
- <div className="pointer-events-none absolute inset-y-0 left-0 w-px bg-white/[0.08]" />
1467
- <div className="pointer-events-none absolute inset-y-0 right-0 w-px bg-black/35" />
1468
- {SIDEBAR_TABS.map(({ key, icon: Icon, labelKey }) => {
1469
- const isActive = activeSection === key;
1470
- return (
1471
- <button
1472
- key={key}
1473
- type="button"
1474
- role="tab"
1475
- aria-selected={isActive}
1476
- className={`relative flex w-full items-center justify-center px-2 py-3.5 transition-colors border-b border-white/[0.06] last:border-b-0 ${
1477
- isActive ? "text-[#f7e7bf]" : "text-[#999da7] hover:text-white/80"
1478
- }`}
1479
- onClick={() => {
1480
- setActiveSection(key);
1481
- if (key === "styleRules" && customizeStep === "examples") {
1482
- setCustomizeStep("core");
1483
- }
1484
- }}
1485
- onKeyDown={(e) => {
1486
- const keys = SIDEBAR_TABS.map((tab) => tab.key);
1487
- const idx = keys.indexOf(key);
1488
- if (e.key === "ArrowDown") {
1489
- e.preventDefault();
1490
- const next = keys[(idx + 1) % keys.length];
1491
- setActiveSection(next);
1492
- } else if (e.key === "ArrowUp") {
1493
- e.preventDefault();
1494
- const prev = keys[(idx - 1 + keys.length) % keys.length];
1495
- setActiveSection(prev);
1496
- }
1497
- }}
1498
- id={`notebook-tab-${key}`}
1499
- data-testid={`notebook-tab-${key}`}
1500
- >
1501
- {isActive && (
1502
- <div className="absolute inset-y-2 left-0 w-[4px] rounded-r-full bg-[#d4a14c] shadow-[0_0_12px_rgba(212,161,76,0.45)]" />
1503
- )}
1504
- <div className="relative z-10 flex flex-col items-center gap-1.5">
1505
- <div
1506
- className={`flex h-10 w-10 items-center justify-center rounded-full border transition-all ${
1507
- isActive
1508
- ? "border-[#f0ce85]/55 bg-[radial-gradient(circle_at_30%_30%,rgba(255,236,195,0.28)_0%,rgba(255,236,195,0.05)_42%,rgba(255,255,255,0)_100%)] shadow-[0_0_18px_rgba(220,176,90,0.22)]"
1509
- : "border-white/10 bg-white/[0.03]"
1510
- }`}
1511
- >
1512
- <Icon className="h-4.5 w-4.5" />
1620
+ {/* ── Sidebar (right) ── */}
1621
+ <div
1622
+ className={`${bookSidebarCls} flex w-20 flex-col items-center rounded-r-[1.75rem] border-l border-white/[0.08] py-3`}
1623
+ role="tablist"
1624
+ aria-orientation="vertical"
1625
+ >
1626
+ <div className="pointer-events-none absolute inset-x-2 top-2 h-10 rounded-full bg-[linear-gradient(180deg,rgba(255,255,255,0.12)_0%,rgba(255,255,255,0)_100%)] blur-sm" />
1627
+ <div className="pointer-events-none absolute inset-y-0 left-0 w-px bg-white/[0.08]" />
1628
+ <div className="pointer-events-none absolute inset-y-0 right-0 w-px bg-black/35" />
1629
+ {SIDEBAR_TABS.map(({ key, icon: Icon, labelKey }) => {
1630
+ const isActive = activeSection === key;
1631
+ return (
1632
+ <button
1633
+ key={key}
1634
+ type="button"
1635
+ role="tab"
1636
+ aria-selected={isActive}
1637
+ className={`relative flex w-full items-center justify-center px-2 py-3.5 transition-colors border-b border-white/[0.06] last:border-b-0 ${
1638
+ isActive
1639
+ ? "text-[#f7e7bf]"
1640
+ : "text-[#999da7] hover:text-white/80"
1641
+ }`}
1642
+ onClick={() => {
1643
+ setActiveSection(key);
1644
+ if (
1645
+ key === "styleRules" &&
1646
+ customizeStep === "examples"
1647
+ ) {
1648
+ setCustomizeStep("core");
1649
+ }
1650
+ }}
1651
+ onKeyDown={(e) => {
1652
+ const keys = SIDEBAR_TABS.map((tab) => tab.key);
1653
+ const idx = keys.indexOf(key);
1654
+ if (e.key === "ArrowDown") {
1655
+ e.preventDefault();
1656
+ const next = keys[(idx + 1) % keys.length];
1657
+ setActiveSection(next);
1658
+ } else if (e.key === "ArrowUp") {
1659
+ e.preventDefault();
1660
+ const prev =
1661
+ keys[(idx - 1 + keys.length) % keys.length];
1662
+ setActiveSection(prev);
1663
+ }
1664
+ }}
1665
+ id={`notebook-tab-${key}`}
1666
+ data-testid={`notebook-tab-${key}`}
1667
+ >
1668
+ {isActive && (
1669
+ <div className="absolute inset-y-2 left-0 w-[4px] rounded-r-full bg-[#d4a14c] shadow-[0_0_12px_rgba(212,161,76,0.45)]" />
1670
+ )}
1671
+ <div className="relative z-10 flex flex-col items-center gap-1.5">
1672
+ <div
1673
+ className={`flex h-10 w-10 items-center justify-center rounded-full border transition-all ${
1674
+ isActive
1675
+ ? "border-[#f0ce85]/55 bg-[radial-gradient(circle_at_30%_30%,rgba(255,236,195,0.28)_0%,rgba(255,236,195,0.05)_42%,rgba(255,255,255,0)_100%)] shadow-[0_0_18px_rgba(220,176,90,0.22)]"
1676
+ : "border-white/10 bg-white/[0.03]"
1677
+ }`}
1678
+ >
1679
+ <Icon className="h-4.5 w-4.5" />
1680
+ </div>
1681
+ <span className="max-w-[4.1rem] text-center text-[8px] font-semibold uppercase tracking-[0.16em] leading-[1.22]">
1682
+ {t(labelKey)}
1683
+ </span>
1513
1684
  </div>
1514
- <span className="max-w-[4.1rem] text-center text-[8px] font-semibold uppercase tracking-[0.16em] leading-[1.22]">
1515
- {t(labelKey)}
1516
- </span>
1517
- </div>
1518
- </button>
1519
- );
1520
- })}
1521
- </div>
1685
+ </button>
1686
+ );
1687
+ })}
1688
+ </div>
1522
1689
  </div>
1523
1690
  </div>
1524
1691
  </div>
@@ -1596,7 +1763,9 @@ export function CharacterView({
1596
1763
  ? handleTestVoice(activeVoicePreset.previewUrl)
1597
1764
  : undefined
1598
1765
  }
1599
- aria-label={voiceTesting ? "Stop voice preview" : "Preview voice"}
1766
+ aria-label={
1767
+ voiceTesting ? "Stop voice preview" : "Preview voice"
1768
+ }
1600
1769
  title={voiceTesting ? "Stop voice preview" : "Preview voice"}
1601
1770
  disabled={!activeVoicePreset || voiceLoading}
1602
1771
  >