Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions public/static/css/generation_progress.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
.generation-progress-overlay {
position: fixed;
inset: 0;
z-index: 1100;
display: none;
place-items: center;
padding: 20px;
background: rgba(11, 31, 51, 0.48);
backdrop-filter: blur(3px);
}

.generation-progress-overlay.active {
display: grid;
}

.generation-progress-card {
width: min(440px, 100%);
padding: 28px;
color: #172b3a;
text-align: center;
background: #fff;
border: 1px solid #dce5eb;
border-radius: 18px;
box-shadow: 0 24px 70px rgba(16, 42, 67, 0.24);
}

.generation-progress-icon {
display: inline-grid;
width: 48px;
height: 48px;
margin-bottom: 14px;
color: #0f766e;
background: #e7f6f3;
border-radius: 50%;
place-items: center;
font-size: 1.35rem;
}

.generation-progress-card h2 {
margin-bottom: 8px;
color: #102a43;
font-size: 1.25rem;
}

.generation-progress-status {
min-height: 24px;
margin-bottom: 18px;
color: #526777;
}

.generation-progress-card .progress {
height: 10px;
overflow: hidden;
background: #e5edf1;
border-radius: 999px;
}

.generation-progress-card .progress-bar {
width: 8%;
background: linear-gradient(90deg, #0f766e, #0d9488);
transition: width 0.55s ease;
}

.generation-progress-percent {
display: block;
margin-top: 9px;
color: #607486;
font-size: 0.82rem;
}

@media (max-width: 576px) {
.generation-progress-card {
padding: 22px 18px;
border-radius: 15px;
}
}

@media (prefers-reduced-motion: reduce) {
.generation-progress-card .progress-bar {
transition: none;
}
}
100 changes: 100 additions & 0 deletions public/static/js/generation_progress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
(function () {
const STAGES = {
model: [
[10, 'Checking your study design…'],
[34, 'Matching compatible statistical models…'],
[62, 'Comparing the strongest candidates…'],
[84, 'Preparing your recommendation…'],
[92, 'Almost ready…']
],
modelAi: [
[10, 'Checking your study design…'],
[32, 'Matching compatible statistical models…'],
[58, 'Reviewing the verified shortlist with AI…'],
[82, 'Preparing your recommendation…'],
[92, 'Almost ready…']
],
questionnaire: [
[10, 'Analyzing your research goals…'],
[36, 'Building questionnaire sections…'],
[68, 'Organizing question types…'],
[86, 'Preparing your questionnaire…'],
[92, 'Almost ready…']
],
questionnaireAi: [
[10, 'Analyzing your research goals…'],
[34, 'Building questionnaire sections…'],
[60, 'Generating focused AI questions…'],
[84, 'Preparing your questionnaire…'],
[92, 'Almost ready…']
]
};

function resetProgress(form, overlay) {
overlay.classList.remove('active');
overlay.setAttribute('aria-hidden', 'true');
const submitButton = form.querySelector('button[type="submit"]');
if (submitButton) {
submitButton.disabled = false;
}
}

function startProgress(form, overlay) {
const aiFieldId = form.dataset.progressAiField;
const aiField = aiFieldId ? document.getElementById(aiFieldId) : null;
const aiEnabled = Boolean(aiField && aiField.checked);
const mode = form.dataset.progressMode || 'model';
const stages = STAGES[`${mode}${aiEnabled ? 'Ai' : ''}`] || STAGES[mode];
const bar = overlay.querySelector('.progress-bar');
const status = overlay.querySelector('.generation-progress-status');
const percent = overlay.querySelector('.generation-progress-percent');
const submitButton = form.querySelector('button[type="submit"]');
let stageIndex = 0;

overlay.classList.add('active');
overlay.setAttribute('aria-hidden', 'false');
if (submitButton) {
submitButton.disabled = true;
}

function showStage() {
const stage = stages[Math.min(stageIndex, stages.length - 1)];
bar.style.width = `${stage[0]}%`;
bar.setAttribute('aria-valuenow', String(stage[0]));
status.textContent = stage[1];
percent.textContent = `${stage[0]}%`;
if (stageIndex < stages.length - 1) {
stageIndex += 1;
}
}

showStage();
window.setInterval(showStage, 1800);
}

document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-generation-progress]').forEach(function (form) {
const overlay = document.getElementById(form.dataset.progressTarget);
if (!overlay) {
return;
}
resetProgress(form, overlay);
form.addEventListener('submit', function (event) {
window.queueMicrotask(function () {
if (!event.defaultPrevented) {
startProgress(form, overlay);
}
});
});
});
});

