cue-console 0.1.0
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/LICENSE +201 -0
- package/README.md +33 -0
- package/bin/cue-console.js +82 -0
- package/components.json +22 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +11 -0
- package/package.json +61 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +304 -0
- package/src/app/layout.tsx +36 -0
- package/src/app/page.tsx +109 -0
- package/src/components/chat-composer.tsx +493 -0
- package/src/components/chat-view.tsx +1463 -0
- package/src/components/conversation-list.tsx +525 -0
- package/src/components/create-group-dialog.tsx +220 -0
- package/src/components/markdown-renderer.tsx +53 -0
- package/src/components/payload-card.tsx +275 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +63 -0
- package/src/components/ui/collapsible.tsx +46 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/scroll-area.tsx +59 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/lib/actions.ts +300 -0
- package/src/lib/db.ts +581 -0
- package/src/lib/types.ts +89 -0
- package/src/lib/utils.ts +135 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
Collapsible,
|
|
9
|
+
CollapsibleContent,
|
|
10
|
+
CollapsibleTrigger,
|
|
11
|
+
} from "@/components/ui/collapsible";
|
|
12
|
+
import { cn, getAgentEmoji, formatTime, truncateText } from "@/lib/utils";
|
|
13
|
+
import { fetchConversationList, type ConversationItem } from "@/lib/actions";
|
|
14
|
+
import {
|
|
15
|
+
Plus,
|
|
16
|
+
MessageCircle,
|
|
17
|
+
Search,
|
|
18
|
+
ChevronDown,
|
|
19
|
+
Users,
|
|
20
|
+
Bot,
|
|
21
|
+
ChevronLeft,
|
|
22
|
+
ChevronRight,
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import { Input } from "@/components/ui/input";
|
|
25
|
+
|
|
26
|
+
const HIDDEN_CONVERSATIONS_STORAGE_KEY = "cuehub:hiddenConversations";
|
|
27
|
+
|
|
28
|
+
function conversationKey(item: Pick<ConversationItem, "type" | "id">) {
|
|
29
|
+
return `${item.type}:${item.id}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ConversationListProps {
|
|
33
|
+
selectedId: string | null;
|
|
34
|
+
selectedType: "agent" | "group" | null;
|
|
35
|
+
onSelect: (id: string, type: "agent" | "group", name: string) => void;
|
|
36
|
+
onCreateGroup: () => void;
|
|
37
|
+
collapsed?: boolean;
|
|
38
|
+
onToggleCollapsed?: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ConversationList({
|
|
42
|
+
selectedId,
|
|
43
|
+
selectedType,
|
|
44
|
+
onSelect,
|
|
45
|
+
onCreateGroup,
|
|
46
|
+
collapsed,
|
|
47
|
+
onToggleCollapsed,
|
|
48
|
+
}: ConversationListProps) {
|
|
49
|
+
const [items, setItems] = useState<ConversationItem[]>([]);
|
|
50
|
+
const [search, setSearch] = useState("");
|
|
51
|
+
const [groupsOpen, setGroupsOpen] = useState(true);
|
|
52
|
+
const [agentsOpen, setAgentsOpen] = useState(true);
|
|
53
|
+
const [hiddenKeys, setHiddenKeys] = useState<Set<string>>(new Set());
|
|
54
|
+
const [menu, setMenu] = useState<
|
|
55
|
+
| { open: false }
|
|
56
|
+
| {
|
|
57
|
+
open: true;
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
item: ConversationItem | null;
|
|
61
|
+
}
|
|
62
|
+
>({ open: false });
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
try {
|
|
66
|
+
const raw = localStorage.getItem(HIDDEN_CONVERSATIONS_STORAGE_KEY);
|
|
67
|
+
const parsed = raw ? (JSON.parse(raw) as unknown) : [];
|
|
68
|
+
if (Array.isArray(parsed)) {
|
|
69
|
+
const next = new Set(parsed.filter((v) => typeof v === "string"));
|
|
70
|
+
setHiddenKeys(next);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
setHiddenKeys(new Set());
|
|
74
|
+
}
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!menu.open) return;
|
|
79
|
+
const onPointerDown = () => setMenu({ open: false });
|
|
80
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
81
|
+
if (e.key === "Escape") setMenu({ open: false });
|
|
82
|
+
};
|
|
83
|
+
window.addEventListener("pointerdown", onPointerDown);
|
|
84
|
+
window.addEventListener("keydown", onKeyDown);
|
|
85
|
+
return () => {
|
|
86
|
+
window.removeEventListener("pointerdown", onPointerDown);
|
|
87
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
88
|
+
};
|
|
89
|
+
}, [menu.open]);
|
|
90
|
+
|
|
91
|
+
const persistHidden = useCallback((next: Set<string>) => {
|
|
92
|
+
setHiddenKeys(next);
|
|
93
|
+
try {
|
|
94
|
+
localStorage.setItem(
|
|
95
|
+
HIDDEN_CONVERSATIONS_STORAGE_KEY,
|
|
96
|
+
JSON.stringify(Array.from(next))
|
|
97
|
+
);
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const hideConversation = useCallback(
|
|
104
|
+
(item: ConversationItem) => {
|
|
105
|
+
const key = conversationKey(item);
|
|
106
|
+
persistHidden(new Set([...hiddenKeys, key]));
|
|
107
|
+
},
|
|
108
|
+
[hiddenKeys, persistHidden]
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const restoreAllHidden = useCallback(() => {
|
|
112
|
+
persistHidden(new Set());
|
|
113
|
+
}, [persistHidden]);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const onAgentNameUpdated = (evt: Event) => {
|
|
117
|
+
const e = evt as CustomEvent<{ agentId: string; displayName: string }>;
|
|
118
|
+
const agentId = e.detail?.agentId;
|
|
119
|
+
const displayName = (e.detail?.displayName || "").trim();
|
|
120
|
+
if (!agentId || !displayName) return;
|
|
121
|
+
setItems((prev) =>
|
|
122
|
+
prev.map((it) =>
|
|
123
|
+
it.type === "agent" && it.id === agentId ? { ...it, displayName } : it
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
window.addEventListener("cuehub:agentDisplayNameUpdated", onAgentNameUpdated);
|
|
129
|
+
return () => {
|
|
130
|
+
window.removeEventListener("cuehub:agentDisplayNameUpdated", onAgentNameUpdated);
|
|
131
|
+
};
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
const loadData = useCallback(async () => {
|
|
135
|
+
const data = await fetchConversationList();
|
|
136
|
+
setItems(data);
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const t0 = setTimeout(() => {
|
|
141
|
+
void loadData();
|
|
142
|
+
}, 0);
|
|
143
|
+
|
|
144
|
+
const tick = () => {
|
|
145
|
+
if (document.visibilityState !== "visible") return;
|
|
146
|
+
void loadData();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const interval = setInterval(tick, 3000);
|
|
150
|
+
|
|
151
|
+
const onVisibilityChange = () => {
|
|
152
|
+
if (document.visibilityState === "visible") tick();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
156
|
+
|
|
157
|
+
return () => {
|
|
158
|
+
clearTimeout(t0);
|
|
159
|
+
clearInterval(interval);
|
|
160
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
161
|
+
};
|
|
162
|
+
}, [loadData]);
|
|
163
|
+
|
|
164
|
+
const filtered = items
|
|
165
|
+
.filter((item) => !hiddenKeys.has(conversationKey(item)))
|
|
166
|
+
.filter((item) => item.displayName.toLowerCase().includes(search.toLowerCase()));
|
|
167
|
+
|
|
168
|
+
const groups = filtered.filter((i) => i.type === "group");
|
|
169
|
+
const agents = filtered.filter((i) => i.type === "agent");
|
|
170
|
+
|
|
171
|
+
const groupsPendingTotal = groups.reduce((sum, g) => sum + g.pendingCount, 0);
|
|
172
|
+
const agentsPendingTotal = agents.reduce((sum, a) => sum + a.pendingCount, 0);
|
|
173
|
+
|
|
174
|
+
const isCollapsed = !!collapsed;
|
|
175
|
+
|
|
176
|
+
const collapsedGroups = useMemo(
|
|
177
|
+
() => items.filter((i) => i.type === "group" && !hiddenKeys.has(conversationKey(i))),
|
|
178
|
+
[items, hiddenKeys]
|
|
179
|
+
);
|
|
180
|
+
const collapsedAgents = useMemo(
|
|
181
|
+
() => items.filter((i) => i.type === "agent" && !hiddenKeys.has(conversationKey(i))),
|
|
182
|
+
[items, hiddenKeys]
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const onItemContextMenu = useCallback(
|
|
186
|
+
(e: React.MouseEvent, item: ConversationItem) => {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
setMenu({ open: true, x: e.clientX, y: e.clientY, item });
|
|
189
|
+
},
|
|
190
|
+
[]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const onEmptyContextMenu = useCallback(
|
|
194
|
+
(e: React.MouseEvent) => {
|
|
195
|
+
const target = e.target as HTMLElement | null;
|
|
196
|
+
if (target?.closest?.("[data-conversation-item='true']")) return;
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
setMenu({ open: true, x: e.clientX, y: e.clientY, item: null });
|
|
199
|
+
},
|
|
200
|
+
[]
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
className={cn(
|
|
206
|
+
"flex h-full min-h-0 flex-col shrink-0",
|
|
207
|
+
"border-r border-border/60",
|
|
208
|
+
"glass-surface-opaque glass-noise",
|
|
209
|
+
"transition-[width] duration-200 ease-out",
|
|
210
|
+
isCollapsed ? "w-16" : "w-72"
|
|
211
|
+
)}
|
|
212
|
+
>
|
|
213
|
+
{/* Header */}
|
|
214
|
+
<div
|
|
215
|
+
className={cn(
|
|
216
|
+
"flex items-center border-b border-border/60",
|
|
217
|
+
isCollapsed ? "px-2 py-3" : "px-4 py-3"
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
{isCollapsed ? (
|
|
221
|
+
<div className="flex w-full flex-col items-center justify-center gap-2">
|
|
222
|
+
<Button
|
|
223
|
+
variant="ghost"
|
|
224
|
+
size="icon"
|
|
225
|
+
className="h-9 w-9"
|
|
226
|
+
onClick={onToggleCollapsed}
|
|
227
|
+
disabled={!onToggleCollapsed}
|
|
228
|
+
title="Expand sidebar"
|
|
229
|
+
>
|
|
230
|
+
<ChevronRight className="h-5 w-5" />
|
|
231
|
+
</Button>
|
|
232
|
+
<Button
|
|
233
|
+
variant="ghost"
|
|
234
|
+
size="icon"
|
|
235
|
+
className="h-9 w-9"
|
|
236
|
+
onClick={onCreateGroup}
|
|
237
|
+
title="Create group"
|
|
238
|
+
>
|
|
239
|
+
<Plus className="h-5 w-5" />
|
|
240
|
+
</Button>
|
|
241
|
+
</div>
|
|
242
|
+
) : (
|
|
243
|
+
<div className="flex w-full items-center justify-between gap-2">
|
|
244
|
+
<h1 className="text-lg font-semibold">Cue Hub</h1>
|
|
245
|
+
<div className="flex items-center gap-1">
|
|
246
|
+
<Button
|
|
247
|
+
variant="ghost"
|
|
248
|
+
size="icon"
|
|
249
|
+
onClick={onToggleCollapsed}
|
|
250
|
+
disabled={!onToggleCollapsed}
|
|
251
|
+
title="Collapse sidebar"
|
|
252
|
+
>
|
|
253
|
+
<ChevronLeft className="h-5 w-5" />
|
|
254
|
+
</Button>
|
|
255
|
+
<Button
|
|
256
|
+
variant="ghost"
|
|
257
|
+
size="icon"
|
|
258
|
+
onClick={onCreateGroup}
|
|
259
|
+
title="Create group"
|
|
260
|
+
>
|
|
261
|
+
<Plus className="h-5 w-5" />
|
|
262
|
+
</Button>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Search */}
|
|
269
|
+
{!isCollapsed && (
|
|
270
|
+
<div className="px-3 py-2">
|
|
271
|
+
<div className="relative">
|
|
272
|
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
273
|
+
<Input
|
|
274
|
+
placeholder="Search"
|
|
275
|
+
className="pl-8 h-9 bg-white/45 border-white/40 focus-visible:border-ring"
|
|
276
|
+
value={search}
|
|
277
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{/* List */}
|
|
284
|
+
{isCollapsed ? (
|
|
285
|
+
<ScrollArea className="flex-1 min-h-0 px-2">
|
|
286
|
+
<div className="py-2 space-y-2" onContextMenu={onEmptyContextMenu}>
|
|
287
|
+
{collapsedGroups.length > 0 && (
|
|
288
|
+
<div className="space-y-1">
|
|
289
|
+
<div className="flex items-center justify-center">
|
|
290
|
+
<Users className="h-4 w-4 text-muted-foreground" />
|
|
291
|
+
</div>
|
|
292
|
+
{collapsedGroups.map((item) => (
|
|
293
|
+
<div
|
|
294
|
+
key={item.id}
|
|
295
|
+
data-conversation-item="true"
|
|
296
|
+
onContextMenu={(e) => onItemContextMenu(e, item)}
|
|
297
|
+
>
|
|
298
|
+
<ConversationIconButton
|
|
299
|
+
item={item}
|
|
300
|
+
isSelected={selectedId === item.id && selectedType === "group"}
|
|
301
|
+
onClick={() => onSelect(item.id, "group", item.name)}
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
{collapsedAgents.length > 0 && (
|
|
309
|
+
<div className="space-y-1">
|
|
310
|
+
<div className="flex items-center justify-center">
|
|
311
|
+
<Bot className="h-4 w-4 text-muted-foreground" />
|
|
312
|
+
</div>
|
|
313
|
+
{collapsedAgents.map((item) => (
|
|
314
|
+
<div
|
|
315
|
+
key={item.id}
|
|
316
|
+
data-conversation-item="true"
|
|
317
|
+
onContextMenu={(e) => onItemContextMenu(e, item)}
|
|
318
|
+
>
|
|
319
|
+
<ConversationIconButton
|
|
320
|
+
item={item}
|
|
321
|
+
isSelected={selectedId === item.id && selectedType === "agent"}
|
|
322
|
+
onClick={() => onSelect(item.id, "agent", item.name)}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{items.length === 0 && (
|
|
330
|
+
<div className="flex flex-col items-center justify-center py-10 text-muted-foreground">
|
|
331
|
+
<MessageCircle className="mb-2 h-7 w-7" />
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
</ScrollArea>
|
|
336
|
+
) : (
|
|
337
|
+
<ScrollArea className="flex-1 min-h-0 px-2">
|
|
338
|
+
<div onContextMenu={onEmptyContextMenu}>
|
|
339
|
+
{/* Groups Section */}
|
|
340
|
+
{groups.length > 0 && (
|
|
341
|
+
<Collapsible open={groupsOpen} onOpenChange={setGroupsOpen} className="mb-1">
|
|
342
|
+
<CollapsibleTrigger className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-muted-foreground hover:bg-accent/50 transition-colors">
|
|
343
|
+
<ChevronDown
|
|
344
|
+
className={cn(
|
|
345
|
+
"h-4 w-4 shrink-0 transition-transform duration-200",
|
|
346
|
+
groupsOpen ? "" : "-rotate-90"
|
|
347
|
+
)}
|
|
348
|
+
/>
|
|
349
|
+
<Users className="h-4 w-4 shrink-0" />
|
|
350
|
+
<span>Groups</span>
|
|
351
|
+
{groupsPendingTotal > 0 && (
|
|
352
|
+
<Badge variant="destructive" className="h-5 min-w-5 px-1.5 text-xs">
|
|
353
|
+
{groupsPendingTotal}
|
|
354
|
+
</Badge>
|
|
355
|
+
)}
|
|
356
|
+
</CollapsibleTrigger>
|
|
357
|
+
<CollapsibleContent className="mt-1 space-y-0.5">
|
|
358
|
+
{groups.map((item) => (
|
|
359
|
+
<div
|
|
360
|
+
key={item.id}
|
|
361
|
+
data-conversation-item="true"
|
|
362
|
+
onContextMenu={(e) => onItemContextMenu(e, item)}
|
|
363
|
+
>
|
|
364
|
+
<ConversationItemCard
|
|
365
|
+
item={item}
|
|
366
|
+
isSelected={selectedId === item.id && selectedType === "group"}
|
|
367
|
+
onClick={() => onSelect(item.id, "group", item.name)}
|
|
368
|
+
/>
|
|
369
|
+
</div>
|
|
370
|
+
))}
|
|
371
|
+
</CollapsibleContent>
|
|
372
|
+
</Collapsible>
|
|
373
|
+
)}
|
|
374
|
+
|
|
375
|
+
{/* Agents Section */}
|
|
376
|
+
{agents.length > 0 && (
|
|
377
|
+
<Collapsible open={agentsOpen} onOpenChange={setAgentsOpen} className="mb-1">
|
|
378
|
+
<CollapsibleTrigger className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-muted-foreground hover:bg-accent/50 transition-colors">
|
|
379
|
+
<ChevronDown
|
|
380
|
+
className={cn(
|
|
381
|
+
"h-4 w-4 shrink-0 transition-transform duration-200",
|
|
382
|
+
agentsOpen ? "" : "-rotate-90"
|
|
383
|
+
)}
|
|
384
|
+
/>
|
|
385
|
+
<Bot className="h-4 w-4 shrink-0" />
|
|
386
|
+
<span>Agents</span>
|
|
387
|
+
</CollapsibleTrigger>
|
|
388
|
+
<CollapsibleContent className="mt-1 space-y-0.5">
|
|
389
|
+
{agents.map((item) => (
|
|
390
|
+
<div
|
|
391
|
+
key={item.id}
|
|
392
|
+
data-conversation-item="true"
|
|
393
|
+
onContextMenu={(e) => onItemContextMenu(e, item)}
|
|
394
|
+
>
|
|
395
|
+
<ConversationItemCard
|
|
396
|
+
item={item}
|
|
397
|
+
isSelected={selectedId === item.id && selectedType === "agent"}
|
|
398
|
+
onClick={() => onSelect(item.id, "agent", item.name)}
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
))}
|
|
402
|
+
</CollapsibleContent>
|
|
403
|
+
</Collapsible>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{filtered.length === 0 && (
|
|
407
|
+
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
|
408
|
+
<MessageCircle className="mb-2 h-8 w-8" />
|
|
409
|
+
<p className="text-sm">No conversations</p>
|
|
410
|
+
</div>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
</ScrollArea>
|
|
414
|
+
)}
|
|
415
|
+
|
|
416
|
+
{menu.open && (
|
|
417
|
+
<div
|
|
418
|
+
className="fixed z-50 min-w-40 rounded-lg border bg-white/90 p-1 shadow-lg backdrop-blur"
|
|
419
|
+
style={{ left: menu.x, top: menu.y }}
|
|
420
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
421
|
+
>
|
|
422
|
+
{menu.open && menu.item ? (
|
|
423
|
+
<button
|
|
424
|
+
className="w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-accent"
|
|
425
|
+
onClick={() => {
|
|
426
|
+
if (menu.item) hideConversation(menu.item);
|
|
427
|
+
setMenu({ open: false });
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
Hide conversation
|
|
431
|
+
</button>
|
|
432
|
+
) : (
|
|
433
|
+
<button
|
|
434
|
+
className="w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-accent disabled:opacity-50"
|
|
435
|
+
onClick={() => {
|
|
436
|
+
restoreAllHidden();
|
|
437
|
+
setMenu({ open: false });
|
|
438
|
+
}}
|
|
439
|
+
disabled={hiddenKeys.size === 0}
|
|
440
|
+
>
|
|
441
|
+
Restore hidden conversations
|
|
442
|
+
</button>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function ConversationIconButton({
|
|
451
|
+
item,
|
|
452
|
+
isSelected,
|
|
453
|
+
onClick,
|
|
454
|
+
}: {
|
|
455
|
+
item: ConversationItem;
|
|
456
|
+
isSelected: boolean;
|
|
457
|
+
onClick: () => void;
|
|
458
|
+
}) {
|
|
459
|
+
const emoji = item.type === "group" ? "👥" : getAgentEmoji(item.name);
|
|
460
|
+
return (
|
|
461
|
+
<button
|
|
462
|
+
className={cn(
|
|
463
|
+
"relative flex h-11 w-11 items-center justify-center rounded-2xl transition",
|
|
464
|
+
"backdrop-blur-sm",
|
|
465
|
+
isSelected
|
|
466
|
+
? "bg-white/60 text-accent-foreground shadow-sm ring-1 ring-white/45"
|
|
467
|
+
: "hover:bg-white/40"
|
|
468
|
+
)}
|
|
469
|
+
onClick={onClick}
|
|
470
|
+
title={item.displayName}
|
|
471
|
+
>
|
|
472
|
+
<span className="text-xl">{emoji}</span>
|
|
473
|
+
{item.pendingCount > 0 && (
|
|
474
|
+
<span className="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full bg-destructive ring-2 ring-sidebar" />
|
|
475
|
+
)}
|
|
476
|
+
</button>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function ConversationItemCard({
|
|
481
|
+
item,
|
|
482
|
+
isSelected,
|
|
483
|
+
onClick,
|
|
484
|
+
}: {
|
|
485
|
+
item: ConversationItem;
|
|
486
|
+
isSelected: boolean;
|
|
487
|
+
onClick: () => void;
|
|
488
|
+
}) {
|
|
489
|
+
const emoji = item.type === "group" ? "👥" : getAgentEmoji(item.name);
|
|
490
|
+
|
|
491
|
+
return (
|
|
492
|
+
<button
|
|
493
|
+
className={cn(
|
|
494
|
+
"flex w-full items-center gap-2.5 rounded-2xl px-2.5 py-1.5 text-left transition overflow-hidden",
|
|
495
|
+
"backdrop-blur-sm",
|
|
496
|
+
isSelected
|
|
497
|
+
? "bg-white/62 text-accent-foreground shadow-sm ring-1 ring-white/45"
|
|
498
|
+
: "hover:bg-white/40"
|
|
499
|
+
)}
|
|
500
|
+
onClick={onClick}
|
|
501
|
+
>
|
|
502
|
+
<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]">
|
|
503
|
+
{emoji}
|
|
504
|
+
{item.pendingCount > 0 && (
|
|
505
|
+
<span className="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full bg-destructive ring-2 ring-background" />
|
|
506
|
+
)}
|
|
507
|
+
</span>
|
|
508
|
+
<div className="flex-1 min-w-0">
|
|
509
|
+
<div className="flex items-center justify-between gap-2">
|
|
510
|
+
<span className="text-sm font-medium leading-5">{truncateText(item.displayName, 18)}</span>
|
|
511
|
+
{item.lastTime && (
|
|
512
|
+
<span className="text-[11px] text-muted-foreground shrink-0">
|
|
513
|
+
{formatTime(item.lastTime)}
|
|
514
|
+
</span>
|
|
515
|
+
)}
|
|
516
|
+
</div>
|
|
517
|
+
{item.lastMessage && (
|
|
518
|
+
<p className="text-[11px] text-muted-foreground whitespace-nowrap leading-4">
|
|
519
|
+
{truncateText(item.lastMessage.replace(/\n/g, ' '), 20)}
|
|
520
|
+
</p>
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
523
|
+
</button>
|
|
524
|
+
);
|
|
525
|
+
}
|