From 69108f9e347cb2371c31b6d88125a7e5b51b7358 Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Wed, 28 Jan 2026 16:57:03 -0700 Subject: [PATCH 01/15] [HOP-7] Added django-allauth, configured settings, added views and urls --- hospexplorer/ask/urls.py | 7 +++-- hospexplorer/ask/views.py | 6 +++- hospexplorer/hospexplorer/settings.py | 44 +++++++++++++++++++++++++-- hospexplorer/hospexplorer/urls.py | 1 + pyproject.toml | 1 + uv.lock | 15 +++++++++ 6 files changed, 68 insertions(+), 6 deletions(-) diff --git a/hospexplorer/ask/urls.py b/hospexplorer/ask/urls.py index 192b970..2a75452 100644 --- a/hospexplorer/ask/urls.py +++ b/hospexplorer/ask/urls.py @@ -1,9 +1,10 @@ -from django.contrib import admin from django.urls import path from ask import views +app_name = "ask" + urlpatterns = [ - path("", views.index), + path("", views.index, name="index"), path("mock", views.mock_response, name="mock-response"), - path("query", views.query, name="query-llm") + path("query", views.query, name="query-llm"), ] \ No newline at end of file diff --git a/hospexplorer/ask/views.py b/hospexplorer/ask/views.py index afcbccb..fe212b2 100644 --- a/hospexplorer/ask/views.py +++ b/hospexplorer/ask/views.py @@ -1,17 +1,21 @@ from django.shortcuts import render from django.http import JsonResponse from django.conf import settings +from django.contrib.auth.decorators import login_required import ask.llm_connector -# Create your views here. +@login_required def index(request): return render(request, "index.html", {}) + +@login_required def mock_response(request): return JsonResponse({ "message": "Okay, the user wants a three-sentence bedtime story about a unicorn. Let's start by thinking about the key elements of a good bedtime story. They usually have a peaceful setting, a gentle conflict or quest, and a happy ending.\n\nFirst sentence needs to set the scene. Maybe a magical forest with a unicorn. Luna is a common unicorn name, sounds soft. Moonlight and stars could add a calming effect.\n\nSecond sentence should introduce a small problem or something the unicorn does. Healing powers are typical for unicorns. Maybe she finds an injured animal, like a fox. Using her horn to heal adds magic.\n\nThird sentence wraps it up with a happy ending. The fox recovers, they become friends, and the forest is peaceful. Emphasize safety and dreams to make it soothing for bedtime.\n\nCheck if it's exactly three sentences. Yes. Language is simple and comforting, suitable for a child. Avoid any scary elements. Make sure it flows smoothly and conveys warmth.\n\n\nUnder the shimmering moonlit sky, a silver-maned unicorn named Luna trotted through the enchanted forest, her hooves leaving trails of stardust. When she discovered a wounded fox whimpering beneath an ancient oak, she touched her glowing horn to its paw, weaving magic that healed the hurt. With the fox curled beside her, Luna rested on a bed of moss, her heart full as the forest whispered lullabies, ensuring all creatures drifted into dreams of peace." }) +@login_required def query(request): return JsonResponse(ask.llm_connector.query_llm(request.GET["query"])) \ No newline at end of file diff --git a/hospexplorer/hospexplorer/settings.py b/hospexplorer/hospexplorer/settings.py index ade57c1..b41aded 100644 --- a/hospexplorer/hospexplorer/settings.py +++ b/hospexplorer/hospexplorer/settings.py @@ -38,7 +38,21 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "ask" + "django.contrib.sites", + + # Project apps + "ask", + + # Allauth + "allauth", + "allauth.account", +] + +SITE_ID = 1 + +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", ] MIDDLEWARE = [ @@ -47,6 +61,7 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "allauth.account.middleware.AccountMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] @@ -126,4 +141,29 @@ LLM_HOST = os.getenv("LLM_HOST", "mockserver") LLM_TOKEN = os.getenv("LLM_TOKEN", "") LLM_MODEL = os.getenv("LLM_MODEL", "") -LLM_QUERY_ENDPOINT = os.getenv("LLM_QUERY_ENDPOINT", "v1/chat/completions") \ No newline at end of file +LLM_QUERY_ENDPOINT = os.getenv("LLM_QUERY_ENDPOINT", "v1/chat/completions") + + +# Django-Allauth Configuration +LOGIN_REDIRECT_URL = "/ask/" +LOGOUT_REDIRECT_URL = "/accounts/login/" +LOGIN_URL = "/accounts/login/" + +ACCOUNT_LOGIN_METHODS = {"email", "username"} +ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"] +ACCOUNT_EMAIL_VERIFICATION = "optional" +ACCOUNT_LOGOUT_ON_GET = False +ACCOUNT_SESSION_REMEMBER = True + + +# For production, uncomment the following setting +# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +# Production SMTP settings +EMAIL_HOST = os.getenv("EMAIL_HOST", "smtp.gmail.com") +EMAIL_PORT = int(os.getenv("EMAIL_PORT", 587)) +EMAIL_USE_TLS = True +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "") +DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "noreply@example.com") \ No newline at end of file diff --git a/hospexplorer/hospexplorer/urls.py b/hospexplorer/hospexplorer/urls.py index f84485d..11d6fa7 100644 --- a/hospexplorer/hospexplorer/urls.py +++ b/hospexplorer/hospexplorer/urls.py @@ -26,5 +26,6 @@ urlpatterns = [ path("admin/", admin.site.urls), + path("accounts/", include("allauth.urls")), path("ask/", include("ask.urls")), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # TODO: set up for prod/dev \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 885c121..8afa900 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,5 +6,6 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "django>=6.0.1", + "django-allauth>=65.0.0", "requests>=2.32.5", ] diff --git a/uv.lock b/uv.lock index f54fe5c..4d46db5 100644 --- a/uv.lock +++ b/uv.lock @@ -91,18 +91,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/b5/814ed98bd21235c116fd3436a7ed44d47560329a6d694ec8aac2982dbb93/django-6.0.1-py3-none-any.whl", hash = "sha256:a92a4ff14f664a896f9849009cb8afaca7abe0d6fc53325f3d1895a15253433d", size = 8338791, upload-time = "2026-01-06T18:55:46.175Z" }, ] +[[package]] +name = "django-allauth" +version = "65.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/9b/061a6ac65c602eb721b13fbf9c665b20fb900f113a03ec8521b5fcf16b83/django_allauth-65.14.0.tar.gz", hash = "sha256:5529227aba2b1377d900e9274a3f24496c645e65400fbae3cad5789944bc4d0b", size = 1991909, upload-time = "2026-01-17T18:43:12.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/c8/2f959ff8466913d95ba72eb4a29bd7998d28a559786033a97b5bbdda2b81/django_allauth-65.14.0-py3-none-any.whl", hash = "sha256:448f5f7877f95fcbe1657256510fe7822d7871f202521a29e23ef937f3325a97", size = 1793052, upload-time = "2026-01-17T18:43:08.954Z" }, +] + [[package]] name = "hosp-explorer" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "django" }, + { name = "django-allauth" }, { name = "requests" }, ] [package.metadata] requires-dist = [ { name = "django", specifier = ">=6.0.1" }, + { name = "django-allauth", specifier = ">=65.0.0" }, { name = "requests", specifier = ">=2.32.5" }, ] From ed39b3c0dba96eadc8a56ab3808d60125d34dff6 Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Thu, 29 Jan 2026 09:50:01 -0700 Subject: [PATCH 02/15] [HOP-7] Templates --- hospexplorer/ask/templates/_base.html | 8 +- .../ask/templates/account/_base_auth.html | 16 ++++ hospexplorer/ask/templates/account/login.html | 83 ++++++++++++++++++ .../ask/templates/account/logout.html | 22 +++++ .../ask/templates/account/password_reset.html | 57 ++++++++++++ .../account/password_reset_done.html | 31 +++++++ .../account/password_reset_from_key.html | 86 +++++++++++++++++++ .../account/password_reset_from_key_done.html | 29 +++++++ hospexplorer/ask/templates/index.html | 2 +- 9 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 hospexplorer/ask/templates/account/_base_auth.html create mode 100644 hospexplorer/ask/templates/account/login.html create mode 100644 hospexplorer/ask/templates/account/logout.html create mode 100644 hospexplorer/ask/templates/account/password_reset.html create mode 100644 hospexplorer/ask/templates/account/password_reset_done.html create mode 100644 hospexplorer/ask/templates/account/password_reset_from_key.html create mode 100644 hospexplorer/ask/templates/account/password_reset_from_key_done.html diff --git a/hospexplorer/ask/templates/_base.html b/hospexplorer/ask/templates/_base.html index f03bec4..f7e42e8 100644 --- a/hospexplorer/ask/templates/_base.html +++ b/hospexplorer/ask/templates/_base.html @@ -7,9 +7,7 @@ HosP - - @@ -22,6 +20,12 @@ Hopper
+ {% if user.is_authenticated %} +
+
Signed in as {{ user.username }}
+ Sign Out +
+ {% endif %}
diff --git a/hospexplorer/ask/templates/account/_base_auth.html b/hospexplorer/ask/templates/account/_base_auth.html new file mode 100644 index 0000000..b62f87b --- /dev/null +++ b/hospexplorer/ask/templates/account/_base_auth.html @@ -0,0 +1,16 @@ +{% load static %} + + + + + + Hopper + + + +
+ {% block content %}{% endblock %} +
+ + + diff --git a/hospexplorer/ask/templates/account/login.html b/hospexplorer/ask/templates/account/login.html new file mode 100644 index 0000000..4b8a7c9 --- /dev/null +++ b/hospexplorer/ask/templates/account/login.html @@ -0,0 +1,83 @@ +{% extends "account/_base_auth.html" %} +{% load static %} + +{% block content %} +
+
+
+
+ Hopper Logo +

