From 7ccfda909e87adbc0bb672fad8b5a0aa3d7112c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 06:52:16 +0000 Subject: [PATCH 01/11] Add chat persistence with sessions, messages, sidebar, rename, delete Agent-Logs-Url: https://github.com/spech66/lifelogbb/sessions/18938176-f8ee-4198-8aed-5bd3c53fb369 Co-authored-by: spech66 <22615238+spech66@users.noreply.github.com> --- LifelogBb/Controllers/ChatController.cs | 171 +++++- ...20260516065018_ChatPersistence.Designer.cs | 557 ++++++++++++++++++ .../20260516065018_ChatPersistence.cs | 69 +++ .../LifelogBbContextModelSnapshot.cs | 71 +++ LifelogBb/Models/Chat/ChatSessionListItem.cs | 9 + LifelogBb/Models/Chat/ChatViewModel.cs | 3 + LifelogBb/Models/Entities/ChatSession.cs | 13 + .../Models/Entities/ChatSessionMessage.cs | 20 + LifelogBb/Models/LifelogBbContext.cs | 8 + LifelogBb/Views/Chat/Index.cshtml | 160 ++++- 10 files changed, 1050 insertions(+), 31 deletions(-) create mode 100644 LifelogBb/Migrations/20260516065018_ChatPersistence.Designer.cs create mode 100644 LifelogBb/Migrations/20260516065018_ChatPersistence.cs create mode 100644 LifelogBb/Models/Chat/ChatSessionListItem.cs create mode 100644 LifelogBb/Models/Entities/ChatSession.cs create mode 100644 LifelogBb/Models/Entities/ChatSessionMessage.cs diff --git a/LifelogBb/Controllers/ChatController.cs b/LifelogBb/Controllers/ChatController.cs index b529917..8a7715a 100644 --- a/LifelogBb/Controllers/ChatController.cs +++ b/LifelogBb/Controllers/ChatController.cs @@ -3,6 +3,7 @@ using LifelogBb.Models.Chat; using LifelogBb.Models.Entities; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using System.Text.Json; using Westwind.AspNetCore.Markdown; @@ -19,12 +20,55 @@ public ChatController(LifelogBbContext context, ChatService chatService) _chatService = chatService; } - public IActionResult Index() + public async Task Index(long? id) { var config = Config.GetConfig(_context); + var sessions = await _context.ChatSessions + .OrderByDescending(s => s.UpdatedAt) + .Select(s => new ChatSessionListItem + { + Id = s.Id, + Name = s.Name, + UpdatedAt = s.UpdatedAt + }) + .ToListAsync(); + + ChatSession? activeSession = null; + if (id.HasValue) + { + activeSession = await _context.ChatSessions + .Include(s => s.Messages.OrderBy(m => m.SortOrder)) + .FirstOrDefaultAsync(s => s.Id == id.Value); + } + + if (activeSession == null && sessions.Count > 0) + { + activeSession = await _context.ChatSessions + .Include(s => s.Messages.OrderBy(m => m.SortOrder)) + .OrderByDescending(s => s.UpdatedAt) + .FirstOrDefaultAsync(); + } + + var messages = new List(); + if (activeSession != null) + { + foreach (var msg in activeSession.Messages) + { + messages.Add(new ChatMessageViewModel + { + Role = msg.Role, + Content = msg.Content + }); + } + } + var model = new ChatViewModel { - IsConfigured = !string.IsNullOrWhiteSpace(config.ChatApiKey) + IsConfigured = !string.IsNullOrWhiteSpace(config.ChatApiKey), + Sessions = sessions, + ActiveSessionId = activeSession?.Id, + ActiveSessionName = activeSession?.Name, + Messages = messages }; return View(model); } @@ -40,15 +84,24 @@ public async Task Send([FromBody] ChatSendRequest request) var conversation = new List(); - // Add existing conversation history - if (request.History != null) + // Load existing messages from the session if we have one + ChatSession? session = null; + if (request.SessionId.HasValue) { - foreach (var msg in request.History) + session = await _context.ChatSessions + .Include(s => s.Messages.OrderBy(m => m.SortOrder)) + .FirstOrDefaultAsync(s => s.Id == request.SessionId.Value); + } + + if (session != null) + { + // Build conversation from persisted messages + foreach (var msg in session.Messages) { conversation.Add(new ChatMessage { Role = msg.Role, - Content = msg.Content + Content = msg.Role == "assistant" ? StripHtml(msg.Content) : msg.Content }); } } @@ -63,13 +116,117 @@ public async Task Send([FromBody] ChatSendRequest request) var response = await _chatService.SendAsync(conversation); var html = Markdown.Parse(response); - return Json(new { response = html }); + // Persist to database + if (session == null) + { + session = new ChatSession + { + Name = request.Message.Length > 50 ? request.Message[..50] + "..." : request.Message + }; + session.SetCreateFields(); + _context.ChatSessions.Add(session); + await _context.SaveChangesAsync(); + } + + var nextSortOrder = session.Messages.Count > 0 + ? session.Messages.Max(m => m.SortOrder) + 1 + : 0; + + var userMsg = new ChatSessionMessage + { + ChatSessionId = session.Id, + Role = "user", + Content = request.Message, + SortOrder = nextSortOrder + }; + userMsg.SetCreateFields(); + + var assistantMsg = new ChatSessionMessage + { + ChatSessionId = session.Id, + Role = "assistant", + Content = html, + SortOrder = nextSortOrder + 1 + }; + assistantMsg.SetCreateFields(); + + _context.ChatSessionMessages.AddRange(userMsg, assistantMsg); + session.SetUpdateFields(); + await _context.SaveChangesAsync(); + + return Json(new { response = html, sessionId = session.Id, sessionName = session.Name }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task CreateSession() + { + var session = new ChatSession + { + Name = "New Chat" + }; + session.SetCreateFields(); + _context.ChatSessions.Add(session); + await _context.SaveChangesAsync(); + + return RedirectToAction(nameof(Index), new { id = session.Id }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RenameSession([FromBody] RenameSessionRequest request) + { + if (request == null || string.IsNullOrWhiteSpace(request.Name)) + { + return Json(new { error = "Name is required." }); + } + + var session = await _context.ChatSessions.FindAsync(request.Id); + if (session == null) + { + return NotFound(); + } + + session.Name = request.Name.Length > 200 ? request.Name[..200] : request.Name; + session.SetUpdateFields(); + await _context.SaveChangesAsync(); + + return Json(new { success = true, name = session.Name }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task DeleteSession(long id) + { + var session = await _context.ChatSessions.FindAsync(id); + if (session == null) + { + return NotFound(); + } + + _context.ChatSessions.Remove(session); + await _context.SaveChangesAsync(); + + return RedirectToAction(nameof(Index)); + } + + private static string StripHtml(string html) + { + // Simple HTML tag removal for sending back to AI as plain text + return System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", "").Trim(); } } public class ChatSendRequest { public string Message { get; set; } = ""; + public long? SessionId { get; set; } public List? History { get; set; } } + + public class RenameSessionRequest + { + public long Id { get; set; } + public string Name { get; set; } = ""; + } } diff --git a/LifelogBb/Migrations/20260516065018_ChatPersistence.Designer.cs b/LifelogBb/Migrations/20260516065018_ChatPersistence.Designer.cs new file mode 100644 index 0000000..dfbb12a --- /dev/null +++ b/LifelogBb/Migrations/20260516065018_ChatPersistence.Designer.cs @@ -0,0 +1,557 @@ +// +using System; +using LifelogBb.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LifelogBb.Migrations +{ + [DbContext(typeof(LifelogBbContext))] + [Migration("20260516065018_ChatPersistence")] + partial class ChatPersistence + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + + modelBuilder.Entity("LifelogBb.Models.Entities.BucketList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ImageFileName") + .HasColumnType("TEXT"); + + b.Property("ImageName") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BucketLists"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.ChatSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ChatSessions"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.ChatSessionMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSessionId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChatSessionId"); + + b.ToTable("ChatSessionMessages"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.Config", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketListPageSize") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(12); + + b.Property("ChatApiKey") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ChatEndpoint") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("https://api.openai.com/v1/chat/completions"); + + b.Property("ChatMaxToolRoundtrips") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(10); + + b.Property("ChatModel") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("gpt-4o"); + + b.Property("ChatSystemPrompt") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("You are a helpful life-tracking assistant for LifelogBB. You can query the user's data (weights, journals, todos, goals, habits, quotes, strength trainings, endurance trainings) using the available tools. Summarize and analyze the data to help the user understand their progress and habits."); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EnduranceTrainingPageSize") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(20); + + b.Property("FeedTimeZone") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Europe/Berlin"); + + b.Property("FeedToken") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("ChangeMeInTheConfig"); + + b.Property("GoalPageSize") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(20); + + b.Property("HabitPageSize") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(20); + + b.Property("Height") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(170); + + b.Property("JournalPageSize") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(20); + + b.Property("QuotePageSize") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(20); + + b.Property("StartOfWeek") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + b.Property("StrengthTrainingPageSize") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(20); + + b.Property("TodoPageSize") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(20); + + b.Property("UnitsType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("WeightPageSize") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(20); + + b.Property("WeightWarning") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(1.0); + + b.Property("WeightWarningText") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("You are gaining weight!"); + + b.HasKey("Id"); + + b.ToTable("Configs"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.EnduranceTraining", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("Exercise") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EnduranceTrainings"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.Goal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("REAL"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("InitialValue") + .HasColumnType("REAL"); + + b.Property("IsCompleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TargetValue") + .HasColumnType("REAL"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Goals"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.Habit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("IsCompleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RecurrenceRules") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Habits"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.Journal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date") + .IsUnique(); + + b.ToTable("Journals"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.StrengthTraining", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Exercise") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Reps") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Weight") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("StrengthTrainings"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.Todo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("DueDate") + .HasColumnType("TEXT"); + + b.Property("IsCompleted") + .HasColumnType("INTEGER"); + + b.Property("IsImportant") + .HasColumnType("INTEGER"); + + b.Property("Progress") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Todos"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.Weight", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bmi") + .HasColumnType("REAL"); + + b.Property("BodyWeight") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Weights"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.ChatSessionMessage", b => + { + b.HasOne("LifelogBb.Models.Entities.ChatSession", "ChatSession") + .WithMany("Messages") + .HasForeignKey("ChatSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSession"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.ChatSession", b => + { + b.Navigation("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LifelogBb/Migrations/20260516065018_ChatPersistence.cs b/LifelogBb/Migrations/20260516065018_ChatPersistence.cs new file mode 100644 index 0000000..e950f35 --- /dev/null +++ b/LifelogBb/Migrations/20260516065018_ChatPersistence.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LifelogBb.Migrations +{ + /// + public partial class ChatPersistence : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ChatSessions", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChatSessions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ChatSessionMessages", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ChatSessionId = table.Column(type: "INTEGER", nullable: false), + Role = table.Column(type: "TEXT", nullable: false), + Content = table.Column(type: "TEXT", nullable: false), + SortOrder = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChatSessionMessages", x => x.Id); + table.ForeignKey( + name: "FK_ChatSessionMessages_ChatSessions_ChatSessionId", + column: x => x.ChatSessionId, + principalTable: "ChatSessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ChatSessionMessages_ChatSessionId", + table: "ChatSessionMessages", + column: "ChatSessionId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ChatSessionMessages"); + + migrationBuilder.DropTable( + name: "ChatSessions"); + } + } +} diff --git a/LifelogBb/Migrations/LifelogBbContextModelSnapshot.cs b/LifelogBb/Migrations/LifelogBbContextModelSnapshot.cs index 022d698..73f9f5f 100644 --- a/LifelogBb/Migrations/LifelogBbContextModelSnapshot.cs +++ b/LifelogBb/Migrations/LifelogBbContextModelSnapshot.cs @@ -56,6 +56,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("BucketLists"); }); + modelBuilder.Entity("LifelogBb.Models.Entities.ChatSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ChatSessions"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.ChatSessionMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSessionId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChatSessionId"); + + b.ToTable("ChatSessionMessages"); + }); + modelBuilder.Entity("LifelogBb.Models.Entities.Config", b => { b.Property("Id") @@ -477,6 +532,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Weights"); }); + + modelBuilder.Entity("LifelogBb.Models.Entities.ChatSessionMessage", b => + { + b.HasOne("LifelogBb.Models.Entities.ChatSession", "ChatSession") + .WithMany("Messages") + .HasForeignKey("ChatSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSession"); + }); + + modelBuilder.Entity("LifelogBb.Models.Entities.ChatSession", b => + { + b.Navigation("Messages"); + }); #pragma warning restore 612, 618 } } diff --git a/LifelogBb/Models/Chat/ChatSessionListItem.cs b/LifelogBb/Models/Chat/ChatSessionListItem.cs new file mode 100644 index 0000000..903e72e --- /dev/null +++ b/LifelogBb/Models/Chat/ChatSessionListItem.cs @@ -0,0 +1,9 @@ +namespace LifelogBb.Models.Chat +{ + public class ChatSessionListItem + { + public long Id { get; set; } + public string Name { get; set; } = ""; + public DateTime UpdatedAt { get; set; } + } +} diff --git a/LifelogBb/Models/Chat/ChatViewModel.cs b/LifelogBb/Models/Chat/ChatViewModel.cs index bfce277..cb5c60a 100644 --- a/LifelogBb/Models/Chat/ChatViewModel.cs +++ b/LifelogBb/Models/Chat/ChatViewModel.cs @@ -5,5 +5,8 @@ public class ChatViewModel public List Messages { get; set; } = new(); public string UserInput { get; set; } = ""; public bool IsConfigured { get; set; } + public List Sessions { get; set; } = new(); + public long? ActiveSessionId { get; set; } + public string? ActiveSessionName { get; set; } } } diff --git a/LifelogBb/Models/Entities/ChatSession.cs b/LifelogBb/Models/Entities/ChatSession.cs new file mode 100644 index 0000000..d2006d4 --- /dev/null +++ b/LifelogBb/Models/Entities/ChatSession.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace LifelogBb.Models.Entities +{ + public class ChatSession : BaseEntity + { + [Required] + [StringLength(200)] + public string Name { get; set; } = "New Chat"; + + public ICollection Messages { get; set; } = new List(); + } +} diff --git a/LifelogBb/Models/Entities/ChatSessionMessage.cs b/LifelogBb/Models/Entities/ChatSessionMessage.cs new file mode 100644 index 0000000..75008ec --- /dev/null +++ b/LifelogBb/Models/Entities/ChatSessionMessage.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace LifelogBb.Models.Entities +{ + public class ChatSessionMessage : BaseEntity + { + [Required] + public long ChatSessionId { get; set; } + + [Required] + public string Role { get; set; } = "user"; + + [Required] + public string Content { get; set; } = ""; + + public int SortOrder { get; set; } + + public ChatSession ChatSession { get; set; } = null!; + } +} diff --git a/LifelogBb/Models/LifelogBbContext.cs b/LifelogBb/Models/LifelogBbContext.cs index 87c1d58..b226cd5 100644 --- a/LifelogBb/Models/LifelogBbContext.cs +++ b/LifelogBb/Models/LifelogBbContext.cs @@ -18,6 +18,8 @@ public class LifelogBbContext : DbContext public DbSet Habits { get; set; } = null!; public DbSet Goals { get; set; } = null!; public DbSet Configs { get; set; } = null!; + public DbSet ChatSessions { get; set; } = null!; + public DbSet ChatSessionMessages { get; set; } = null!; private readonly IConfiguration _configuration; @@ -77,6 +79,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().Property(b => b.ChatSystemPrompt).HasDefaultValue("You are a helpful life-tracking assistant for LifelogBB. You can query the user's data (weights, journals, todos, goals, habits, quotes, strength trainings, endurance trainings) using the available tools. Summarize and analyze the data to help the user understand their progress and habits."); modelBuilder.Entity().Property(b => b.ChatMaxToolRoundtrips).HasDefaultValue(10); + modelBuilder.Entity() + .HasMany(s => s.Messages) + .WithOne(m => m.ChatSession) + .HasForeignKey(m => m.ChatSessionId) + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity().HasIndex(j => j.Date).IsUnique(); } diff --git a/LifelogBb/Views/Chat/Index.cshtml b/LifelogBb/Views/Chat/Index.cshtml index 1359587..a8535a7 100644 --- a/LifelogBb/Views/Chat/Index.cshtml +++ b/LifelogBb/Views/Chat/Index.cshtml @@ -7,9 +7,9 @@
@Html.AntiForgeryToken()
-
- @if (!Model.IsConfigured) - { + @if (!Model.IsConfigured) + { +
- } +
+ } +
+
+
+

Chats

+
+
+ +
+
+
+
+ @if (Model.Sessions.Count == 0) + { +
+ No chats yet +
+ } + @foreach (var session in Model.Sessions) + { +
+ + @session.Name + +
+ +
+ +
+
+
+ } +
+
+
+
-

Chat

+

+ + @(Model.ActiveSessionName ?? "Chat") +

-
-
- - Ask me anything about your life tracking data. -
+ @if (Model.Messages.Count == 0) + { +
+ + Ask me anything about your life tracking data. +
+ }