Skip to content

Commit 3b2a864

Browse files
committed
feat: new database, add gemini support
1. (BREAKING CHANGES) chat database is totally refactored. Old db is no longer usable but new db is migratable. 2. Refactor chat attachments, remove ImageAttachment, combine into FileAttachment 2. Add support for gemini 3. Optimize model provider icon 4. Optimize `Customizable` Bindable value display
1 parent 073c3ee commit 3b2a864

23 files changed

+1660
-346
lines changed

3rd/shad-ui

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
2121
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="$(EntityFrameworkVersion)" />
2222
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(EntityFrameworkVersion)" />
23+
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7" />
2324
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftPackageVersion)" />
2425
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftPackageVersion)" />
2526
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftPackageVersion)" />

src/Everywhere.Windows/Program.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
using System.Text.Json;
2-
using Avalonia;
1+
using Avalonia;
32
using Avalonia.Controls;
43
using Avalonia.Input.Platform;
54
using Avalonia.Platform.Storage;
65
using Everywhere.Chat;
76
using Everywhere.Database;
8-
using Everywhere.Enums;
97
using Everywhere.Extensions;
108
using Everywhere.Initialization;
119
using Everywhere.Interfaces;
@@ -14,11 +12,9 @@
1412
using Everywhere.Views;
1513
using Everywhere.Views.Pages;
1614
using Everywhere.Windows.Services;
17-
using Microsoft.Extensions.Configuration;
1815
using Microsoft.Extensions.DependencyInjection;
1916
using Serilog;
2017
using ShadUI;
21-
using WritableJsonConfiguration;
2218

2319
namespace Everywhere.Windows;
2420

@@ -79,15 +75,14 @@ public static void Main(string[] args)
7975

8076
#region Database
8177

82-
.AddDbContext<IChatDatabase, ChatDbContext>(ServiceLifetime.Singleton)
78+
.AddChatDbContextAndStorage()
8379

8480
#endregion
8581

8682
#region Initialize
8783

88-
.AddSingleton<IAsyncInitializer, HotkeyInitializer>()
89-
.AddSingleton<IAsyncInitializer>(xx => xx.GetRequiredService<ChatContextManager>())
90-
.AddSingleton<IAsyncInitializer>(xx => xx.GetRequiredService<ChatDbContext>())
84+
.AddTransient<IAsyncInitializer, HotkeyInitializer>()
85+
.AddTransient<IAsyncInitializer>(xx => xx.GetRequiredService<ChatContextManager>())
9186

9287
#endregion
9388

src/Everywhere/AttachedProperties/DataTemplatesAttach.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ namespace Everywhere.AttachedProperties;
1010
/// </summary>
1111
public class DataTemplatesAttach : AvaloniaObject
1212
{
13-
static DataTemplatesAttach()
14-
{
15-
DataTemplatesProperty.Changed.AddClassHandler<Control>(HandleDataTemplatesChanges);
16-
}
17-
1813
public static readonly AttachedProperty<DataTemplates> DataTemplatesProperty =
1914
AvaloniaProperty.RegisterAttached<DataTemplatesAttach, Control, DataTemplates>("DataTemplates");
2015

2116
public static void SetDataTemplates(Control obj, DataTemplates value) => obj.SetValue(DataTemplatesProperty, value);
2217

2318
public static DataTemplates GetDataTemplates(Control obj) => obj.GetValue(DataTemplatesProperty);
2419

20+
static DataTemplatesAttach()
21+
{
22+
DataTemplatesProperty.Changed.AddClassHandler<Control>(HandleDataTemplatesChanges);
23+
}
24+
2525
private static void HandleDataTemplatesChanges(Control sender, AvaloniaPropertyChangedEventArgs args)
2626
{
2727
sender.DataTemplates.Clear();
Lines changed: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
using System.Diagnostics;
2-
using System.Diagnostics.CodeAnalysis;
1+
using System.Diagnostics.CodeAnalysis;
32
using CommunityToolkit.Mvvm.ComponentModel;
43
using CommunityToolkit.Mvvm.Input;
5-
using Everywhere.Database;
64
using Everywhere.Enums;
75
using Everywhere.Models;
6+
using Everywhere.Utilities;
87
using Microsoft.SemanticKernel.ChatCompletion;
98
using ObservableCollections;
109
using ZLinq;
1110

1211
namespace 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
}

src/Everywhere/Chat/ChatService.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,13 @@ private async Task GenerateAsync(
140140

141141
var kernel = BuildKernel(kernelMixin);
142142
var chatHistory = new ChatHistory(
143-
chatContext
143+
await chatContext
144144
.Select(n => n.Message)
145145
.Where(m => !Equals(m, assistantChatMessage)) // exclude the current assistant message
146146
.Where(m => m.Role.Label is "system" or "assistant" or "user" or "tool")
147-
.Select(CreateChatMessageContent));
147+
.ToAsyncEnumerable()
148+
.SelectAwait(CreateChatMessageContentAsync)
149+
.ToArrayAsync(cancellationToken: cancellationToken));
148150

149151
while (true)
150152
{
@@ -241,7 +243,7 @@ private async Task GenerateAsync(
241243
assistantChatMessage.IsBusy = false;
242244
}
243245

244-
ChatMessageContent CreateChatMessageContent(ChatMessage chatMessage)
246+
async ValueTask<ChatMessageContent> CreateChatMessageContentAsync(ChatMessage chatMessage)
245247
{
246248
ChatMessageContent? content;
247249
switch (chatMessage)
@@ -250,11 +252,13 @@ ChatMessageContent CreateChatMessageContent(ChatMessage chatMessage)
250252
{
251253
content = new ChatMessageContent(chatMessage.Role, user.UserPrompt);
252254

253-
if (user.Attachments.OfType<ChatImageAttachment>().ToArray() is not { Length: > 0 } imageAttachments) break;
254-
foreach (var imageAttachment in imageAttachments)
255+
foreach (var imageAttachment in user.Attachments.OfType<ChatFileAttachment>().Where(a => a.IsImage))
255256
{
257+
using var image = await imageAttachment.GetImageAsync();
258+
if (image is null) continue;
259+
256260
using var ms = new MemoryStream();
257-
imageAttachment.Image.Save(ms, 100);
261+
image.Save(ms, 100);
258262
ms.Position = 0;
259263
content.Items.Add(new ImageContent(ms.ToArray(), "image/png"));
260264
}

0 commit comments

Comments
 (0)