Sign In to Hopper

+

Enter your credentials to continue

+
+ + {% if form.errors %} + + {% endif %} + +
+ {% csrf_token %} + +
+ + + {% if form.login.errors %} +
+ {{ form.login.errors.0 }} +
+ {% endif %} +
+ +
+ + + {% if form.password.errors %} +
+ {{ form.password.errors.0 }} +
+ {% endif %} +
+ +
+ + +
+ + {% if redirect_field_value %} + + {% endif %} + +
+ +
+
+ +
+ + +
+
+
+{% endblock %} diff --git a/hospexplorer/ask/templates/account/logout.html b/hospexplorer/ask/templates/account/logout.html new file mode 100644 index 0000000..1e361ff --- /dev/null +++ b/hospexplorer/ask/templates/account/logout.html @@ -0,0 +1,22 @@ +{% extends "account/_base_auth.html" %} +{% load static %} + +{% block content %} +
+
+
+ Hopper Logo +

Sign Out

+

Are you sure you want to sign out?

+ +
+ {% csrf_token %} +
+ + Cancel +
+
+
+
+
+{% endblock %} diff --git a/hospexplorer/ask/templates/account/password_reset.html b/hospexplorer/ask/templates/account/password_reset.html new file mode 100644 index 0000000..f29ed08 --- /dev/null +++ b/hospexplorer/ask/templates/account/password_reset.html @@ -0,0 +1,57 @@ +{% extends "account/_base_auth.html" %} +{% load static %} + +{% block content %} +
+
+
+
+ Hopper Logo +

