Skip to content

Commit 21b5098

Browse files
committed
Implemented Chat session/coordinator layer; Refactored ChatViewModel
Added: IChatSession, IChatCoordinator Updated: refactor ChatViewModel to use new session and coordinator services to offload work Fixed: Conversation/Message sync across Chat and Overlay via shared session
1 parent 4fcce47 commit 21b5098

File tree

13 files changed

+734
-386
lines changed

13 files changed

+734
-386
lines changed

LudereAI.WPF/App.xaml.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ private void ConfigureServices(IServiceCollection services)
138138
services.AddSingleton<IOverlayService, OverlayService>();
139139
services.AddSingleton<IAudioService, AudioService>();
140140

141+
// --- Session Services ---
142+
services.AddSingleton<IChatSession, ChatSession>();
143+
services.AddTransient<IChatCoordinator, ChatCoordinator>();
144+
141145
// --- Operational Services ---
142146
services.AddTransient<IGameService, GameService>();
143147
services.AddTransient<IChatService, ChatService>();
@@ -157,6 +161,7 @@ private void ConfigureServices(IServiceCollection services)
157161
// --- ViewModels ---
158162
services.AddTransient<SetupViewModel>();
159163
services.AddTransient<ChatViewModel>();
164+
services.AddTransient<OverlayViewModel>();
160165
services.AddTransient<SettingsViewModel>();
161166

162167
// --- Views ---
@@ -188,6 +193,7 @@ protected override async void OnExit(ExitEventArgs e)
188193

