@@ -168,13 +168,18 @@ export class ChatActions extends Actions<ChatState> {
168168 tag,
169169 noNotification,
170170 submitMentionsRef,
171+ extraInput,
172+ name,
171173 } : {
172174 input ?: string ;
173175 sender_id ?: string ;
174176 reply_to ?: Date ;
175177 tag ?: string ;
176178 noNotification ?: boolean ;
177179 submitMentionsRef ?;
180+ extraInput ?: string ;
181+ // if name is given, rename thread to have that name
182+ name ?: string ;
178183 } ) : string => {
179184 if ( this . syncdb == null || this . store == null ) {
180185 console . warn ( "attempt to sendChat before chat actions initialized" ) ;
@@ -186,11 +191,15 @@ export class ChatActions extends Actions<ChatState> {
186191 if ( submitMentionsRef ?. current != null ) {
187192 input = submitMentionsRef . current ?.( { chat : `${ time_stamp . valueOf ( ) } ` } ) ;
188193 }
194+ if ( extraInput ) {
195+ input = ( input ?? "" ) + extraInput ;
196+ }
189197 input = input ?. trim ( ) ;
190198 if ( ! input ) {
191199 // do not send when there is nothing to send.
192200 return "" ;
193201 }
202+ const trimmedName = name ?. trim ( ) ;
194203 const message : ChatMessage = {
195204 sender_id,
196205 event : "chat" ,
@@ -205,7 +214,12 @@ export class ChatActions extends Actions<ChatState> {
205214 reply_to : reply_to ?. toISOString ( ) ,
206215 editing : { } ,
207216 } ;
217+ if ( trimmedName && ! reply_to ) {
218+ ( message as any ) . name = trimmedName ;
219+ }
208220 this . syncdb . set ( message ) ;
221+ const messagesState = this . store . get ( "messages" ) ;
222+ let selectedThreadKey : string ;
209223 if ( ! reply_to ) {
210224 this . deleteDraft ( 0 ) ;
211225 // NOTE: we also clear search, since it's confusing to send a message and not
@@ -214,17 +228,29 @@ export class ChatActions extends Actions<ChatState> {
214228 // Also, only do this clearing when not replying.
215229 // For replies search find full threads not individual messages.
216230 this . clearAllFilters ( ) ;
231+ selectedThreadKey = `${ time_stamp . valueOf ( ) } ` ;
217232 } else {
218233 // when replying we make sure that the thread is expanded, since otherwise
219234 // our reply won't be visible
220- const messages = this . store . get ( "messages" ) ;
221235 if (
222- messages
236+ messagesState
223237 ?. getIn ( [ `${ reply_to . valueOf ( ) } ` , "folding" ] )
224238 ?. includes ( sender_id )
225239 ) {
226240 this . toggleFoldThread ( reply_to ) ;
227241 }
242+ const root =
243+ getThreadRootDate ( {
244+ date : reply_to . valueOf ( ) ,
245+ messages : messagesState ,
246+ } ) ?? reply_to . valueOf ( ) ;
247+ selectedThreadKey = `${ root } ` ;
248+ }
249+ if ( selectedThreadKey != "0" ) {
250+ this . setSelectedThread ( selectedThreadKey ) ;
251+ }
252+ if ( trimmedName && reply_to ) {
253+ this . renameThread ( selectedThreadKey , trimmedName ) ;
228254 }
229255
230256 const project_id = this . store ?. get ( "project_id" ) ;
@@ -481,6 +507,151 @@ export class ChatActions extends Actions<ChatState> {
481507 } ) ;
482508 } ;
483509
510+ // returns number of deleted messages
511+ // threadKey = iso timestamp root of thread.
512+ deleteThread = ( threadKey : string ) : number => {
513+ if ( this . syncdb == null || this . store == null ) {
514+ return 0 ;
515+ }
516+ const messages = this . store . get ( "messages" ) ;
517+ if ( messages == null ) {
518+ return 0 ;
519+ }
520+ const rootTarget = parseInt ( `${ threadKey } ` ) ;
521+ if ( ! isFinite ( rootTarget ) ) {
522+ return 0 ;
523+ }
524+ let deleted = 0 ;
525+ for ( const [ _ , message ] of messages ) {
526+ if ( message == null ) continue ;
527+ const dateField = message . get ( "date" ) ;
528+ let dateValue : number | undefined ;
529+ let dateIso : string | undefined ;
530+ if ( dateField instanceof Date ) {
531+ dateValue = dateField . valueOf ( ) ;
532+ dateIso = dateField . toISOString ( ) ;
533+ } else if ( typeof dateField === "number" ) {
534+ dateValue = dateField ;
535+ dateIso = new Date ( dateField ) . toISOString ( ) ;
536+ } else if ( typeof dateField === "string" ) {
537+ const t = Date . parse ( dateField ) ;
538+ dateValue = isNaN ( t ) ? undefined : t ;
539+ dateIso = dateField ;
540+ }
541+ if ( dateValue == null || dateIso == null ) {
542+ continue ;
543+ }
544+ const rootDate =
545+ getThreadRootDate ( { date : dateValue , messages } ) || dateValue ;
546+ if ( rootDate !== rootTarget ) {
547+ continue ;
548+ }
549+ this . syncdb . delete ( { event : "chat" , date : dateIso } ) ;
550+ deleted ++ ;
551+ }
552+ if ( deleted > 0 ) {
553+ this . syncdb . commit ( ) ;
554+ }
555+ return deleted ;
556+ } ;
557+
558+ renameThread = ( threadKey : string , name : string ) : boolean => {
559+ if ( this . syncdb == null ) {
560+ return false ;
561+ }
562+ const entry = this . getThreadRootDoc ( threadKey ) ;
563+ if ( entry == null ) {
564+ return false ;
565+ }
566+ const trimmed = name . trim ( ) ;
567+ if ( trimmed ) {
568+ entry . doc . name = trimmed ;
569+ } else {
570+ delete entry . doc . name ;
571+ }
572+ this . syncdb . set ( entry . doc ) ;
573+ this . syncdb . commit ( ) ;
574+ return true ;
575+ } ;
576+
577+ setThreadPin = ( threadKey : string , pinned : boolean ) : boolean => {
578+ if ( this . syncdb == null ) {
579+ return false ;
580+ }
581+ const entry = this . getThreadRootDoc ( threadKey ) ;
582+ if ( entry == null ) {
583+ return false ;
584+ }
585+ if ( pinned ) {
586+ entry . doc . pin = true ;
587+ } else {
588+ entry . doc . pin = false ;
589+ }
590+ this . syncdb . set ( entry . doc ) ;
591+ this . syncdb . commit ( ) ;
592+ return true ;
593+ } ;
594+
595+ markThreadRead = (
596+ threadKey : string ,
597+ count : number ,
598+ commit = true ,
599+ ) : boolean => {
600+ if ( this . syncdb == null ) {
601+ return false ;
602+ }
603+ const account_id = this . redux . getStore ( "account" ) . get_account_id ( ) ;
604+ if ( ! account_id || ! Number . isFinite ( count ) ) {
605+ return false ;
606+ }
607+ const entry = this . getThreadRootDoc ( threadKey ) ;
608+ if ( entry == null ) {
609+ return false ;
610+ }
611+ entry . doc [ `read-${ account_id } ` ] = count ;
612+ this . syncdb . set ( entry . doc ) ;
613+ if ( commit ) {
614+ this . syncdb . commit ( ) ;
615+ }
616+ return true ;
617+ } ;
618+
619+ private getThreadRootDoc = (
620+ threadKey : string ,
621+ ) : { doc : any ; message : ChatMessageTyped } | null => {
622+ if ( this . store == null ) {
623+ return null ;
624+ }
625+ const messages = this . store . get ( "messages" ) ;
626+ if ( messages == null ) {
627+ return null ;
628+ }
629+ const normalizedKey = toMsString ( threadKey ) ;
630+ const fallbackKey = `${ parseInt ( threadKey , 10 ) } ` ;
631+ const candidates = [ normalizedKey , threadKey , fallbackKey ] ;
632+ let message : ChatMessageTyped | undefined ;
633+ for ( const key of candidates ) {
634+ if ( ! key ) continue ;
635+ message = messages . get ( key ) ;
636+ if ( message != null ) break ;
637+ }
638+ if ( message == null ) {
639+ return null ;
640+ }
641+ const dateField = message . get ( "date" ) ;
642+ const dateIso =
643+ dateField instanceof Date
644+ ? dateField . toISOString ( )
645+ : typeof dateField === "string"
646+ ? dateField
647+ : new Date ( dateField ) . toISOString ( ) ;
648+ if ( ! dateIso ) {
649+ return null ;
650+ }
651+ const doc = { ...message . toJS ( ) , date : dateIso } ;
652+ return { doc, message } ;
653+ } ;
654+
484655 save_scroll_state = ( position , height , offset ) : void => {
485656 if ( height == 0 ) {
486657 // height == 0 means chat room is not rendered
@@ -605,30 +776,44 @@ export class ChatActions extends Actions<ChatState> {
605776 * This checks a thread of messages to see if it is a language model thread and if so, returns it.
606777 */
607778 isLanguageModelThread = ( date ?: Date ) : false | LanguageModel => {
608- if ( date == null ) {
779+ if ( date == null || this . store == null ) {
609780 return false ;
610781 }
611- const thread = this . getMessagesInThread ( date . toISOString ( ) ) ;
612- if ( thread == null ) {
782+ const messages = this . store . get ( "messages" ) ;
783+ if ( messages == null ) {
784+ return false ;
785+ }
786+ const rootMs =
787+ getThreadRootDate ( { date : date . valueOf ( ) , messages } ) || date . valueOf ( ) ;
788+ const entry = this . getThreadRootDoc ( `${ rootMs } ` ) ;
789+ const rootMessage = entry ?. message ;
790+ if ( rootMessage == null ) {
613791 return false ;
614792 }
615793
616- // We deliberately start at the last most recent message.
617- // Why? If we use the LLM regenerate dropdown button to change the LLM, we want to keep it.
618- for ( const message of thread . reverse ( ) ) {
619- const lastHistory = message . get ( "history" ) ?. first ( ) ;
620- // this must be an invalid message, because there is no history
621- if ( lastHistory == null ) continue ;
622- const sender_id = lastHistory . get ( "author_id" ) ;
623- if ( isLanguageModelService ( sender_id ) ) {
624- return service2model ( sender_id ) ;
625- }
626- const input = lastHistory . get ( "content" ) ?. toLowerCase ( ) ;
627- if ( mentionsLanguageModel ( input ) ) {
628- return getLanguageModel ( input ) ;
629- }
794+ const thread = this . getMessagesInThread (
795+ rootMessage . get ( "date" ) ?. toISOString ?.( ) ?? `${ rootMs } ` ,
796+ ) ;
797+ if ( thread == null ) {
798+ return false ;
630799 }
631800
801+ const firstMessage = thread . first ( ) ;
802+ if ( firstMessage == null ) {
803+ return false ;
804+ }
805+ const firstHistory = firstMessage . get ( "history" ) ?. first ( ) ;
806+ if ( firstHistory == null ) {
807+ return false ;
808+ }
809+ const sender_id = firstHistory . get ( "author_id" ) ;
810+ if ( isLanguageModelService ( sender_id ) ) {
811+ return service2model ( sender_id ) ;
812+ }
813+ const input = firstHistory . get ( "content" ) ?. toLowerCase ( ) ;
814+ if ( mentionsLanguageModel ( input ) ) {
815+ return getLanguageModel ( input ) ;
816+ }
632817 return false ;
633818 } ;
634819
@@ -1104,13 +1289,15 @@ export class ChatActions extends Actions<ChatState> {
11041289 } ;
11051290
11061291 setFragment = ( date ?) => {
1292+ let fragmentId ;
11071293 if ( ! date ) {
11081294 Fragment . clear ( ) ;
1295+ fragmentId = "" ;
11091296 } else {
1110- const fragmentId = toMsString ( date ) ;
1297+ fragmentId = toMsString ( date ) ;
11111298 Fragment . set ( { chat : fragmentId } ) ;
1112- this . frameTreeActions ?. set_frame_data ( { id : this . frameId , fragmentId } ) ;
11131299 }
1300+ this . frameTreeActions ?. set_frame_data ( { id : this . frameId , fragmentId } ) ;
11141301 } ;
11151302
11161303 setShowPreview = ( showPreview ) => {
@@ -1119,6 +1306,13 @@ export class ChatActions extends Actions<ChatState> {
11191306 showPreview,
11201307 } ) ;
11211308 } ;
1309+
1310+ setSelectedThread = ( threadKey : string | null ) => {
1311+ this . frameTreeActions ?. set_frame_data ( {
1312+ id : this . frameId ,
1313+ selectedThreadKey : threadKey ,
1314+ } ) ;
1315+ } ;
11221316}
11231317
11241318// We strip out any cased version of the string @chatgpt and also all mentions.
0 commit comments