Reset Password

+

+ Enter your email address and we'll send you a link to reset your password. +

+
+ + {% if form.errors %} + + {% endif %} + +
+ {% csrf_token %} + +
+ + + {% if form.email.errors %} +
+ {{ form.email.errors.0 }} +
+ {% endif %} +
+ +
+ +
+
+ +
+ + +
+
+
+{% endblock %} diff --git a/hospexplorer/ask/templates/account/password_reset_done.html b/hospexplorer/ask/templates/account/password_reset_done.html new file mode 100644 index 0000000..2197c3c --- /dev/null +++ b/hospexplorer/ask/templates/account/password_reset_done.html @@ -0,0 +1,31 @@ +{% extends "account/_base_auth.html" %} +{% load static %} + +{% block content %} +
+
+
+ Hopper Logo +
+ + + +
+

Check Your Email

+

+ We've sent a password reset link to your email address. + Please check your inbox and follow the instructions. +

+

+ If you don't see the email, check your spam folder. +

+ + +
+
+
+{% endblock %} diff --git a/hospexplorer/ask/templates/account/password_reset_from_key.html b/hospexplorer/ask/templates/account/password_reset_from_key.html new file mode 100644 index 0000000..5146682 --- /dev/null +++ b/hospexplorer/ask/templates/account/password_reset_from_key.html @@ -0,0 +1,86 @@ +{% extends "account/_base_auth.html" %} +{% load static %} + +{% block content %} +
+
+
+
+ Hopper Logo +