window.addEventListener('pageshow', function () {
document.querySelectorAll('[data-generation-progress]').forEach(function (form) {
const overlay = document.getElementById(form.dataset.progressTarget);
if (overlay) {
resetProgress(form, overlay);
}
});
});
}());
102 changes: 72 additions & 30 deletions routes/questionnaire_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,29 @@
This module provides routes for the questionnaire design service,
allowing users to create, preview, and edit questionnaires.
"""
from flask import Blueprint, render_template, request, redirect, url_for, session, flash, send_file
import hashlib
import logging
from datetime import datetime, timezone

from flask import (
Blueprint,
current_app,
flash,
redirect,
render_template,
request,
send_file,
session,
url_for,
)
from flask_login import login_required, current_user
from sqlalchemy.exc import SQLAlchemyError
from utils.questionnaire_generator import generate_questionnaire

from models import db, Questionnaire
from utils.ai_service import is_ai_enabled
from utils.ai_usage import consume_user_ai_quota
from utils.export_utils import export_to_word
from models import db, Questionnaire
from datetime import datetime, timezone
import logging
from utils.questionnaire_generator import generate_questionnaire

# Try to import PDF export functionality
try:
Expand All @@ -37,9 +51,19 @@ def design():
research_description = request.form.get('research_description', '')
target_audience = request.form.get('target_audience', '')
questionnaire_purpose = request.form.get('questionnaire_purpose', '')
if not all(
[
research_topic.strip(),
research_description.strip(),
target_audience.strip(),
questionnaire_purpose.strip(),
]
):
flash('Please complete all required questionnaire fields.', 'warning')
return redirect(url_for('questionnaire.design'))
# Check if AI enhancement was requested
use_ai = request.form.get('use_ai_enhancement', 'off') == 'on'
# Get the number of AI questions per type (default to 3 if not provided or not using AI)
# Get the total number of focused AI questions to add.
num_ai_questions = 3 # Default value
if use_ai:
if not current_user.is_authenticated:
Expand All @@ -51,44 +75,62 @@ def design():
num_ai_questions = max(1, min(num_ai_questions, 5))
except ValueError:
num_ai_questions = 3 # Fallback to default if conversion fails
try:
allowed, _ = consume_user_ai_quota(
current_user.id,
units=num_ai_questions,
)
except SQLAlchemyError:
db.session.rollback()
logger.exception(
"Could not record questionnaire AI usage for user %s.",
current_user.id,
)
flash(
'AI usage tracking is not initialized. Please contact the administrator.',
'danger',
)
return redirect(url_for('questionnaire.design'))
if not allowed:
flash(
'You have reached the hourly AI usage limit. Please try again later.',
'warning',
)
return redirect(url_for('questionnaire.design'))
if is_ai_enabled():
try:
allowed, _ = consume_user_ai_quota(current_user.id)
except SQLAlchemyError:
db.session.rollback()
logger.exception(
"Could not record questionnaire AI usage for user %s.",
current_user.id,
)
flash(
'AI usage tracking is not initialized. Please contact the administrator.',
'danger',
)
return redirect(url_for('questionnaire.design'))
if not allowed:
flash(
'You have reached the hourly AI usage limit. Please try again later.',
'warning',
)
return redirect(url_for('questionnaire.design'))
safety_identifier = None
if use_ai and current_user.is_authenticated:
safety_identifier = hashlib.sha256(
(
f"{current_app.config['SECRET_KEY']}:"
f"{current_user.id}"
).encode()
).hexdigest()
# Generate questionnaire based on research description
questionnaire = generate_questionnaire(
research_description,
research_topic,
target_audience,
questionnaire_purpose,
use_ai_enhancement=use_ai,
num_ai_questions=num_ai_questions
num_ai_questions=num_ai_questions,
safety_identifier=safety_identifier,
)
ai_applied = any(
question.get('ai_created') or question.get('ai_enhanced')
for section in questionnaire
for question in section.get('questions', [])
)
if use_ai and not ai_applied:
flash(
'AI enhancement was unavailable, so a complete rules-based '
'questionnaire was generated instead.',
'warning',
)
# Store questionnaire data in session
session['questionnaire'] = questionnaire
session['research_topic'] = research_topic
session['research_description'] = research_description
session['target_audience'] = target_audience
session['questionnaire_purpose'] = questionnaire_purpose
session['used_ai_enhancement'] = use_ai
session['used_ai_enhancement'] = ai_applied
return redirect(url_for('questionnaire.preview'))
return render_template('questionnaire/design.html')
@questionnaire_bp.route('/preview')
Expand Down
35 changes: 29 additions & 6 deletions templates/analysis_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

{% block container_start %}{% endblock %}

{% block extra_css %}
<style>
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/generation_progress.css', v='20260725.1') }}">
<style>
/* Reset navbar margin */
.navbar {
margin-bottom: 0 !important;
Expand Down Expand Up @@ -172,7 +173,11 @@ <h1 class="form-title">Describe your research design</h1>
</div>
</div>

<form method="POST" action="{{ url_for('main.results') }}">
<form method="POST" action="{{ url_for('main.results') }}"
data-generation-progress
data-progress-target="model-selection-progress"
data-progress-mode="model"
data-progress-ai-field="use_ai_review">
<div class="form-group">
<label for="research_question" class="form-label required-field">What are you trying to find out?</label>
<input type="text" class="form-control" id="research_question" name="research_question" required
Expand Down Expand Up @@ -372,9 +377,26 @@ <h1 class="form-title">Describe your research design</h1>
</form>
</div>
</div>
</div>

<script>
</div>

<div id="model-selection-progress" class="generation-progress-overlay"
role="status" aria-live="polite" aria-hidden="true">
<div class="generation-progress-card">
<div class="generation-progress-icon" aria-hidden="true">
<i class="bi bi-diagram-3"></i>
</div>
<h2>Selecting your statistical model</h2>
<p class="generation-progress-status">Checking your study design…</p>
<div class="progress" aria-label="Model selection progress">
<div class="progress-bar" role="progressbar" aria-valuenow="8"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
<span class="generation-progress-percent">8%</span>
<small class="text-muted d-block mt-3">We are matching your inputs to compatible methods.</small>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const independentVariablesCheckboxes = document.querySelectorAll('input[name="independent_variables"]');
Expand Down Expand Up @@ -636,5 +658,6 @@ <h1 class="form-title">Describe your research design</h1>
}
}
</script>
<script src="{{ url_for('static', filename='js/generation_progress.js', v='20260725.1') }}"></script>
{% endblock %}
{% block container_end %}{% endblock %}
Loading