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 +7 -0
- package/package.json +3 -1
- package/src/components/chat-view.tsx +190 -4
- package/src/components/conversation-list.tsx +67 -3
- package/src/lib/avatar.ts +96 -0
- package/src/types/dicebear.d.ts +3 -0
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# cue-console
|
|
2
2
|
|
|
3
|
+
[](https://github.com/nmhjklnm/cue-stack)
|
|
4
|
+
[](https://github.com/nmhjklnm/cue-console)
|
|
5
|
+
[](https://github.com/nmhjklnm/cue-mcp)
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/cue-console)
|
|
8
|
+
[](https://www.npmjs.com/package/cue-console)
|
|
9
|
+
|
|
3
10
|
| Mobile | Desktop |
|
|
4
11
|
| --- | --- |
|
|
5
12
|
|  |  |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cue-console",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
+
}
|