Set New Password

+ {% if token_fail %} +

+ This password reset link is invalid or has expired. +

+ {% else %} +

+ Enter your new password below. +

+ {% endif %} +
+ + {% if token_fail %} + + + {% else %} + + {% if form.errors %} + + {% endif %} + +
+ {% csrf_token %} + +
+ + + {% if form.password1.errors %} +
+ {{ form.password1.errors.0 }} +
+ {% endif %} +
+ Password must be at least 8 characters and not entirely numeric. +
+
+ +
+ + + {% if form.password2.errors %} +
+ {{ form.password2.errors.0 }} +
+ {% endif %} +
+ +
+ +
+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/hospexplorer/ask/templates/account/password_reset_from_key_done.html b/hospexplorer/ask/templates/account/password_reset_from_key_done.html new file mode 100644 index 0000000..38c62e1 --- /dev/null +++ b/hospexplorer/ask/templates/account/password_reset_from_key_done.html @@ -0,0 +1,29 @@ +{% extends "account/_base_auth.html" %} +{% load static %} + +{% block content %} +
+
+
+ Hopper Logo +
+ + + + +
+

Password Reset Complete

+

+ Your password has been successfully reset. + You can now sign in with your new password. +

+ + +
+
+
+{% endblock %} diff --git a/hospexplorer/ask/templates/index.html b/hospexplorer/ask/templates/index.html index a1631af..6944669 100644 --- a/hospexplorer/ask/templates/index.html +++ b/hospexplorer/ask/templates/index.html @@ -16,7 +16,7 @@

Hopper

