cue-console 0.1.6 → 0.1.7

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/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # cue-console
2
2
 
3
+ [![Repo: cue-stack](https://img.shields.io/badge/repo-cue--stack-111827)](https://github.com/nmhjklnm/cue-stack)
4
+ [![Repo: cue-console](https://img.shields.io/badge/repo-cue--console-111827)](https://github.com/nmhjklnm/cue-console)
5
+ [![Repo: cue-mcp](https://img.shields.io/badge/repo-cue--mcp-111827)](https://github.com/nmhjklnm/cue-mcp)
6
+
7
+ [![npm](https://img.shields.io/npm/v/cue-console?label=cue-console&color=CB3837)](https://www.npmjs.com/package/cue-console)
8
+ [![npm downloads](https://img.shields.io/npm/dw/cue-console?color=CB3837)](https://www.npmjs.com/package/cue-console)
9
+
3
10
  | Mobile | Desktop |
4
11
  | --- | --- |
5
12
  | ![Mobile screenshot](./assets/iphone.png) | ![Desktop screenshot](./assets/desktop.png) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cue-console",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Cue Hub console launcher (Next.js UI)",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": ["mcp", "cue", "console", "nextjs"],
@@ -34,6 +34,8 @@
34
34
  "@radix-ui/react-slot": "^1.2.4",
35
35
  "@tailwindcss/postcss": "^4",
36
36
  "@types/node": "^20",
37
+ "@dicebear/core": "^9.2.4",
38
+ "@dicebear/thumbs": "^9.2.4",
37
39
  "better-sqlite3": "^12.5.0",
38
40
  "class-variance-authority": "^0.7.1",
39
41
  "clsx": "^2.1.1",
@@ -24,6 +24,14 @@ import {
24
24
  formatFullTime,
25
25
  getWaitingDuration,
26
26
  } from "@/lib/utils";
27
+ import {
28
+ getOrInitAvatarSeed,
29
+ getOrInitGroupAvatarSeed,
30
+ randomSeed,
31
+ setAvatarSeed,
32
+ setGroupAvatarSeed,
33
+ thumbsAvatarDataUrl,
34
+ } from "@/lib/avatar";
27
35
  import {
28
36
  fetchAgentTimeline,
29
37
  fetchGroupTimeline,
@@ -85,6 +93,15 @@ export function ChatView({ type, id, name, onBack }: ChatViewProps) {
85
93
  const [mentionActive, setMentionActive] = useState(0);
86
94
  const [mentionAtIndex, setMentionAtIndex] = useState<number | null>(null);
87
95
 
96
+ const [avatarUrlMap, setAvatarUrlMap] = useState<Record<string, string>>({});
97
+ const [avatarPickerOpen, setAvatarPickerOpen] = useState(false);
98
+ const [avatarPickerTarget, setAvatarPickerTarget] = useState<
99
+ | { kind: "agent"; id: string }
100
+ | { kind: "group"; id: string }
101
+ | null
102
+ >(null);
103
+ const [avatarCandidates, setAvatarCandidates] = useState<{ seed: string; url: string }[]>([]);
104
+
88
105
  const textareaRef = useRef<HTMLTextAreaElement>(null);
89
106
  const inputWrapRef = useRef<HTMLDivElement>(null);
90
107
  const mentionListRef = useRef<HTMLDivElement>(null);
@@ -194,6 +211,41 @@ export function ChatView({ type, id, name, onBack }: ChatViewProps) {
194
211
  return groupTitle;
195
212
  }, [agentNameMap, groupTitle, id, type]);
196
213
 
214
+ const ensureAvatarUrl = useCallback(async (kind: "agent" | "group", rawId: string) => {
215
+ if (!rawId) return;
216
+ const key = `${kind}:${rawId}`;
217
+ setAvatarUrlMap((prev) => {
218
+ if (prev[key]) return prev;
219
+ return { ...prev, [key]: "" };
220
+ });
221
+
222
+ try {
223
+ const seed =
224
+ kind === "agent" ? getOrInitAvatarSeed(rawId) : getOrInitGroupAvatarSeed(rawId);
225
+ const url = await thumbsAvatarDataUrl(seed);
226
+ setAvatarUrlMap((prev) => ({ ...prev, [key]: url }));
227
+ } catch {
228
+ // ignore
229
+ }
230
+ }, []);
231
+
232
+ const setTargetAvatarSeed = useCallback(
233
+ async (kind: "agent" | "group", rawId: string, seed: string) => {
234
+ if (!rawId) return;
235
+ if (kind === "agent") setAvatarSeed(rawId, seed);
236
+ else setGroupAvatarSeed(rawId, seed);
237
+
238
+ const key = `${kind}:${rawId}`;
239
+ try {
240
+ const url = await thumbsAvatarDataUrl(seed);
241
+ setAvatarUrlMap((prev) => ({ ...prev, [key]: url }));
242
+ } catch {
243
+ // ignore
244
+ }
245
+ },
246
+ []
247
+ );
248
+
197
249
  useEffect(() => {
198
250
  if (type !== "group") return;
199
251
  setGroupTitle(name);
@@ -375,6 +427,38 @@ export function ChatView({ type, id, name, onBack }: ChatViewProps) {
375
427
  void loadNames();
376
428
  }, [id, members, type]);
377
429
 
430
+ useEffect(() => {
431
+ if (type === "agent") {
432
+ void ensureAvatarUrl("agent", id);
433
+ return;
434
+ }
435
+
436
+ // group header avatar
437
+ void ensureAvatarUrl("group", id);
438
+
439
+ // message bubble avatars
440
+ for (const mid of members) {
441
+ void ensureAvatarUrl("agent", mid);
442
+ }
443
+ }, [ensureAvatarUrl, id, members, type]);
444
+
445
+ const openAvatarPicker = useCallback(
446
+ async (target: { kind: "agent" | "group"; id: string }) => {
447
+ setAvatarPickerTarget(target);
448
+ setAvatarPickerOpen(true);
449
+ void ensureAvatarUrl(target.kind, target.id);
450
+
451
+ try {
452
+ const seeds = Array.from({ length: 20 }, () => randomSeed());
453
+ const urls = await Promise.all(seeds.map((s) => thumbsAvatarDataUrl(s)));
454
+ setAvatarCandidates(seeds.map((seed, i) => ({ seed, url: urls[i] || "" })));
455
+ } catch {
456
+ setAvatarCandidates([]);
457
+ }
458
+ },
459
+ [ensureAvatarUrl]
460
+ );
461
+
378
462
  const closeMention = () => {
379
463
  setMentionQuery("");
380
464
  setMentionOpen(false);
@@ -1064,9 +1148,35 @@ export function ChatView({ type, id, name, onBack }: ChatViewProps) {
1064
1148
  <ChevronLeft className="h-5 w-5" />
1065
1149
  </Button>
1066
1150
  )}
1067
- <span className="text-2xl">
1068
- {type === "group" ? "👥" : getAgentEmoji(id)}
1069
- </span>
1151
+ {type === "group" ? (
1152
+ <button
1153
+ type="button"
1154
+ className="h-9 w-9 shrink-0 rounded-full bg-muted overflow-hidden"
1155
+ onClick={() => openAvatarPicker({ kind: "group", id })}
1156
+ title="Change avatar"
1157
+ >
1158
+ {avatarUrlMap[`group:${id}`] ? (
1159
+ <img src={avatarUrlMap[`group:${id}`]} alt="" className="h-full w-full" />
1160
+ ) : (
1161
+ <span className="flex h-full w-full items-center justify-center text-lg">👥</span>
1162
+ )}
1163
+ </button>
1164
+ ) : (
1165
+ <button
1166
+ type="button"
1167
+ className="h-9 w-9 shrink-0 rounded-full bg-muted overflow-hidden"
1168
+ onClick={() => openAvatarPicker({ kind: "agent", id })}
1169
+ title="Change avatar"
1170
+ >
1171
+ {avatarUrlMap[`agent:${id}`] ? (
1172
+ <img src={avatarUrlMap[`agent:${id}`]} alt="" className="h-full w-full" />
1173
+ ) : (
1174
+ <span className="flex h-full w-full items-center justify-center text-lg">
1175
+ {getAgentEmoji(id)}
1176
+ </span>
1177
+ )}
1178
+ </button>
1179
+ )}
1070
1180
  <div className="flex-1">
1071
1181
  {editingTitle ? (
1072
1182
  <input
@@ -1185,6 +1295,7 @@ export function ChatView({ type, id, name, onBack }: ChatViewProps) {
1185
1295
  request={item.request}
1186
1296
  showAgent={type === "group"}
1187
1297
  agentNameMap={agentNameMap}
1298
+ avatarUrlMap={avatarUrlMap}
1188
1299
  showName={!prevSameSender}
1189
1300
  showAvatar={!prevSameSender}
1190
1301
  compact={compact}
@@ -1284,6 +1395,74 @@ export function ChatView({ type, id, name, onBack }: ChatViewProps) {
1284
1395
  )}
1285
1396
  </DialogContent>
1286
1397
  </Dialog>
1398
+
1399
+ <Dialog open={avatarPickerOpen} onOpenChange={setAvatarPickerOpen}>
1400
+ <DialogContent className="max-w-lg glass-surface glass-noise">
1401
+ <DialogHeader>
1402
+ <DialogTitle>Avatar</DialogTitle>
1403
+ </DialogHeader>
1404
+ {avatarPickerTarget && (
1405
+ <div className="flex flex-col gap-4">
1406
+ <div className="flex items-center gap-3">
1407
+ <div className="h-14 w-14 rounded-full bg-muted overflow-hidden">
1408
+ {avatarUrlMap[`${avatarPickerTarget.kind}:${avatarPickerTarget.id}`] ? (
1409
+ <img
1410
+ src={
1411
+ avatarUrlMap[`${avatarPickerTarget.kind}:${avatarPickerTarget.id}`]
1412
+ }
1413
+ alt=""
1414
+ className="h-full w-full"
1415
+ />
1416
+ ) : (
1417
+ <div className="flex h-full w-full items-center justify-center text-xl">
1418
+ {avatarPickerTarget.kind === "group" ? "👥" : getAgentEmoji(id)}
1419
+ </div>
1420
+ )}
1421
+ </div>
1422
+ <div className="flex-1 min-w-0">
1423
+ <p className="text-sm font-semibold truncate">{titleDisplay}</p>
1424
+ <p className="text-xs text-muted-foreground truncate">Click a thumb to apply</p>
1425
+ </div>
1426
+ <Button
1427
+ variant="outline"
1428
+ size="sm"
1429
+ onClick={async () => {
1430
+ const s = randomSeed();
1431
+ await setTargetAvatarSeed(avatarPickerTarget.kind, avatarPickerTarget.id, s);
1432
+ // refresh candidate grid
1433
+ void openAvatarPicker(avatarPickerTarget);
1434
+ }}
1435
+ >
1436
+ Random
1437
+ </Button>
1438
+ </div>
1439
+
1440
+ <div className="max-h-52 overflow-y-auto pr-1">
1441
+ <div className="grid grid-cols-5 gap-2">
1442
+ {avatarCandidates.map((c) => (
1443
+ <button
1444
+ key={c.seed}
1445
+ type="button"
1446
+ className="h-12 w-12 rounded-full bg-muted overflow-hidden hover:ring-2 hover:ring-ring/40"
1447
+ onClick={async () => {
1448
+ await setTargetAvatarSeed(
1449
+ avatarPickerTarget.kind,
1450
+ avatarPickerTarget.id,
1451
+ c.seed
1452
+ );
1453
+ setAvatarPickerOpen(false);
1454
+ }}
1455
+ title="Apply"
1456
+ >
1457
+ {c.url ? <img src={c.url} alt="" className="h-full w-full" /> : null}
1458
+ </button>
1459
+ ))}
1460
+ </div>
1461
+ </div>
1462
+ </div>
1463
+ )}
1464
+ </DialogContent>
1465
+ </Dialog>
1287
1466
  </div>
1288
1467
  );
1289
1468
  }
@@ -1292,6 +1471,7 @@ function MessageBubble({
1292
1471
  request,
1293
1472
  showAgent,
1294
1473
  agentNameMap,
1474
+ avatarUrlMap,
1295
1475
  isHistory,
1296
1476
  showName,
1297
1477
  showAvatar,
@@ -1308,6 +1488,7 @@ function MessageBubble({
1308
1488
  request: CueRequest;
1309
1489
  showAgent?: boolean;
1310
1490
  agentNameMap?: Record<string, string>;
1491
+ avatarUrlMap?: Record<string, string>;
1311
1492
  isHistory?: boolean;
1312
1493
  showName?: boolean;
1313
1494
  showAvatar?: boolean;
@@ -1347,6 +1528,7 @@ function MessageBubble({
1347
1528
  const rawId = request.agent_id || "";
1348
1529
  const displayName = (agentNameMap && rawId ? agentNameMap[rawId] || rawId : rawId) || "";
1349
1530
  const cardMaxWidth = (showAvatar ?? true) ? "calc(100% - 3rem)" : "100%";
1531
+ const avatarUrl = rawId && avatarUrlMap ? avatarUrlMap[`agent:${rawId}`] : "";
1350
1532
 
1351
1533
  return (
1352
1534
  <div
@@ -1374,7 +1556,11 @@ function MessageBubble({
1374
1556
  onMentionAgent?.(agentId);
1375
1557
  }}
1376
1558
  >
1377
- {getAgentEmoji(request.agent_id || "")}
1559
+ {avatarUrl ? (
1560
+ <img src={avatarUrl} alt="" className="h-full w-full rounded-full" />
1561
+ ) : (
1562
+ getAgentEmoji(request.agent_id || "")
1563
+ )}
1378
1564
  </span>
1379
1565
  ) : (
1380
1566
  <span className="h-9 w-9 shrink-0" />
@@ -10,6 +10,11 @@ import {
10
10
  CollapsibleTrigger,
11
11
  } from "@/components/ui/collapsible";
12
12
  import { cn, getAgentEmoji, formatTime, truncateText } from "@/lib/utils";
13
+ import {
14
+ getOrInitAvatarSeed,
15
+ getOrInitGroupAvatarSeed,
16
+ thumbsAvatarDataUrl,
17
+ } from "@/lib/avatar";
13
18
  import {
14
19
  archiveConversations,
15
20
  deleteConversations,
@@ -56,6 +61,7 @@ export function ConversationList({
56
61
  onToggleCollapsed,
57
62
  }: ConversationListProps) {
58
63
  const [items, setItems] = useState<ConversationItem[]>([]);
64
+ const [avatarUrlMap, setAvatarUrlMap] = useState<Record<string, string>>({});
59
65
  const [search, setSearch] = useState("");
60
66
  const [view, setView] = useState<"active" | "archived">("active");
61
67
  const [archivedCount, setArchivedCount] = useState(0);
@@ -76,6 +82,24 @@ export function ConversationList({
76
82
  }
77
83
  >({ open: false });
78
84
 
85
+ const ensureAvatarUrl = useCallback(async (kind: "agent" | "group", rawId: string) => {
86
+ if (!rawId) return;
87
+ const key = `${kind}:${rawId}`;
88
+ setAvatarUrlMap((prev) => {
89
+ if (prev[key]) return prev;
90
+ return { ...prev, [key]: "" };
91
+ });
92
+
93
+ try {
94
+ const seed =
95
+ kind === "agent" ? getOrInitAvatarSeed(rawId) : getOrInitGroupAvatarSeed(rawId);
96
+ const url = await thumbsAvatarDataUrl(seed);
97
+ setAvatarUrlMap((prev) => ({ ...prev, [key]: url }));
98
+ } catch {
99
+ // ignore
100
+ }
101
+ }, []);
102
+
79
103
  const [moreMenu, setMoreMenu] = useState<
80
104
  | { open: false }
81
105
  | {
@@ -115,9 +139,27 @@ export function ConversationList({
115
139
  );
116
140
  };
117
141
 
142
+ const onAvatarSeedUpdated = (evt: Event) => {
143
+ const e = evt as CustomEvent<{ kind: "agent" | "group"; id: string; seed: string }>;
144
+ const kind = e.detail?.kind;
145
+ const rawId = e.detail?.id;
146
+ const seed = e.detail?.seed;
147
+ if (!kind || !rawId || !seed) return;
148
+ void (async () => {
149
+ try {
150
+ const url = await thumbsAvatarDataUrl(seed);
151
+ setAvatarUrlMap((prev) => ({ ...prev, [`${kind}:${rawId}`]: url }));
152
+ } catch {
153
+ // ignore
154
+ }
155
+ })();
156
+ };
157
+
118
158
  window.addEventListener("cuehub:agentDisplayNameUpdated", onAgentNameUpdated);
159
+ window.addEventListener("cue-console:avatarSeedUpdated", onAvatarSeedUpdated);
119
160
  return () => {
120
161
  window.removeEventListener("cuehub:agentDisplayNameUpdated", onAgentNameUpdated);
162
+ window.removeEventListener("cue-console:avatarSeedUpdated", onAvatarSeedUpdated);
121
163
  };
122
164
  }, []);
123
165
 
@@ -128,6 +170,12 @@ export function ConversationList({
128
170
  setArchivedCount(count);
129
171
  }, [view]);
130
172
 
173
+ useEffect(() => {
174
+ for (const it of items) {
175
+ void ensureAvatarUrl(it.type, it.id);
176
+ }
177
+ }, [ensureAvatarUrl, items]);
178
+
131
179
  useEffect(() => {
132
180
  const t0 = setTimeout(() => {
133
181
  void loadData();
@@ -512,6 +560,7 @@ export function ConversationList({
512
560
  <div key={item.id} data-conversation-item="true">
513
561
  <ConversationIconButton
514
562
  item={item}
563
+ avatarUrl={avatarUrlMap[`group:${item.id}`]}
515
564
  isSelected={selectedId === item.id && selectedType === "group"}
516
565
  onClick={() => onSelect(item.id, "group", item.name)}
517
566
  />
@@ -529,6 +578,7 @@ export function ConversationList({
529
578
  <div key={item.id} data-conversation-item="true">
530
579
  <ConversationIconButton
531
580
  item={item}
581
+ avatarUrl={avatarUrlMap[`agent:${item.id}`]}
532
582
  isSelected={selectedId === item.id && selectedType === "agent"}
533
583
  onClick={() => onSelect(item.id, "agent", item.name)}
534
584
  />
@@ -602,6 +652,7 @@ export function ConversationList({
602
652
  >
603
653
  <ConversationItemCard
604
654
  item={item}
655
+ avatarUrl={avatarUrlMap[`group:${item.id}`]}
605
656
  isSelected={selectedId === item.id && selectedType === "group"}
606
657
  bulkMode={bulkMode}
607
658
  checked={selectedKeys.has(conversationKey(item))}
@@ -643,6 +694,7 @@ export function ConversationList({
643
694
  >
644
695
  <ConversationItemCard
645
696
  item={item}
697
+ avatarUrl={avatarUrlMap[`agent:${item.id}`]}
646
698
  isSelected={selectedId === item.id && selectedType === "agent"}
647
699
  bulkMode={bulkMode}
648
700
  checked={selectedKeys.has(conversationKey(item))}
@@ -850,10 +902,12 @@ export function ConversationList({
850
902
 
851
903
  function ConversationIconButton({
852
904
  item,
905
+ avatarUrl,
853
906
  isSelected,
854
907
  onClick,
855
908
  }: {
856
909
  item: ConversationItem;
910
+ avatarUrl?: string;
857
911
  isSelected: boolean;
858
912
  onClick: () => void;
859
913
  }) {
@@ -870,7 +924,11 @@ function ConversationIconButton({
870
924
  onClick={onClick}
871
925
  title={item.displayName}
872
926
  >
873
- <span className="text-xl">{emoji}</span>
927
+ {avatarUrl ? (
928
+ <img src={avatarUrl} alt="" className="h-7 w-7 rounded-full" />
929
+ ) : (
930
+ <span className="text-xl">{emoji}</span>
931
+ )}
874
932
  {item.pendingCount > 0 && (
875
933
  <span className="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full bg-destructive ring-2 ring-sidebar" />
876
934
  )}
@@ -880,6 +938,7 @@ function ConversationIconButton({
880
938
 
881
939
  function ConversationItemCard({
882
940
  item,
941
+ avatarUrl,
883
942
  isSelected,
884
943
  onClick,
885
944
  bulkMode,
@@ -888,6 +947,7 @@ function ConversationItemCard({
888
947
  view,
889
948
  }: {
890
949
  item: ConversationItem;
950
+ avatarUrl?: string;
891
951
  isSelected: boolean;
892
952
  onClick: () => void;
893
953
  bulkMode?: boolean;
@@ -918,8 +978,12 @@ function ConversationItemCard({
918
978
  />
919
979
  </span>
920
980
  )}
921
- <span className="relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/55 ring-1 ring-white/40 text-[18px]">
922
- {emoji}
981
+ <span className="relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/55 ring-1 ring-white/40 text-[18px] overflow-hidden">
982
+ {avatarUrl ? (
983
+ <img src={avatarUrl} alt="" className="h-full w-full" />
984
+ ) : (
985
+ emoji
986
+ )}
923
987
  {item.pendingCount > 0 && (
924
988
  <span className="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full bg-destructive ring-2 ring-background" />
925
989
  )}
@@ -0,0 +1,96 @@
1
+ export function safeLocalStorageGet(key: string): string | null {
2
+ try {
3
+ if (typeof window === "undefined") return null;
4
+ return window.localStorage.getItem(key);
5
+ } catch {
6
+ return null;
7
+ }
8
+ }
9
+
10
+ export function safeLocalStorageSet(key: string, value: string): void {
11
+ try {
12
+ if (typeof window === "undefined") return;
13
+ window.localStorage.setItem(key, value);
14
+ } catch {
15
+ // ignore
16
+ }
17
+ }
18
+
19
+ export function randomSeed(): string {
20
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
21
+ return crypto.randomUUID();
22
+ }
23
+ return String(Date.now()) + "-" + Math.random().toString(16).slice(2);
24
+ }
25
+
26
+ export function avatarSeedKey(agentId: string): string {
27
+ return `cue-console.avatarSeed.agent.${agentId}`;
28
+ }
29
+
30
+ export function groupAvatarSeedKey(groupId: string): string {
31
+ return `cue-console.avatarSeed.group.${groupId}`;
32
+ }
33
+
34
+ export function notifyAvatarSeedUpdated(
35
+ kind: "agent" | "group",
36
+ id: string,
37
+ seed: string
38
+ ): void {
39
+ try {
40
+ if (typeof window === "undefined") return;
41
+ window.dispatchEvent(
42
+ new CustomEvent("cue-console:avatarSeedUpdated", {
43
+ detail: { kind, id, seed },
44
+ })
45
+ );
46
+ } catch {
47
+ // ignore
48
+ }
49
+ }
50
+
51
+ export function getOrInitAvatarSeed(agentId: string): string {
52
+ const key = avatarSeedKey(agentId);
53
+ const existing = safeLocalStorageGet(key);
54
+ if (existing) return existing;
55
+ const seed = randomSeed();
56
+ safeLocalStorageSet(key, seed);
57
+ return seed;
58
+ }
59
+
60
+ export function getOrInitGroupAvatarSeed(groupId: string): string {
61
+ const key = groupAvatarSeedKey(groupId);
62
+ const existing = safeLocalStorageGet(key);
63
+ if (existing) return existing;
64
+ const seed = randomSeed();
65
+ safeLocalStorageSet(key, seed);
66
+ return seed;
67
+ }
68
+
69
+ export function setAvatarSeed(agentId: string, seed: string): void {
70
+ safeLocalStorageSet(avatarSeedKey(agentId), seed);
71
+ notifyAvatarSeedUpdated("agent", agentId, seed);
72
+ }
73
+
74
+ export function setGroupAvatarSeed(groupId: string, seed: string): void {
75
+ safeLocalStorageSet(groupAvatarSeedKey(groupId), seed);
76
+ notifyAvatarSeedUpdated("group", groupId, seed);
77
+ }
78
+
79
+ export async function thumbsAvatarDataUrl(seed: string): Promise<string> {
80
+ const [{ createAvatar }, thumbsStyle] = await Promise.all([
81
+ import("@dicebear/core"),
82
+ import("@dicebear/thumbs"),
83
+ ]);
84
+
85
+ const svg = createAvatar(thumbsStyle as any, {
86
+ seed,
87
+ }).toString();
88
+
89
+ // Use base64 to avoid data-uri escaping issues (spaces/newlines/quotes) that can break rendering.
90
+ const utf8 = new TextEncoder().encode(svg);
91
+ let binary = "";
92
+ for (let i = 0; i < utf8.length; i++) binary += String.fromCharCode(utf8[i]);
93
+ const b64 = btoa(binary);
94
+
95
+ return `data:image/svg+xml;base64,${b64}`;
96
+ }
@@ -0,0 +1,3 @@
1
+ declare module "@dicebear/core";
2
+
3
+ declare module "@dicebear/thumbs";