@@ -56,6 +56,7 @@ type ThreadMeta = ThreadListItem & {
5656 hasCustomName : boolean ;
5757 readCount : number ;
5858 unreadCount : number ;
59+ isAI : boolean ;
5960} ;
6061
6162const 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