answers: [], async getAnswer() { - this.answer = await (await fetch('{% url 'query-llm' %}?query=Tell me a story')).json(); + this.answer = await (await fetch('{% url 'ask:query-llm' %}?query=Tell me a story')).json(); // log out all the posts to the console this.answers.push(this.answer.message) From 9f8a3efa22042e6bf650f403cdfc0ba8992d637a Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Thu, 29 Jan 2026 12:20:18 -0700 Subject: [PATCH 03/15] [HOP-7] Final templates, better comments in settings, uv lock --- .../email/password_reset_key_message.html | 58 +++++++++++++++++++ .../email/password_reset_key_message.txt | 9 +++ .../email/password_reset_key_subject.txt | 1 + hospexplorer/hospexplorer/settings.py | 3 +- uv.lock | 20 +++---- 5 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 hospexplorer/ask/templates/account/email/password_reset_key_message.html create mode 100644 hospexplorer/ask/templates/account/email/password_reset_key_message.txt create mode 100644 hospexplorer/ask/templates/account/email/password_reset_key_subject.txt diff --git a/hospexplorer/ask/templates/account/email/password_reset_key_message.html b/hospexplorer/ask/templates/account/email/password_reset_key_message.html new file mode 100644 index 0000000..acf0352 --- /dev/null +++ b/hospexplorer/ask/templates/account/email/password_reset_key_message.html @@ -0,0 +1,58 @@ +{% load i18n %} + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ Arizona State University +

Password Reset

+
+

+ {% blocktrans %}You're receiving this email because you or someone else has requested a password reset for your user account.{% endblocktrans %} +

+

+ {% blocktrans %}If you did not request this, you can safely ignore this email.{% endblocktrans %} +

+ + + + + +
+ {% trans "Reset Your Password" %} +
+ {% if username %} +

+ {% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %} +

+ {% endif %} +
+

+ {% blocktrans %}Thank you for using Hopper!{% endblocktrans %}
+ {{ current_site.domain }} +

+
+
+ + diff --git a/hospexplorer/ask/templates/account/email/password_reset_key_message.txt b/hospexplorer/ask/templates/account/email/password_reset_key_message.txt new file mode 100644 index 0000000..c7e40e4 --- /dev/null +++ b/hospexplorer/ask/templates/account/email/password_reset_key_message.txt @@ -0,0 +1,9 @@ +{% load i18n %}{% autoescape off %}{% blocktrans %}You're receiving this email because you or someone else has requested a password reset for your user account. +It can be safely ignored if you did not request a password reset. Click the link below to reset your password.{% endblocktrans %} + +{{ password_reset_url }}{% if username %} + +{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}{% endif %} + +{% blocktrans %}Thank you for using {{ current_site.name }}! +{{ current_site.domain }}{% endblocktrans %}{% endautoescape %} diff --git a/hospexplorer/ask/templates/account/email/password_reset_key_subject.txt b/hospexplorer/ask/templates/account/email/password_reset_key_subject.txt new file mode 100644 index 0000000..5ffeba6 --- /dev/null +++ b/hospexplorer/ask/templates/account/email/password_reset_key_subject.txt @@ -0,0 +1 @@ +{% load i18n %}{% autoescape off %}{% blocktrans %}Password Reset{% endblocktrans %}{% endautoescape %} diff --git a/hospexplorer/hospexplorer/settings.py b/hospexplorer/hospexplorer/settings.py index edd3992..d18d342 100644 --- a/hospexplorer/hospexplorer/settings.py +++ b/hospexplorer/hospexplorer/settings.py @@ -163,8 +163,9 @@ ACCOUNT_SESSION_REMEMBER = True -# For production, uncomment the following setting +# For production, uncomment the following setting and comment out the console backend # EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # Production SMTP settings diff --git a/uv.lock b/uv.lock index 1d1c735..6ad3ff4 100644 --- a/uv.lock +++ b/uv.lock @@ -120,6 +120,16 @@ requires-dist = [ { name = "django", specifier = ">=6.0.1" }, { name = "django-allauth", specifier = ">=65.0.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.0" }, + { name = "requests", specifier = ">=2.32.5" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] @@ -178,16 +188,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" }, { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" }, { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" }, - { name = "requests", specifier = ">=2.32.5" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] From 08ca48c715116b8febfb0adc7aba50245262b1e7 Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Fri, 30 Jan 2026 13:02:59 -0700 Subject: [PATCH 04/15] [HOP-8] Added models, views for QA storage and added to admin --- hospexplorer/ask/admin.py | 15 ++++++++- hospexplorer/ask/migrations/0001_initial.py | 35 +++++++++++++++++++++ hospexplorer/ask/models.py | 30 +++++++++++++++++- hospexplorer/ask/views.py | 24 +++++++++++++- 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 hospexplorer/ask/migrations/0001_initial.py diff --git a/hospexplorer/ask/admin.py b/hospexplorer/ask/admin.py index 8c38f3f..3910bbc 100644 --- a/hospexplorer/ask/admin.py +++ b/hospexplorer/ask/admin.py @@ -1,3 +1,16 @@ from django.contrib import admin +from ask.models import QARecord -# Register your models here. + +@admin.register(QARecord) +class QARecordAdmin(admin.ModelAdmin): + list_display = ["id", "user", "truncated_question", "question_timestamp", "answer_timestamp"] + list_filter = ["question_timestamp", "user"] + search_fields = ["question_text", "answer_text", "user__username"] + readonly_fields = ["question_timestamp", "answer_timestamp", "answer_raw_response"] + raw_id_fields = ["user"] + date_hierarchy = "question_timestamp" + + def truncated_question(self, obj): + return obj.question_text[:75] + "..." if len(obj.question_text) > 75 else obj.question_text + truncated_question.short_description = "Question" diff --git a/hospexplorer/ask/migrations/0001_initial.py b/hospexplorer/ask/migrations/0001_initial.py new file mode 100644 index 0000000..98d680a --- /dev/null +++ b/hospexplorer/ask/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.1 on 2026-01-30 19:36 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='QARecord', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question_text', models.TextField()), + ('question_timestamp', models.DateTimeField(auto_now_add=True)), + ('answer_text', models.TextField(blank=True, default='')), + ('answer_raw_response', models.JSONField(default=dict)), + ('answer_timestamp', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qa_records', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Q&A Record', + 'verbose_name_plural': 'Q&A Records', + 'ordering': ['-question_timestamp'], + 'indexes': [models.Index(fields=['user', '-question_timestamp'], name='ask_qarecor_user_id_f4353f_idx')], + }, + ), + ] diff --git a/hospexplorer/ask/models.py b/hospexplorer/ask/models.py index 71a8362..0763157 100644 --- a/hospexplorer/ask/models.py +++ b/hospexplorer/ask/models.py @@ -1,3 +1,31 @@ from django.db import models +from django.conf import settings -# Create your models here. + +class QARecord(models.Model): + """ + Stores a question-answer pair from user interactions with the LLM. + """ + # Question fields + question_text = models.TextField() + question_timestamp = models.DateTimeField(auto_now_add=True) + + # Answer fields + answer_text = models.TextField(blank=True,default="") + answer_raw_response = models.JSONField(default=dict) + answer_timestamp = models.DateTimeField(null=True, blank=True) + + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="qa_records") + + class Meta: + ordering = ["-question_timestamp"] + verbose_name = "Q&A Record" + verbose_name_plural = "Q&A Records" + indexes = [ + models.Index(fields=["user", "-question_timestamp"]), + ] + + def __str__(self): + truncated = self.question_text[:50] + suffix = "..." if len(self.question_text) > 50 else "" + return f"{self.user.username}: {truncated}{suffix}" diff --git a/hospexplorer/ask/views.py b/hospexplorer/ask/views.py index fe212b2..13e64f6 100644 --- a/hospexplorer/ask/views.py +++ b/hospexplorer/ask/views.py @@ -2,7 +2,9 @@ from django.http import JsonResponse from django.conf import settings from django.contrib.auth.decorators import login_required +from django.utils import timezone import ask.llm_connector +from ask.models import QARecord @login_required @@ -18,4 +20,24 @@ def mock_response(request): @login_required def query(request): - return JsonResponse(ask.llm_connector.query_llm(request.GET["query"])) \ No newline at end of file + query_text = request.GET.get("query", "") + llm_response = ask.llm_connector.query_llm(query_text) + + answer_text = "" + if "choices" in llm_response and llm_response["choices"]: + answer_text = llm_response["choices"][0].get("message", {}).get("content", "") + elif "message" in llm_response: # Fallback for mock response format + answer_text = llm_response["message"] + + QARecord.objects.create( + question_text=query_text, + answer_text=answer_text, + answer_raw_response=llm_response, + answer_timestamp=timezone.now(), + user=request.user, + ) + + print(query_text) + print(llm_response) + + return JsonResponse(llm_response) \ No newline at end of file From 5b6217fe8bec9195fd1c2fc3f93103334abb5ad5 Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Fri, 30 Jan 2026 14:20:39 -0700 Subject: [PATCH 05/15] [HOP-7] comments --- hospexplorer/ask/templates/_base.html | 2 ++ .../templates/account/email/password_reset_key_message.html | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/hospexplorer/ask/templates/_base.html b/hospexplorer/ask/templates/_base.html index f7e42e8..0489cea 100644 --- a/hospexplorer/ask/templates/_base.html +++ b/hospexplorer/ask/templates/_base.html @@ -7,7 +7,9 @@ HosP + + diff --git a/hospexplorer/ask/templates/account/email/password_reset_key_message.html b/hospexplorer/ask/templates/account/email/password_reset_key_message.html index acf0352..cab286c 100644 --- a/hospexplorer/ask/templates/account/email/password_reset_key_message.html +++ b/hospexplorer/ask/templates/account/email/password_reset_key_message.html @@ -10,14 +10,12 @@ - -
Arizona State University

