Skip to content

Commit 24cc2f0

Browse files
committed
chat: divide thread list into human and ai, with total counter
1 parent 2432677 commit 24cc2f0

File tree

1 file changed

+158
-64
lines changed

1 file changed

+158
-64
lines changed

src/packages/frontend/chat/chatroom.tsx

Lines changed: 158 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type ThreadMeta = ThreadListItem & {
5656
hasCustomName: boolean;
5757
readCount: number;
5858
unreadCount: number;
59+
isAI: boolean;
5960
};
6061

6162
const FILTER_RECENT_NONE = {
@@ -146,6 +147,7 @@ export function ChatRoom({
146147
const [filterRecentHCustom, setFilterRecentHCustom] = useState<string>("");
147148
const [filterRecentOpen, setFilterRecentOpen] = useState<boolean>(false);
148149
const rawThreads = useThreadList(messages);
150+
const llmCacheRef = useRef<Map<string, boolean>>(new Map());
149151
const threads = useMemo<ThreadMeta[]>(() => {
150152
return rawThreads.map((thread) => {
151153
const rootMessage = thread.rootMessage;
@@ -167,15 +169,28 @@ export function ChatRoom({
167169
const readCount =
168170
Number.isFinite(readValue) && readValue > 0 ? readValue : 0;
169171
const unreadCount = Math.max(thread.messageCount - readCount, 0);
172+
let isAI = llmCacheRef.current.get(thread.key);
173+
if (isAI == null) {
174+
if (actions?.isLanguageModelThread) {
175+
const result = actions.isLanguageModelThread(
176+
new Date(parseInt(thread.key, 10)),
177+
);
178+
isAI = result !== false;
179+
} else {
180+
isAI = false;
181+
}
182+
llmCacheRef.current.set(thread.key, isAI);
183+
}
170184
return {
171185
...thread,
172186
displayLabel,
173187
hasCustomName,
174188
readCount,
175189
unreadCount,
190+
isAI: !!isAI,
176191
};
177192
});
178-
}, [rawThreads, account_id]);
193+
}, [rawThreads, account_id, actions]);
179194
const [selectedThreadKey, setSelectedThreadKey0] = useState<string | null>(
180195
desc?.get("data-selectedThreadKey") ?? null,
181196
);
@@ -567,61 +582,135 @@ export function ChatRoom({
567582
sendMessage();
568583
}
569584

585+
function renderThreadRow(thread: ThreadMeta) {
586+
const { key, displayLabel, hasCustomName, unreadCount, isAI } = thread;
587+
const isHovered = hoveredThread === key;
588+
const showMenu = isHovered || selectedThreadKey === key;
589+
return {
590+
key,
591+
label: (
592+
<div
593+
style={{
594+
display: "flex",
595+
alignItems: "center",
596+
gap: "8px",
597+
width: "100%",
598+
}}
599+
onMouseEnter={() => setHoveredThread(key)}
600+
onMouseLeave={() =>
601+
setHoveredThread((prev) => (prev === key ? null : prev))
602+
}
603+
>
604+
<StaticMarkdown
605+
value={displayLabel}
606+
style={THREAD_ITEM_LABEL_STYLE}
607+
/>
608+
{unreadCount > 0 && (
609+
<Badge
610+
count={unreadCount}
611+
size="small"
612+
overflowCount={99}
613+
style={{
614+
backgroundColor: COLORS.GRAY_L0,
615+
color: COLORS.GRAY_D,
616+
}}
617+
/>
618+
)}
619+
{showMenu && (
620+
<Dropdown
621+
menu={threadMenuProps(key, displayLabel, hasCustomName)}
622+
trigger={["click"]}
623+
>
624+
<Button
625+
type="text"
626+
size="small"
627+
onClick={(event) => event.stopPropagation()}
628+
icon={<Icon name="ellipsis" />}
629+
/>
630+
</Dropdown>
631+
)}
632+
</div>
633+
),
634+
};
635+
}
636+
637+
function renderThreadSection({
638+
title,
639+
icon,
640+
threads: list,
641+
maxHeight,
642+
}: {
643+
title: string;
644+
icon: React.ComponentProps<typeof Icon>["name"];
645+
threads: ThreadMeta[];
646+
maxHeight?: string;
647+
}) {
648+
const unreadTotal = list.reduce(
649+
(sum, thread) => sum + thread.unreadCount,
650+
0,
651+
);
652+
const items = list.map(renderThreadRow);
653+
return (
654+
<div style={{ marginBottom: "15px" }}>
655+
<div
656+
style={{
657+
display: "flex",
658+
alignItems: "center",
659+
justifyContent: "space-between",
660+
marginBottom: "6px",
661+
}}
662+
>
663+
<div
664+
style={{
665+
display: "flex",
666+
alignItems: "center",
667+
gap: "6px",
668+
paddingLeft: "10px",
669+
}}
670+
>
671+
<Icon name={icon} />
672+
<span style={{ fontWeight: 600 }}>{title}</span>
673+
</div>
674+
{unreadTotal > 0 && (
675+
<Badge
676+
count={unreadTotal}
677+
size="small"
678+
style={{
679+
backgroundColor: COLORS.GRAY_L0,
680+
color: COLORS.GRAY_D,
681+
}}
682+
/>
683+
)}
684+
</div>
685+
{list.length === 0 ? (
686+
<div style={{ color: "#999", fontSize: "12px", marginLeft: "4px" }}>
687+
No chats
688+
</div>
689+
) : (
690+
<Menu
691+
mode="inline"
692+
selectedKeys={selectedThreadKey ? [selectedThreadKey] : []}
693+
onClick={({ key }) => {
694+
setAllowAutoSelectThread(true);
695+
setSelectedThreadKey(String(key));
696+
}}
697+
items={items}
698+
style={{
699+
border: "none",
700+
background: "transparent",
701+
padding: 0,
702+
maxHeight: maxHeight ?? "28vh",
703+
overflowY: "auto",
704+
}}
705+
/>
706+
)}
707+
</div>
708+
);
709+
}
710+
570711
function renderThreadSidebar(): React.JSX.Element {
571-
const menuItems =
572-
threads.length === 0
573-
? []
574-
: threads.map((thread) => {
575-
const { key, displayLabel, hasCustomName, unreadCount } = thread;
576-
const isHovered = hoveredThread === key;
577-
const showMenu = isHovered || selectedThreadKey === key;
578-
return {
579-
key,
580-
label: (
581-
<div
582-
style={{
583-
display: "flex",
584-
alignItems: "center",
585-
gap: "8px",
586-
width: "100%",
587-
}}
588-
onMouseEnter={() => setHoveredThread(key)}
589-
onMouseLeave={() =>
590-
setHoveredThread((prev) => (prev === key ? null : prev))
591-
}
592-
>
593-
<StaticMarkdown
594-
value={displayLabel}
595-
style={THREAD_ITEM_LABEL_STYLE}
596-
/>
597-
{unreadCount > 0 && !isHovered && (
598-
<Badge
599-
count={unreadCount}
600-
size="small"
601-
overflowCount={99}
602-
style={{
603-
backgroundColor: COLORS.GRAY_L0,
604-
color: COLORS.GRAY_D,
605-
}}
606-
/>
607-
)}
608-
{showMenu && (
609-
<Dropdown
610-
menu={threadMenuProps(key, displayLabel, hasCustomName)}
611-
trigger={["click"]}
612-
>
613-
<Button
614-
type="text"
615-
size="small"
616-
onClick={(event) => event.stopPropagation()}
617-
icon={<Icon name="ellipsis" />}
618-
/>
619-
</Dropdown>
620-
)}
621-
</div>
622-
),
623-
};
624-
});
712+
const humanThreads = threads.filter((thread) => !thread.isAI);
713+
const aiThreads = threads.filter((thread) => thread.isAI);
625714

626715
return (
627716
<Layout.Sider width={THREAD_SIDEBAR_WIDTH} style={THREAD_SIDEBAR_STYLE}>
@@ -678,15 +767,20 @@ export function ChatRoom({
678767
No messages yet.
679768
</div>
680769
) : (
681-
<Menu
682-
mode="inline"
683-
selectedKeys={selectedThreadKey ? [selectedThreadKey] : []}
684-
onClick={({ key }) => {
685-
setAllowAutoSelectThread(true);
686-
setSelectedThreadKey(String(key));
687-
}}
688-
items={menuItems}
689-
/>
770+
<>
771+
{renderThreadSection({
772+
title: "Humans",
773+
icon: "users",
774+
threads: humanThreads,
775+
maxHeight: "30vh",
776+
})}
777+
{renderThreadSection({
778+
title: "AI",
779+
icon: "robot",
780+
threads: aiThreads,
781+
maxHeight: "30vh",
782+
})}
783+
</>
690784
)}
691785
</Layout.Sider>
692786
);

0 commit comments

Comments
 (0)