add ai interview questions

This commit is contained in:
ismail 2025-12-16 14:46:38 +03:00
parent 444eedc208
commit 85f895c891
13 changed files with 298 additions and 200 deletions

View File

@ -33,12 +33,12 @@ urlpatterns = [
path('api/v1/templates/save/', views.save_form_template, name='save_form_template'),
path('api/v1/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'),
path('api/v1/templates/<slug:template_slug>/delete/', views.delete_form_template, name='delete_form_template'),
path('api/v1/webhooks/zoom/', views.zoom_webhook_view, name='zoom_webhook_view'),
path('api/v1/sync/task/<str:task_id>/status/', views.sync_task_status, name='sync_task_status'),
path('api/v1/sync/history/', views.sync_history, name='sync_history'),
path('api/v1/sync/history/<slug:job_slug>/', views.sync_history, name='sync_history_job'),
path('api/v1/webhooks/zoom/', views.zoom_webhook_view, name='zoom_webhook_view'),
]
urlpatterns += i18n_patterns(

View File

@ -27,4 +27,5 @@ admin.site.register(IntegrationLog)
admin.site.register(HiringAgency)
admin.site.register(JobPosting)
admin.site.register(Settings)
admin.site.register(FormSubmission)
admin.site.register(FormSubmission)
# admin.site.register(InterviewQuestion)

View File

@ -0,0 +1,14 @@
# Generated by Django 6.0 on 2025-12-16 08:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0004_interviewquestion'),
('recruitment', '0004_settings_name'),
]
operations = [
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-16 09:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0005_merge_0004_interviewquestion_0004_settings_name'),
]
operations = [
migrations.AddField(
model_name='interview',
name='join_url',
field=models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-16 10:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0006_interview_join_url'),
]
operations = [
migrations.AlterField(
model_name='interview',
name='status',
field=models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('updated', 'Updated'), ('ended', 'Ended'), ('deleted', 'Deleted')], db_index=True, default='waiting', max_length=20),
),
]

View File

@ -0,0 +1,48 @@
# Generated by Django 6.0 on 2025-12-16 10:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0007_alter_interview_status'),
]
operations = [
migrations.RemoveIndex(
model_name='interviewquestion',
name='recruitment_schedul_b09a70_idx',
),
migrations.AddField(
model_name='interviewquestion',
name='data',
field=models.JSONField(blank=True, default=1, verbose_name='Question Data'),
preserve_default=False,
),
migrations.AlterField(
model_name='interview',
name='status',
field=models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('updated', 'Updated'), ('deleted', 'Deleted'), ('ended', 'Ended')], db_index=True, default='waiting', max_length=20),
),
migrations.AddIndex(
model_name='interviewquestion',
index=models.Index(fields=['schedule'], name='recruitment_schedul_dbb350_idx'),
),
migrations.RemoveField(
model_name='interviewquestion',
name='category',
),
migrations.RemoveField(
model_name='interviewquestion',
name='difficulty_level',
),
migrations.RemoveField(
model_name='interviewquestion',
name='question_text',
),
migrations.RemoveField(
model_name='interviewquestion',
name='question_type',
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 6.0 on 2025-12-16 10:48
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0008_remove_interviewquestion_recruitment_schedul_b09a70_idx_and_more'),
]
operations = [
migrations.RemoveIndex(
model_name='interviewquestion',
name='recruitment_schedul_dbb350_idx',
),
migrations.AddField(
model_name='scheduledinterview',
name='interview_questions',
field=models.JSONField(blank=True, default={}, verbose_name='Question Data'),
preserve_default=False,
),
migrations.DeleteModel(
name='InterviewQuestion',
),
]

View File