189194
private static void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
190195
{
196+
Clipboard.SetText(e.Exception.Message);
191197
Log.Error(e.Exception, "Unhandled application exception");
192198
var result = MessageBox.Show(
193199
$"An unhandled error occurred: {e.Exception.Message}\n\nDo you want to continue?",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using LudereAI.Shared.Models;
2+
using LudereAI.WPF.Models;
3+
4+
namespace LudereAI.WPF.Interfaces;
5+
6+
public interface IChatCoordinator
7+
{
8+
bool CanSend(string message, ConversationModel? conversation, string? gameContext);
9+
10+
Task Initialize();
11+
Task RefreshConversations();
12+
Task DeleteConversation(ConversationModel conversation);
13+
Task SendMessage(string message, string? gameContext, WindowInfo? windowContext);
14+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Collections.ObjectModel;
2+
using LudereAI.Shared.Enums;
3+
using LudereAI.Shared.Models;
4+
using LudereAI.WPF.Models;
5+
6+
namespace LudereAI.WPF.Interfaces;
7+
8+
public interface IChatSession
9+
{
10+
ObservableCollection<ConversationModel> Conversations { get; }
11+
ConversationModel? CurrentConversation { get; set; }
12+
ObservableCollection<MessageModel> Messages { get; }
13+
14+
bool IsAssistantThinking { get; set; }
15+
16+
bool IsOverrideEnabled { get; set; }
17+
string ManualGameName { get; set; }
18+
WindowInfo? ManualWindow { get; set; }
19+
WindowInfo? PredictedWindow { get; set; }
20+
21+
string? EffectiveGameContext { get; }
22+
WindowInfo? EffectiveWindow { get; }
23+
24+
event Action OnConversationChanged;
25+
event Action OnMessagesChanged;
26+
event Action OnAssistantThinkingChanged;
27+
event Action OnGameContextChanged;
28+
29+
30+
void SetConversations(ObservableCollection<ConversationModel> newConversations);
31+
void SetMessages(ObservableCollection<MessageModel> newMessages);
32+
void AddMessageLocal(MessageModel message);
33+
void AddMessageLocal(string content, MessageRole role);
34+
public void AddSystemMessage(string content);
35+
}
Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using CommunityToolkit.Mvvm.ComponentModel;
1+
using System.Collections.ObjectModel;
2+
using CommunityToolkit.Mvvm.ComponentModel;
23

34
namespace LudereAI.WPF.Models;
45

@@ -8,12 +9,6 @@ public partial class ConversationModel : ObservableObject
89
[ObservableProperty] private string _gameContext = string.Empty;
910
[ObservableProperty] private DateTime _createdAt = DateTime.Now;
1011
[ObservableProperty] private DateTime _updatedAt = DateTime.Now;
11-
12-
[ObservableProperty] private IEnumerable<MessageModel> _messages = new List<MessageModel>();
13-
14-
15-
public void AddMessage(MessageModel messageModel)
16-
{
17-
Messages = Messages.Append(messageModel);
18-
}
12+
13+
[ObservableProperty] private ObservableCollection<MessageModel> _messages = [];
1914
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
using System.Collections.ObjectModel;
2+
using AutoMapper;
3+
using LudereAI.Core.Entities.Chat;
4+
using LudereAI.Core.Interfaces.Services;
5+
using LudereAI.Shared.Enums;
6+
using LudereAI.Shared.Models;
7+
using LudereAI.WPF.Interfaces;
8+
using LudereAI.WPF.Models;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace LudereAI.WPF.Services;
12+
13+
public class ChatCoordinator : IChatCoordinator
14+
{
15+
private readonly ILogger<ChatCoordinator> _logger;
16+
private readonly IChatService _chat;
17+
private readonly IAudioService _audio;
18+
private readonly IMapper _mapper;
19+
private readonly IChatSession _session;
20+
21+
public ChatCoordinator(ILogger<ChatCoordinator> logger,
22+
IChatService chat,
23+
IAudioService audio,
24+
IMapper mapper,
25+
IChatSession session)
26+
{
27+
_logger = logger;
28+
_chat = chat;
29+
_audio = audio;
30+
_mapper = mapper;
31+
_session = session;
32+
}
33+
34+
public async Task Initialize() => await RefreshConversations();
35+
36+
public bool CanSend(string message, ConversationModel? conversation, string? gameContext)
37+
=> !string.IsNullOrWhiteSpace(message)
38+
&& !_session.IsAssistantThinking
39+
&& conversation != null
40+
&& !string.IsNullOrWhiteSpace(gameContext);
41+
42+
public async Task RefreshConversations()
43+
{
44+
try
45+
{
46+
var conversations = await _chat.GetConversations();
47+
var mappedConversations = _mapper.Map<IEnumerable<ConversationModel>>(conversations).ToList();
48+
foreach (var m in mappedConversations.SelectMany(c => c.Messages))
49+
m.CreatedAt = m.CreatedAt.ToLocalTime();
50+
51+
_session.SetConversations(new ObservableCollection<ConversationModel>(mappedConversations));
52+
}
53+
catch (Exception ex)
54+
{
55+
_logger.LogError(ex, "Failed to refresh conversations");
56+
}
57+
}
58+
59+
public async Task DeleteConversation(ConversationModel conversation)
60+
{
61+
try
62+
{
63+
var success = await _chat.DeleteConversation(conversation.Id);
64+
if (success)
65+
{
66+
_session.Conversations.Remove(conversation);
67+
if (_session.CurrentConversation == conversation)
68+
{
69+
_session.CurrentConversation = _session.Conversations.FirstOrDefault();
70+
}
71+
72+
await RefreshConversations();
73+
}
74+
else
75+
{
76+
_logger.LogWarning("Failed to delete conversation with ID {ConversationId}", conversation.Id);
77+
}
78+
}
79+
catch (Exception ex)
80+
{
81+
_logger.LogError(ex, "Failed to delete conversation");
82+
}
83+
}
84+
85+
public async Task SendMessage(string message, string? gameContext, WindowInfo? windowContext)
86+
{
87+
if (_session.CurrentConversation == null) return;
88+
89+
try
90+
{
91+
_session.IsAssistantThinking = true;
92+
_session.AddMessageLocal(message, MessageRole.User);
93+
94+
var result = await _chat.SendMessage(new ChatRequest
95+
{
96+
Message = message,
97+
ConversationId = _session.CurrentConversation.Id,
98+
GameContext = gameContext,
99+
Window = windowContext
100+
});
101+
102+
if (result is { IsSuccessful: true, Value: not null })
103+
{
104+
UpdateConversationState(result.Value);
105+
var lastAssistantMessage =
106+
_session.CurrentConversation.Messages.LastOrDefault(m => m.Role == MessageRole.Assistant);
107+
if (lastAssistantMessage?.Audio.Length > 0)
108+
{
109+
await _audio.PlayAudioAsync(lastAssistantMessage.Audio);
110+
}
111+
}
112+
else
113+
{
114+
_session.AddSystemMessage(result.Message);
115+
}
116+
}
117+
catch (Exception ex)
118+
{
119+
_logger.LogError(ex, "Failed to send message");
120+
_session.AddSystemMessage("An error occurred while sending your message. Please try again.");
121+
}
122+
finally
123+
{
124+
_session.IsAssistantThinking = false;
125+
}
126+
}
127+
128+
private void UpdateConversationState(Conversation conversation)
129+
{
130+
var updated = _mapper.Map<ConversationModel>(conversation);
131+
foreach (var m in updated.Messages) m.CreatedAt = m.CreatedAt.ToLocalTime();
132+
133+
var existing = _session.Conversations.FirstOrDefault(c => c.Id == updated.Id);
134+
if (existing != null)
135+
{
136+
var index = _session.Conversations.IndexOf(existing);
137+
_session.Conversations[index] = updated;
138+
}
139+
else
140+
{
141+
_session.Conversations.Insert(0, updated);
142+
}
143+
144+
_session.CurrentConversation = updated;
145+
_session.SetMessages(new ObservableCollection<MessageModel>(updated.Messages));
146+
}
147+
}

0 commit comments

Comments
 (0)