diff --git a/hospexplorer/ask/admin.py b/hospexplorer/ask/admin.py index 3910bbc..d221989 100644 --- a/hospexplorer/ask/admin.py +++ b/hospexplorer/ask/admin.py @@ -1,14 +1,29 @@ from django.contrib import admin -from ask.models import QARecord +from ask.models import Conversation, QARecord + + +class QARecordInline(admin.TabularInline): + model = QARecord + extra = 0 + readonly_fields = ("question_text", "question_timestamp", "answer_text", "answer_timestamp", "is_error") + fields = ("question_text", "question_timestamp", "answer_text", "answer_timestamp", "is_error") + + +@admin.register(Conversation) +class ConversationAdmin(admin.ModelAdmin): + list_display = ("id", "title", "user", "created_at", "updated_at") + list_filter = ("user",) + search_fields = ("title", "user__username") + inlines = [QARecordInline] @admin.register(QARecord) class QARecordAdmin(admin.ModelAdmin): - list_display = ["id", "user", "truncated_question", "question_timestamp", "answer_timestamp"] - list_filter = ["question_timestamp", "user"] + list_display = ["id", "user", "conversation", "truncated_question", "question_timestamp", "answer_timestamp", "is_error"] + list_filter = ["question_timestamp", "user", "is_error"] search_fields = ["question_text", "answer_text", "user__username"] readonly_fields = ["question_timestamp", "answer_timestamp", "answer_raw_response"] - raw_id_fields = ["user"] + raw_id_fields = ["user", "conversation"] date_hierarchy = "question_timestamp" def truncated_question(self, obj): diff --git a/hospexplorer/ask/context_processors.py b/hospexplorer/ask/context_processors.py new file mode 100644 index 0000000..8dfd5cc --- /dev/null +++ b/hospexplorer/ask/context_processors.py @@ -0,0 +1,26 @@ +from django.conf import settings +from django.urls import reverse +from ask.models import Conversation + + +def sidebar_conversations(request): + if request.user.is_authenticated: + limit = getattr(settings, "SIDEBAR_CONVERSATIONS_LIMIT", 10) + conversations = Conversation.objects.filter( + user=request.user + )[:limit] + + sidebar_items = [] + for conv in conversations: + label = conv.title if conv.title else conv.created_at.strftime("%b %d, %Y %I:%M %p") + sidebar_items.append({ + "id": conv.id, + "label": label, + "url": reverse("ask:conversation", kwargs={"conversation_id": conv.id}), + "updated_at": conv.updated_at.isoformat(), + }) + return { + "sidebar_conversations": sidebar_items, + "sidebar_conversations_limit": limit, + } + return {"sidebar_conversations": [], "sidebar_conversations_limit": 0} diff --git a/hospexplorer/ask/migrations/0001_initial.py b/hospexplorer/ask/migrations/0001_initial.py index 98d680a..9738a6a 100644 --- a/hospexplorer/ask/migrations/0001_initial.py +++ b/hospexplorer/ask/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.1 on 2026-01-30 19:36 +# Generated by Django 6.0.1 on 2026-02-20 21:44 import django.db.models.deletion from django.conf import settings @@ -14,6 +14,18 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Conversation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-updated_at'], + }, + ), migrations.CreateModel( name='QARecord', fields=[ @@ -23,6 +35,8 @@ class Migration(migrations.Migration): ('answer_text', models.TextField(blank=True, default='')), ('answer_raw_response', models.JSONField(default=dict)), ('answer_timestamp', models.DateTimeField(blank=True, null=True)), + ('is_error', models.BooleanField(default=False)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qa_records', to='ask.conversation')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qa_records', to=settings.AUTH_USER_MODEL)), ], options={ diff --git a/hospexplorer/ask/migrations/0002_qarecord_is_error.py b/hospexplorer/ask/migrations/0002_conversation_title.py similarity index 52% rename from hospexplorer/ask/migrations/0002_qarecord_is_error.py rename to hospexplorer/ask/migrations/0002_conversation_title.py index 10ddcac..bbdc0e3 100644 --- a/hospexplorer/ask/migrations/0002_qarecord_is_error.py +++ b/hospexplorer/ask/migrations/0002_conversation_title.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.1 on 2026-02-04 23:32 +# Generated by Django 6.0.1 on 2026-02-25 22:44 from django.db import migrations, models @@ -11,8 +11,8 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( - model_name='qarecord', - name='is_error', - field=models.BooleanField(default=False), + model_name='conversation', + name='title', + field=models.CharField(blank=True, default='', max_length=200), ), ] diff --git a/hospexplorer/ask/models.py b/hospexplorer/ask/models.py index aa69d52..76e8364 100644 --- a/hospexplorer/ask/models.py +++ b/hospexplorer/ask/models.py @@ -2,10 +2,37 @@ from django.conf import settings +class Conversation(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="conversations", + ) + title = models.CharField(max_length=200, blank=True, default="") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-updated_at"] + + def __str__(self): + if self.title: + truncated = self.title[:50] + suffix = "..." if len(self.title) > 50 else "" + return f"Conversation {self.id}: {truncated}{suffix}" + return f"Conversation {self.id} ({self.user.username})" + + class QARecord(models.Model): """ Stores a question-answer pair from user interactions with the LLM. """ + conversation = models.ForeignKey( + Conversation, + on_delete=models.CASCADE, + related_name="qa_records", + ) + # Question fields question_text = models.TextField() question_timestamp = models.DateTimeField(auto_now_add=True) @@ -16,7 +43,11 @@ class QARecord(models.Model): answer_timestamp = models.DateTimeField(null=True, blank=True) is_error = models.BooleanField(default=False) - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="qa_records") + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="qa_records", + ) class Meta: ordering = ["-question_timestamp"] diff --git a/hospexplorer/ask/static/css/ask.css b/hospexplorer/ask/static/css/ask.css index 37f72d1..23e60a7 100644 --- a/hospexplorer/ask/static/css/ask.css +++ b/hospexplorer/ask/static/css/ask.css @@ -358,4 +358,5 @@ .chat-input-bar { padding: 0.5rem 1rem; } + } diff --git a/hospexplorer/ask/static/css/styles.css b/hospexplorer/ask/static/css/styles.css index 9967a22..53d956c 100644 --- a/hospexplorer/ask/static/css/styles.css +++ b/hospexplorer/ask/static/css/styles.css @@ -10894,10 +10894,19 @@ body.sb-sidenav-toggled #wrapper #sidebar-wrapper { } #sidebar-wrapper .question-item:hover { - border-left-color: #0d6efd; + border-left-color: #8c1d40; background-color: #f8f9fa; } +#sidebar-wrapper .question-item.active { + border-left-color: #8c1d40; + background-color: rgba(140, 29, 64, 0.1); + color: #8c1d40; + font-weight: 600; + border-color: transparent; + border-left: 3px solid #8c1d40; +} + #sidebar-wrapper .sidebar-section-heading { font-weight: 600; text-transform: uppercase; diff --git a/hospexplorer/ask/templates/_base.html b/hospexplorer/ask/templates/_base.html index 6fdaca8..2619e7e 100644 --- a/hospexplorer/ask/templates/_base.html +++ b/hospexplorer/ask/templates/_base.html @@ -18,31 +18,41 @@