1- using System . Diagnostics ;
2- using System . Diagnostics . CodeAnalysis ;
1+ using System . Diagnostics . CodeAnalysis ;
32using CommunityToolkit . Mvvm . ComponentModel ;
43using CommunityToolkit . Mvvm . Input ;
5- using Everywhere . Database ;
64using Everywhere . Enums ;
75using Everywhere . Models ;
6+ using Everywhere . Utilities ;
87using Microsoft . SemanticKernel . ChatCompletion ;
98using ObservableCollections ;
109using ZLinq ;
1110
1211namespace Everywhere . Chat ;
1312
14- public partial class ChatContextManager ( Settings settings , IChatDatabase chatDatabase ) : ObservableObject , IChatContextManager , IAsyncInitializer
13+ public partial class ChatContextManager : ObservableObject , IChatContextManager , IAsyncInitializer
1514{
1615 public NotifyCollectionChangedSynchronizedViewList < ChatMessageNode > ChatMessageNodes =>
1716 Current . ToNotifyCollectionChanged (
@@ -22,16 +21,22 @@ public ChatContext Current
2221 {
2322 get
2423 {
25- if ( current is not null ) return current ;
26- CreateNew ( ) ;
27- return current ;
24+ if ( _current is null ) CreateNew ( ) ;
25+ _current . Changed += HandleChatContextChanged ;
26+ return _current ;
2827 }
2928 set
3029 {
31- Debug . Assert ( history . ContainsKey ( value ) , "The value must be part of the history." ) ;
30+ if ( value . Metadata . Id == Guid . Empty )
31+ throw new ArgumentException ( "The provided chat context does not have a valid ID." , nameof ( value ) ) ;
3232
33- var previous = current ;
34- if ( ! SetProperty ( ref current , value ) ) return ;
33+ if ( ! _history . ContainsKey ( value . Metadata . Id ) )
34+ throw new ArgumentException ( "The provided chat context is not part of the history." , nameof ( value ) ) ;
35+
36+ var previous = _current ;
37+ if ( ! SetProperty ( ref _current , value ) ) return ;
38+
39+ if ( previous is not null ) previous . Changed -= HandleChatContextChanged ;
3540 if ( IsEmptyContext ( previous ) ) Remove ( previous ) ;
3641
3742 OnPropertyChanged ( ) ;
@@ -44,7 +49,7 @@ public IEnumerable<ChatContextHistory> History
4449 get
4550 {
4651 var currentDate = DateTimeOffset . UtcNow ;
47- return history . Keys . GroupBy ( c => ( currentDate - c . Metadata . DateModified ) . TotalDays switch
52+ return _history . Values . GroupBy ( c => ( currentDate - c . Metadata . DateModified ) . TotalDays switch
4853 {
4954 < 1 => HumanizedDate . Today ,
5055 < 2 => HumanizedDate . Yesterday ,
@@ -61,30 +66,59 @@ public IEnumerable<ChatContextHistory> History
6166
6267 [ field: AllowNull , MaybeNull ]
6368 public IRelayCommand CreateNewCommand =>
64- field ??= new RelayCommand ( CreateNew , ( ) => ! IsEmptyContext ( current ) ) ;
69+ field ??= new RelayCommand ( CreateNew , ( ) => ! IsEmptyContext ( _current ) ) ;
70+
71+ private ChatContext ? _current ;
6572
66- private readonly Dictionary < ChatContext , ChatContextDbItem > history = [ ] ;
73+ private readonly Dictionary < Guid , ChatContext > _history = [ ] ;
74+ private readonly HashSet < ChatContext > _saveBuffer = [ ] ;
75+ private readonly Settings _settings ;
76+ private readonly IChatContextStorage _chatContextStorage ;
77+ private readonly DebounceExecutor < ChatContextManager > _saveDebounceExecutor ;
78+
79+ public ChatContextManager ( Settings settings , IChatContextStorage chatContextStorage )
80+ {
81+ _settings = settings ;
82+ _chatContextStorage = chatContextStorage ;
83+ _saveDebounceExecutor = new DebounceExecutor < ChatContextManager > (
84+ ( ) => this ,
85+ static that =>
86+ {
87+ ChatContext [ ] toSave ;
88+ lock ( that . _saveBuffer )
89+ {
90+ toSave = that . _saveBuffer . ToArray ( ) ;
91+ that . _saveBuffer . Clear ( ) ;
92+ }
93+ Task . WhenAll ( toSave . AsValueEnumerable ( ) . Select ( c => that . _chatContextStorage . SaveChatContextAsync ( c ) ) . ToArray ( ) ) . Detach ( ) ;
94+ } ,
95+ TimeSpan . FromSeconds ( 0.5 )
96+ ) ;
97+ }
6798
68- private ChatContext ? current ;
99+ private void HandleChatContextChanged ( ChatContext sender )
100+ {
101+ lock ( _saveBuffer ) _saveBuffer . Add ( sender ) ;
102+ _saveDebounceExecutor . Trigger ( ) ;
103+ }
69104
70- [ MemberNotNull ( nameof ( current ) ) ]
105+ [ MemberNotNull ( nameof ( _current ) ) ]
71106 private void CreateNew ( )
72107 {
73- if ( IsEmptyContext ( current ) ) return ;
108+ if ( IsEmptyContext ( _current ) ) return ;
74109
75110 var renderedSystemPrompt = Prompts . RenderPrompt (
76111 Prompts . DefaultSystemPrompt ,
77112 new Dictionary < string , Func < string > >
78113 {
79114 { "OS" , ( ) => Environment . OSVersion . ToString ( ) } ,
80115 { "Time" , ( ) => DateTime . Now . ToString ( "F" ) } ,
81- { "SystemLanguage" , ( ) => settings . Common . Language } ,
116+ { "SystemLanguage" , ( ) => _settings . Common . Language } ,
82117 } ) ;
83118
84- current = new ChatContext ( renderedSystemPrompt ) ;
85- var dbItem = new ChatContextDbItem ( current ) ;
86- history . Add ( current , dbItem ) ;
87- Task . Run ( ( ) => chatDatabase . AddChatContext ( dbItem ) ) . Detach ( ) ;
119+ _current = new ChatContext ( renderedSystemPrompt ) ;
120+ _history . Add ( _current . Metadata . Id , _current ) ;
121+ Task . Run ( ( ) => _chatContextStorage . AddChatContextAsync ( _current ) ) . Detach ( ) ;
88122
89123 OnPropertyChanged ( nameof ( History ) ) ;
90124 OnPropertyChanged ( nameof ( Current ) ) ;
@@ -94,14 +128,14 @@ private void CreateNew()
94128 [ RelayCommand ]
95129 private void Remove ( ChatContext chatContext )
96130 {
97- if ( ! history . Remove ( chatContext , out var dbItem ) ) return ;
131+ if ( ! _history . Remove ( chatContext . Metadata . Id ) ) return ;
98132
99- Task . Run ( ( ) => chatDatabase . RemoveChatContext ( dbItem ) ) . Detach ( ) ;
133+ Task . Run ( ( ) => _chatContextStorage . DeleteChatContextAsync ( chatContext . Metadata . Id ) ) . Detach ( ) ;
100134
101135 // If the current chat context is being removed, we need to set a new current context
102- if ( ReferenceEquals ( chatContext , current ) )
136+ if ( ReferenceEquals ( chatContext , _current ) )
103137 {
104- if ( history . Keys . OrderByDescending ( c => c . Metadata . DateModified ) . FirstOrDefault ( ) is { } historyItem )
138+ if ( _history . Values . OrderByDescending ( c => c . Metadata . DateModified ) . FirstOrDefault ( ) is { } historyItem )
105139 {
106140 Current = historyItem ;
107141 }
@@ -118,25 +152,35 @@ private void Remove(ChatContext chatContext)
118152 [ RelayCommand ]
119153 private void Rename ( ChatContext chatContext )
120154 {
121- foreach ( var other in history . Keys ) other . IsRenamingMetadataTitle = false ;
155+ foreach ( var other in _history . Values ) other . IsRenamingMetadataTitle = false ;
122156 chatContext . IsRenamingMetadataTitle = true ;
123157 }
124158
125159 public void UpdateHistory ( )
126160 {
127- OnPropertyChanged ( nameof ( History ) ) ;
161+ UpdateHistoryAsync ( int . MaxValue ) . Detach ( ) ;
128162 }
129163
130- public int Priority => 10 ;
131-
132- public Task InitializeAsync ( ) => Task . Run ( ( ) =>
164+ private Task UpdateHistoryAsync ( int count ) => Task . Run ( async ( ) =>
133165 {
134- foreach ( var chatContext in chatDatabase . QueryChatContexts ( q => q . OrderByDescending ( c => c . DateModified ) ) )
166+ await foreach ( var metadata in _chatContextStorage . QueryChatContextsAsync ( count , ChatContextOrderBy . UpdatedAt , true ) )
135167 {
136- current ??= chatContext . Value ;
137- history . Add ( chatContext . Value , chatContext ) ;
168+ if ( _history . ContainsKey ( metadata . Id ) )
169+ {
170+ continue ;
171+ }
172+
173+ var chatContext = await _chatContextStorage . GetChatContextAsync ( metadata . Id ) ;
174+ _current ??= chatContext ;
175+ _history . Add ( metadata . Id , chatContext ) ;
138176 }
177+
178+ OnPropertyChanged ( nameof ( History ) ) ;
139179 } ) ;
140180
181+ public int Priority => 10 ;
182+
183+ public Task InitializeAsync ( ) => UpdateHistoryAsync ( 8 ) ;
184+
141185 private static bool IsEmptyContext ( [ NotNullWhen ( true ) ] ChatContext ? chatContext ) => chatContext is { MessageCount : 1 } ;
142186}
0 commit comments