@ -1120,8 +1120,9 @@ class Interview(Base):
class Status(models.TextChoices):
WAITING = "waiting", _("Waiting")
STARTED = "started", _("Started")
UPDATED = "updated", _("Updated")
DELETED = "deleted", _("Deleted")
ENDED = "ended", _("Ended")
CANCELLED = "cancelled", _("Cancelled")
class InterviewResult(models.TextChoices):
PASSED="passed",_("Passed")
@ -1154,7 +1155,7 @@ class Interview(Base):
blank=True,
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'"),
)
details_url = models.URLField(
join_url = models.URLField(
verbose_name=_("Meeting/Location URL"), max_length=2048, blank=True, null=True
)
timezone = models.CharField(
@ -1351,6 +1352,10 @@ class ScheduledInterview(Base):
choices=InterviewStatus.choices,
default=InterviewStatus.SCHEDULED,
)
interview_questions = models.JSONField(
verbose_name=_("Question Data"),
blank=True
)
def __str__(self):
return (
@ -2591,58 +2596,6 @@ class Document(Base):
return ""
class InterviewQuestion(Base):
"""Model to store AI-generated interview questions"""
class QuestionType(models.TextChoices):
TECHNICAL = "technical", _("Technical")
BEHAVIORAL = "behavioral", _("Behavioral")
SITUATIONAL = "situational", _("Situational")
schedule = models.ForeignKey(
'ScheduledInterview',
on_delete=models.CASCADE,
related_name="ai_questions",
verbose_name=_("Interview Schedule")
)
question_text = models.TextField(
verbose_name=_("Question Text")
)
question_type = models.CharField(
max_length=20,
choices=QuestionType.choices,
default=QuestionType.TECHNICAL,
verbose_name=_("Question Type")
)
difficulty_level = models.CharField(
max_length=20,
choices=[
("easy", _("Easy")),
("medium", _("Medium")),
("hard", _("Hard")),
],
default="medium",
verbose_name=_("Difficulty Level")
)
category = models.CharField(
max_length=100,
blank=True,
verbose_name=_("Category")
)
class Meta:
verbose_name = _("Interview Question")
verbose_name_plural = _("Interview Questions")
ordering = ["created_at"]
indexes = [
models.Index(fields=["schedule", "question_type"]),
]
def __str__(self):
return f"{self.get_question_type_display()} Question for {self.schedule}"
class Settings(Base):
"""Model to store key-value pair settings"""
name = models.CharField(
@ -2662,7 +2615,7 @@ class Settings(Base):
verbose_name=_("Setting Value"),
help_text=_("Value for the setting"),
)
class Meta:
verbose_name = _("Setting")

View File

@ -732,7 +732,7 @@ def create_interview_and_meeting(schedule_id):
if result["status"] == "success":
interview.meeting_id = result["meeting_details"]["meeting_id"]
interview.details_url = result["meeting_details"]["join_url"]
interview.join_url = result["meeting_details"]["join_url"]
interview.host_email = result["meeting_details"]["host_email"]
interview.password = result["meeting_details"]["password"]
interview.zoom_gateway_response = result["zoom_gateway_response"]
@ -757,32 +757,20 @@ def handle_zoom_webhook_event(payload):
event_type = payload.get("event")
object_data = payload["payload"]["object"]
# Zoom often uses a long 'id' for the scheduled meeting and sometimes a 'uuid'.
# We rely on the unique 'id' that maps to your ZoomMeeting.meeting_id field.
meeting_id_zoom = str(object_data.get("id"))
if not meeting_id_zoom:
meeting_id = str(object_data.get("id"))
if not meeting_id:
logger.warning(f"Webhook received without a valid Meeting ID: {event_type}")
return False
try:
# Use filter().first() to avoid exceptions if the meeting doesn't exist yet,
# and to simplify the logic flow.
meeting_instance = "" # TODO:update #ZoomMeetingDetails.objects.filter(meeting_id=meeting_id_zoom).first()
print(meeting_instance)
# --- 1. Creation and Update Events ---
meeting_instance = Interview.objects.filter(meeting_id=meeting_id).first()
if event_type == "meeting.updated":
logger.info(f"Zoom meeting updated: {meeting_id}")
if meeting_instance:
# Update key fields from the webhook payload
meeting_instance.topic = object_data.get(
"topic", meeting_instance.topic
)
# Check for and update status and time details
# if event_type == 'meeting.created':
# meeting_instance.status = 'scheduled'
# elif event_type == 'meeting.updated':
# Only update time fields if they are in the payload
print(object_data)
meeting_instance.start_time = object_data.get(
"start_time", meeting_instance.start_time
)
@ -792,7 +780,6 @@ def handle_zoom_webhook_event(payload):
meeting_instance.timezone = object_data.get(
"timezone", meeting_instance.timezone
)
meeting_instance.status = object_data.get(
"status", meeting_instance.status
)
@ -807,31 +794,19 @@ def handle_zoom_webhook_event(payload):
]
)
# --- 2. Status Change Events (Start/End) ---
elif event_type == "meeting.started":
if meeting_instance:
meeting_instance.status = "started"
meeting_instance.save(update_fields=["status"])
elif event_type == "meeting.ended":
if meeting_instance:
meeting_instance.status = "ended"
meeting_instance.save(update_fields=["status"])
# --- 3. Deletion Event (User Action) ---
elif event_type == "meeting.deleted":
elif event_type in ["meeting.started","meeting.ended","meeting.deleted"]:
if meeting_instance:
try:
meeting_instance.status = "cancelled"
meeting_instance.status = event_type.split(".")[-1]
meeting_instance.save(update_fields=["status"])
except Exception as e:
logger.error(f"Failed to mark Zoom meeting as cancelled: {e}")
return True
except Exception as e:
logger.error(
f"Failed to process Zoom webhook for {event_type} (ID: {meeting_id_zoom}): {e}",
f"Failed to process Zoom webhook for {event_type} (ID: {meeting_id}): {e}",
exc_info=True,
)
return False
@ -1578,7 +1553,7 @@ def generate_interview_questions(schedule_id: int) -> dict:
Returns:
dict: Result containing status and generated questions or error message
"""
from .models import ScheduledInterview, InterviewQuestion
from .models import ScheduledInterview
try:
# Get the scheduled interview with related data
@ -1619,7 +1594,7 @@ def generate_interview_questions(schedule_id: int) -> dict:
{candidate_resume_text}
TASK:
Generate 8-10 interview questions that are:
Generate 8-10 interview questions in english and arabic that are:
1. Technical questions related to the job requirements
2. Behavioral questions to assess soft skills and cultural fit
3. Situational questions to evaluate problem-solving abilities
@ -1634,17 +1609,27 @@ def generate_interview_questions(schedule_id: int) -> dict:
OUTPUT FORMAT:
Return a JSON object with the following structure:
{{
"questions": [
"questions": {{
"en":[
{{
"question_text": "The actual question text",
"question_type": "technical|behavioral|situational",
"difficulty_level": "easy|medium|hard",
"category": "Category name"
}}
]
],
"ar":[
{{
"question_text": "The actual question text",
"question_type": "technical|behavioral|situational",
"difficulty_level": "easy|medium|hard",
"category": "Category name"
}}
]}}
}}
Make questions specific to the job requirements and candidate background. Avoid generic questions.
Output only valid JSON no markdown, no extra text.
"""
# Call AI handler
@ -1664,33 +1649,14 @@ def generate_interview_questions(schedule_id: int) -> dict:
if not questions:
return {"status": "error", "message": "No questions generated"}
# Clear existing questions for this schedule
InterviewQuestion.objects.filter(schedule=schedule).delete()
schedule.interview_questions.update(questions)
schedule.save(update_fields=["interview_questions"])
# Save generated questions to database
created_questions = []
for q_data in questions:
question = InterviewQuestion.objects.create(
schedule=schedule,
question_text=q_data.get("question_text", ""),
question_type=q_data.get("question_type", "technical"),
difficulty_level=q_data.get("difficulty_level", "medium"),
category=q_data.get("category", "General")
)
created_questions.append({
"id": question.id,
"text": question.question_text,
"type": question.question_type,
"difficulty": question.difficulty_level,
"category": question.category
})
logger.info(f"Successfully generated {len(created_questions)} questions for schedule {schedule_id}")
logger.info(f"Successfully generated questions for schedule {schedule_id}")
return {
"status": "success",
"questions": created_questions,
"message": f"Generated {len(created_questions)} interview questions"
"message": f"Generated interview questions"
}
except ScheduledInterview.DoesNotExist:

View File

@ -870,7 +870,7 @@ def update_meeting(instance, updated_data):
instance.topic = zoom_details.get("topic", instance.topic)
instance.duration = zoom_details.get("duration", instance.duration)
instance.details_url = zoom_details.get("join_url", instance.details_url)
instance.join_url = zoom_details.get("join_url", instance.join_url)
instance.password = zoom_details.get("password", instance.password)
instance.status = zoom_details.get("status")

View File

@ -1605,7 +1605,6 @@ def _handle_preview_submission(request, slug, job):
"""
SESSION_DATA_KEY = "interview_schedule_data"
form = BulkInterviewTemplateForm(slug, request.POST)
# break_formset = BreakTimeFormSet(request.POST,prefix='breaktime')
if form.is_valid():
# Get the form data
@ -1622,9 +1621,6 @@ def _handle_preview_submission(request, slug, job):
schedule_interview_type = form.cleaned_data["schedule_interview_type"]
physical_address = form.cleaned_data["physical_address"]
# Create a temporary schedule object (not saved to DB)
# if start_date == datetime.now().date():
# start_time = (datetime.now() + timedelta(minutes=30)).time()
temp_schedule = BulkInterviewTemplate(
job=job,
start_date=start_date,
@ -2110,7 +2106,6 @@ def reschedule_meeting_for_application(request, slug):
if request.method == "POST":
if interview.location_type == "Remote":
form = ScheduledInterviewForm(request.POST)
else:
form = OnsiteScheduleInterviewUpdateForm(request.POST)
@ -2123,7 +2118,7 @@ def reschedule_meeting_for_application(request, slug):
if interview.location_type == "Remote":
updated_data = {
"topic": topic,
"start_time": start_time.isoformat() + "Z",
"start_time": start_time.strftime("%Y-%m-%dT%H:%M:%S"),
"duration": duration,
}
result = update_meeting(schedule.interview, updated_data)
@ -2504,13 +2499,15 @@ def account_toggle_status(request, pk):
@csrf_exempt
def zoom_webhook_view(request):
from .utils import get_setting
api_key = request.headers.get("X-Zoom-API-KEY")
if api_key != settings.ZOOM_WEBHOOK_API_KEY:
if api_key != get_setting("ZOOM_WEBHOOK_API_KEY"):
return HttpResponse(status=405)
if request.method == "POST":
try:
payload = json.loads(request.body)
logger.info(payload)
async_task("recruitment.tasks.handle_zoom_webhook_event", payload)
return HttpResponse(status=200)
except Exception:
@ -4775,7 +4772,6 @@ def interview_list(request):
def generate_ai_questions(request, slug):
"""Generate AI-powered interview questions for a scheduled interview"""
from django_q.tasks import async_task
from .models import InterviewQuestion
schedule = get_object_or_404(ScheduledInterview, slug=slug)
@ -4783,40 +4779,41 @@ def generate_ai_questions(request, slug):
# Queue the AI question generation task
task_id = async_task(
"recruitment.tasks.generate_interview_questions",
schedule.id
schedule.id,
sync=True
)
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({
"status": "success",
"message": "AI question generation started in background",
"task_id": task_id
})
else:
messages.success(
request,
"AI question generation started. Questions will appear shortly."
)
return redirect("interview_detail", slug=slug)
# if request.headers.get("X-Requested-With") == "XMLHttpRequest":
# return JsonResponse({
# "status": "success",
# "message": "AI question generation started in background",
# "task_id": task_id
# })
# else:
# messages.success(
# request,
# "AI question generation started. Questions will appear shortly."
# )
# return redirect("interview_detail", slug=slug)
# For GET requests, return existing questions if any
questions = schedule.ai_questions.all().order_by("created_at")
# # For GET requests, return existing questions if any
# questions = schedule.ai_questions.all().order_by("created_at")
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({
"status": "success",
"questions": [
{
"id": q.id,
"text": q.question_text,
"type": q.question_type,
"difficulty": q.difficulty_level,
"category": q.category,
"created_at": q.created_at.isoformat()
}
for q in questions
]
})
# if request.headers.get("X-Requested-With") == "XMLHttpRequest":
# return JsonResponse({
# "status": "success",
# "questions": [
# {
# "id": q.id,
# "text": q.question_text,
# "type": q.question_type,
# "difficulty": q.difficulty_level,
# "category": q.category,
# "created_at": q.created_at.isoformat()
# }
# for q in questions
# ]
# })
return redirect("interview_detail", slug=slug)
@ -4829,24 +4826,20 @@ def interview_detail(request, slug):
ScheduledInterviewUpdateStatusForm,
OnsiteScheduleInterviewUpdateForm,
)
schedule = get_object_or_404(ScheduledInterview, slug=slug)
interview = schedule.interview
interview_result_form=InterviewResultForm(instance=interview)
application = schedule.application
job = schedule.job
print(interview.location_type)
if interview.location_type == "Remote":
reschedule_form = ScheduledInterviewForm()
else:
reschedule_form = OnsiteScheduleInterviewUpdateForm()
reschedule_form.initial["physical_address"] = interview.physical_address
reschedule_form.initial["room_number"] = interview.room_number
reschedule_form.initial["topic"] = interview.topic
reschedule_form.initial["start_time"] = interview.start_time
reschedule_form.initial["duration"] = interview.duration
reschedule_form.initial["topic"] = interview.topic
reschedule_form.initial["start_time"] = interview.start_time
reschedule_form.initial["duration"] = interview.duration
meeting = interview
interview_email_form = InterviewEmailForm(job, application, schedule)
@ -6601,7 +6594,10 @@ def compose_application_email(request, slug):
if not email_addresses:
messages.error(request, "No email selected")
referer = request.META.get("HTTP_REFERER")
if "HX-Request" in request.headers:
response = HttpResponse()
response.headers["HX-Refresh"] = "true"
return response
if referer:
# Redirect back to the referring page
return redirect(referer)
@ -6628,21 +6624,21 @@ def compose_application_email(request, slug):
},
)
if "HX-Request" in request.headers:
response = HttpResponse()
response.headers["HX-Refresh"] = "true"
return response
return redirect(request.path)
else:
# Form validation errors
messages.error(request, "Please correct the errors below.")
# For HTMX requests, return error response
if "HX-Request" in request.headers:
return JsonResponse(
{
"success": False,
"error": "Please correct the form errors and try again.",
}
)
response = HttpResponse()
response.headers["HX-Refresh"] = "true"
return response
return render(
request,

View File

@ -459,7 +459,7 @@
});
}
//form_loader();
form_loader();
try{
document.body.addEventListener('htmx:afterRequest', function(evt) {

View File

@ -305,11 +305,12 @@
text-align: center;
padding: 2rem;
}
.htmx-indicator {
display: none;
.htmx-indicator {
opacity: 0;
transition: opacity 200ms ease-in;
}
.htmx-indicator.htmx-request {
display: block;
.htmx-request .htmx-indicator {
opacity: 1;
}
/* Responsive adjustments */
@ -459,7 +460,7 @@
<span class="detail-label">{% trans "Status:" %}</span>
<span class="detail-value">
<span class="badge bg-primary-theme">
{{ schedule.status }}</span>
{{ interview.status }}</span>
</span>
</div>
</div>
@ -481,9 +482,9 @@
<span class="detail-label">{% trans "Password:" %}</span>
<span class="detail-value">{{ interview.password }}</span>
</div>
{% if interview.details_url %}
{% if interview.join_url %}
<div class="mt-3">
<a href="{{ interview.zoommeetingdetails.details_url }}"
<a href="{{ interview.join_url }}"
target="_blank"
class="btn btn-main-action btn-sm w-100">
<i class="fas fa-video me-1"></i> {% trans "Join Meeting" %}
@ -517,28 +518,84 @@
<h5 class="mb-0" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-brain me-2"></i> {% trans "AI Generated Questions" %}
</h5>
<div class="d-flex gap-2">
<button type="button"
class="btn btn-main-action btn-sm"
id="generateQuestionsBtn"
hx-post="{% url 'generate_ai_questions' schedule.slug %}"
hx-target="#aiQuestionsContainer"
hx-indicator="#generateQuestionsSpinner">
<i class="fas fa-magic me-1"></i> {% trans "Generate Questions" %}
</button>
<button type="button"
class="btn btn-outline-secondary btn-sm"
id="refreshQuestionsBtn"
hx-get="{% url 'generate_ai_questions' schedule.slug %}"
hx-target="#aiQuestionsContainer"
hx-indicator="#refreshQuestionsSpinner">
<i class="fas fa-sync-alt me-1"></i> {% trans "Refresh" %}
</button>
<div class="d-flex gap-2">
<form action="{% url 'generate_ai_questions' schedule.slug %}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-main-action btn-sm">
<span id="button-text-content">
<i class="fas fa-magic me-1"></i> {% trans "Generate Interview Questions" %}
</span>
</button>
</form>
</div>
</div>
<div class="accordion" id="aiQuestionsAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="aiQuestionsHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#aiQuestionsCollapse" aria-expanded="false" aria-controls="aiQuestionsCollapse">
<span class="accordion-icon"></span>
<span class="accordion-header-text">{% trans "Interview Questions" %}</span>
</button>
</h2>
<div id="aiQuestionsCollapse" class="accordion-collapse collapse show" aria-labelledby="aiQuestionsHeading">
<div class="accordion-body table-view">
<div class="table-responsive d-none d-lg-block">
<table class="table table-hover align-middle mb-0 ">
<thead>
<tr>
<th scope="col">{% trans "Question" %}</th>
<th scope="col">{% trans "Type" %}</th>
<th scope="col">{% trans "Difficulty" %}</th>
<th scope="col">{% trans "Category" %}</th>
</tr>
</thead>
<tbody>
{% if LANGUAGE_CODE == "ar" %}
{% for question in schedule.interview_questions.ar %}
<tr>
<td class="text-break">
<span class="d-block" style="font-size: 0.8rem; color: #757575">{{ question.question_text }}</span>
</td>
<td class="text-center">
<span class="badge rounded-pill bg-primary-theme">{{ question.question_type|capfirst }}</span>
</td>
<td class="text-center">
<span class="badge rounded-pill bg-primary-theme">{{ question.difficulty_level|capfirst }}</span>
</td>
<td class="text-center">
<span class="badge rounded-pill bg-primary-theme">{{ question.category|capfirst }}</span>
</td>
</tr>
{% endfor %}
{% else %}
{% for question in schedule.interview_questions.en %}
<tr>
<td class="text-break">
<span class="d-block" style="font-size: 0.8rem; color: #757575">{{ question.question_text }}</span>
</td>
<td class="text-center">
<span class="badge rounded-pill bg-primary-theme">{{ question.question_type|capfirst }}</span>
</td>
<td class="text-center">
<span class="badge rounded-pill bg-primary-theme">{{ question.difficulty_level|capfirst }}</span>
</td>
<td class="text-center">
<span class="badge rounded-pill bg-primary-theme">{{ question.category|capfirst }}</span>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Spinners -->
<div class="text-center py-3" id="generateQuestionsSpinner" class="htmx-indicator d-none">
{% comment %} <div class="text-center py-3" id="generateQuestionsSpinner" class="htmx-indicator d-none">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{% trans "Generating questions..." %}</span>
</div>
@ -558,7 +615,7 @@
<i class="fas fa-brain fa-2x mb-3"></i>
<p class="mb-0">{% trans "No AI questions generated yet. Click 'Generate Questions' to create personalized interview questions based on the candidate's profile and job requirements." %}</p>
</div>
</div>
</div> {% endcomment %}
</div>
<div class="kaauh-card shadow-sm p-4">
@ -682,13 +739,13 @@
<div class="action-buttons">
{% if schedule.status != 'cancelled' and schedule.status != 'completed' %}
<button type="button" class="btn btn-main-action btn-sm"
<button type="button" class="btn btn-main-action"
data-bs-toggle="modal"
data-bs-target="#rescheduleModal">
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
</button>
<button type="button" class="btn btn-outline-danger btn-sm"
<button type="button" class="btn btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#cancelModal">
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}