Password Reset

@@ -26,7 +24,6 @@

{% blocktrans %}If you did not request this, you can safely ignore this email.{% endblocktrans %}

- -
@@ -41,7 +38,6 @@

From 878af421f5bc765982d49b0a0f4070aa32ccc48e Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Fri, 30 Jan 2026 14:56:13 -0700 Subject: [PATCH 06/15] [HOP-8] Removed debug print --- hospexplorer/ask/views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/hospexplorer/ask/views.py b/hospexplorer/ask/views.py index 13e64f6..a0622bf 100644 --- a/hospexplorer/ask/views.py +++ b/hospexplorer/ask/views.py @@ -36,8 +36,5 @@ def query(request): answer_timestamp=timezone.now(), user=request.user, ) - - print(query_text) - print(llm_response) - + return JsonResponse(llm_response) \ No newline at end of file From a808edf5a6500ca1e9dd7ef238b0c307da6cfe8b Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Wed, 4 Feb 2026 15:30:57 -0700 Subject: [PATCH 07/15] [HOP-7] Added login view to / --- hospexplorer/hospexplorer/settings.py | 6 +++--- hospexplorer/hospexplorer/urls.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/hospexplorer/hospexplorer/settings.py b/hospexplorer/hospexplorer/settings.py index 5015ad3..08d309a 100644 --- a/hospexplorer/hospexplorer/settings.py +++ b/hospexplorer/hospexplorer/settings.py @@ -151,8 +151,8 @@ # Django-Allauth Configuration LOGIN_REDIRECT_URL = "/ask/" -LOGOUT_REDIRECT_URL = "/accounts/login/" -LOGIN_URL = "/accounts/login/" +LOGOUT_REDIRECT_URL = "/" +LOGIN_URL = "/" ACCOUNT_LOGIN_METHODS = {"email", "username"} ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"] @@ -167,7 +167,7 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # Production SMTP settings -EMAIL_HOST = os.getenv("EMAIL_HOST", "smtp.gmail.com") +EMAIL_HOST = os.getenv("EMAIL_HOST", "") EMAIL_PORT = int(os.getenv("EMAIL_PORT", 587)) EMAIL_USE_TLS = True EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "") diff --git a/hospexplorer/hospexplorer/urls.py b/hospexplorer/hospexplorer/urls.py index 5f936bd..69680be 100644 --- a/hospexplorer/hospexplorer/urls.py +++ b/hospexplorer/hospexplorer/urls.py @@ -20,12 +20,14 @@ from django.urls import include, path from django.conf.urls.static import static from django.conf import settings +from allauth.account.views import LoginView urlpatterns = [ path(settings.APP_ROOT, include([ + path("", LoginView.as_view(), name="home"), path("admin/", admin.site.urls), path("accounts/", include("allauth.urls")), path("ask/", include("ask.urls")), From 77534debf16bd66a93742d4f09a1d069bc0d495e Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Wed, 4 Feb 2026 16:56:44 -0700 Subject: [PATCH 08/15] [HOP-8] missing try except block, added record before sending to llm and recording answer based on llm behavior --- .../ask/migrations/0002_qarecord_is_error.py | 18 +++++++++++++ hospexplorer/ask/models.py | 3 ++- hospexplorer/ask/views.py | 26 ++++++++++++------- 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 hospexplorer/ask/migrations/0002_qarecord_is_error.py diff --git a/hospexplorer/ask/migrations/0002_qarecord_is_error.py b/hospexplorer/ask/migrations/0002_qarecord_is_error.py new file mode 100644 index 0000000..10ddcac --- /dev/null +++ b/hospexplorer/ask/migrations/0002_qarecord_is_error.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-02-04 23:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ask', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='qarecord', + name='is_error', + field=models.BooleanField(default=False), + ), + ] diff --git a/hospexplorer/ask/models.py b/hospexplorer/ask/models.py index 0763157..aa69d52 100644 --- a/hospexplorer/ask/models.py +++ b/hospexplorer/ask/models.py @@ -11,9 +11,10 @@ class QARecord(models.Model): question_timestamp = models.DateTimeField(auto_now_add=True) # Answer fields - answer_text = models.TextField(blank=True,default="") + answer_text = models.TextField(blank=True, default="") answer_raw_response = models.JSONField(default=dict) 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") diff --git a/hospexplorer/ask/views.py b/hospexplorer/ask/views.py index 6ea42ba..4284720 100644 --- a/hospexplorer/ask/views.py +++ b/hospexplorer/ask/views.py @@ -21,6 +21,10 @@ def mock_response(request): @login_required def query(request): query_text = request.GET.get("query", "") + record = QARecord.objects.create( + question_text=query_text, + user=request.user, + ) try: llm_response = ask.llm_connector.query_llm(query_text) @@ -30,16 +34,20 @@ def query(request): elif "message" in llm_response: answer_text = llm_response["message"] - QARecord.objects.create( - question_text=query_text, - answer_text=answer_text, - answer_raw_response=llm_response, - answer_timestamp=timezone.now(), - user=request.user, - ) + record.answer_text = answer_text + record.answer_raw_response = llm_response + record.answer_timestamp = timezone.now() + record.save() return JsonResponse({"message": answer_text}) except (KeyError, IndexError, TypeError) as e: - return JsonResponse({"error": f"Unexpected response from server: {e}"}, status=500) + error_msg = f"Unexpected response from server: {e}" except Exception as e: - return JsonResponse({"error": f"Failed to connect to server: {e}"}, status=500) + error_msg = f"Failed to connect to server: {e}" + + # The try block returns on success, so this only runs on error. + record.is_error = True + record.answer_text = error_msg + record.answer_timestamp = timezone.now() + record.save() + return JsonResponse({"error": error_msg}, status=500) From 36f29c60d0810de7b23a4eb6e7f843c53f0fa82d Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Thu, 5 Feb 2026 12:43:11 -0700 Subject: [PATCH 09/15] [HOP-11] Added recent question bar --- hospexplorer/ask/static/css/styles.css | 33 ++++++++++++ hospexplorer/ask/static/js/scripts.js | 18 ++++--- hospexplorer/ask/templates/_base.html | 75 ++++++++++++++++++-------- hospexplorer/ask/templates/index.html | 10 +++- hospexplorer/ask/views.py | 8 ++- 5 files changed, 111 insertions(+), 33 deletions(-) diff --git a/hospexplorer/ask/static/css/styles.css b/hospexplorer/ask/static/css/styles.css index 07c61b4..42db981 100644 --- a/hospexplorer/ask/static/css/styles.css +++ b/hospexplorer/ask/static/css/styles.css @@ -10870,3 +10870,36 @@ body.sb-sidenav-toggled #wrapper #sidebar-wrapper { margin-left: -15rem; } } + +/* Recent Questions Sidebar Styles */ +#sidebar-wrapper { + display: flex; + flex-direction: column; +} + +#sidebar-wrapper .list-group { + flex: 1; + overflow-y: auto; + max-height: calc(100vh - 180px); +} + +#sidebar-wrapper .question-item { + font-size: 0.875rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border-left: 3px solid transparent; + transition: border-color 0.2s, background-color 0.2s; +} + +#sidebar-wrapper .question-item:hover { + border-left-color: #0d6efd; + background-color: #f8f9fa; +} + +#sidebar-wrapper .sidebar-section-heading { + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 0.75rem; +} diff --git a/hospexplorer/ask/static/js/scripts.js b/hospexplorer/ask/static/js/scripts.js index 81efcaa..92acbb1 100644 --- a/hospexplorer/ask/static/js/scripts.js +++ b/hospexplorer/ask/static/js/scripts.js @@ -10,17 +10,19 @@ window.addEventListener('DOMContentLoaded', event => { // Toggle the side navigation - const sidebarToggle = document.body.querySelector('#sidebarToggle'); - if (sidebarToggle) { - // Uncomment Below to persist sidebar toggle between refreshes - // if (localStorage.getItem('sb|sidebar-toggle') === 'true') { - // document.body.classList.toggle('sb-sidenav-toggled'); - // } - sidebarToggle.addEventListener('click', event => { + const sidebarToggles = document.body.querySelectorAll('#sidebarToggle'); + + // Restore sidebar state from localStorage + if (localStorage.getItem('sb|sidebar-toggle') === 'true') { + document.body.classList.add('sb-sidenav-toggled'); + } + + sidebarToggles.forEach(toggle => { + toggle.addEventListener('click', event => { event.preventDefault(); document.body.classList.toggle('sb-sidenav-toggled'); localStorage.setItem('sb|sidebar-toggle', document.body.classList.contains('sb-sidenav-toggled')); }); - } + }); }); diff --git a/hospexplorer/ask/templates/_base.html b/hospexplorer/ask/templates/_base.html index 0489cea..dd5766a 100644 --- a/hospexplorer/ask/templates/_base.html +++ b/hospexplorer/ask/templates/_base.html @@ -16,14 +16,30 @@

-