Spaces:
Sleeping
Sleeping
husseinelsaadi Claude Opus 4.8 commited on
Commit ·
5f99f0f
1
Parent(s): a99ecf6
Seed professional demo data; fix duplicate LUNA avatar on interview
Browse files- interview.html: reuse the placeholder avatar/bubble for the first
question instead of appending a second LUNA avatar, so only the real
question shows. Follow-up questions and talking animation unchanged.
- database.py: add idempotent seed_demo_data() (demo recruiter, demo
applicant, and 5 curated tech jobs owned by the recruiter) run on
startup, so the listing looks polished and the full demo flow works
after each ephemeral-DB restart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- backend/models/database.py +141 -9
- backend/templates/interview.html +20 -12
backend/models/database.py
CHANGED
|
@@ -112,12 +112,144 @@ def init_db(app):
|
|
| 112 |
# ``num_questions`` column.
|
| 113 |
pass
|
| 114 |
|
| 115 |
-
#
|
| 116 |
-
#
|
| 117 |
-
#
|
| 118 |
-
#
|
| 119 |
-
#
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
# ``num_questions`` column.
|
| 113 |
pass
|
| 114 |
|
| 115 |
+
# Seed professional demo data (recruiter + applicant + a curated set of
|
| 116 |
+
# jobs) so the platform always looks polished and the full demo flow
|
| 117 |
+
# works after every restart. On Hugging Face the SQLite DB lives in
|
| 118 |
+
# the ephemeral /tmp directory and is wiped on each restart, so this
|
| 119 |
+
# idempotent seeding repopulates a clean, consistent dataset every time.
|
| 120 |
+
seed_demo_data()
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# Known demo accounts used for the marketing demo. Passwords are intentionally
|
| 124 |
+
# simple here because these are throwaway demo accounts on an ephemeral DB.
|
| 125 |
+
DEMO_RECRUITER_EMAIL = 'hr@codingo.ai'
|
| 126 |
+
DEMO_APPLICANT_EMAIL = 'candidate@codingo.ai'
|
| 127 |
+
DEMO_PASSWORD = 'codingo123'
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def seed_demo_data():
|
| 131 |
+
"""Idempotently seed a demo recruiter, a demo applicant and a curated set
|
| 132 |
+
of professional job listings.
|
| 133 |
+
|
| 134 |
+
Safe to call on every startup: each entity is only created when missing,
|
| 135 |
+
and jobs are only inserted when the ``jobs`` table is empty. Any failure
|
| 136 |
+
is rolled back and swallowed so seeding can never block app startup.
|
| 137 |
+
"""
|
| 138 |
+
try:
|
| 139 |
+
# --- Demo recruiter (owns the seeded jobs so they appear in the HR
|
| 140 |
+
# dashboard, which filters by recruiter_id) ---
|
| 141 |
+
recruiter = User.query.filter_by(email=DEMO_RECRUITER_EMAIL).first()
|
| 142 |
+
if recruiter is None:
|
| 143 |
+
recruiter = User(
|
| 144 |
+
username='Codingo HR',
|
| 145 |
+
email=DEMO_RECRUITER_EMAIL,
|
| 146 |
+
role='recruiter',
|
| 147 |
+
)
|
| 148 |
+
recruiter.set_password(DEMO_PASSWORD)
|
| 149 |
+
db.session.add(recruiter)
|
| 150 |
+
db.session.commit()
|
| 151 |
+
|
| 152 |
+
# --- Demo applicant (job-seeker role is 'unemployed') ---
|
| 153 |
+
applicant = User.query.filter_by(email=DEMO_APPLICANT_EMAIL).first()
|
| 154 |
+
if applicant is None:
|
| 155 |
+
applicant = User(
|
| 156 |
+
username='Demo Candidate',
|
| 157 |
+
email=DEMO_APPLICANT_EMAIL,
|
| 158 |
+
role='unemployed',
|
| 159 |
+
)
|
| 160 |
+
applicant.set_password(DEMO_PASSWORD)
|
| 161 |
+
db.session.add(applicant)
|
| 162 |
+
db.session.commit()
|
| 163 |
+
|
| 164 |
+
# --- Jobs: only seed when there are none, to avoid duplicates and to
|
| 165 |
+
# never clobber jobs a recruiter may have posted at runtime ---
|
| 166 |
+
if Job.query.count() == 0:
|
| 167 |
+
demo_jobs = [
|
| 168 |
+
{
|
| 169 |
+
'role': 'Data Scientist',
|
| 170 |
+
'seniority': 'Mid-level',
|
| 171 |
+
'company': 'NorthBridge Analytics',
|
| 172 |
+
'skills': ['Python', 'SQL', 'Pandas', 'scikit-learn',
|
| 173 |
+
'Machine Learning', 'Statistics', 'Data Visualization'],
|
| 174 |
+
'description': (
|
| 175 |
+
"We are looking for a Data Scientist to turn raw data into "
|
| 176 |
+
"actionable insight. You will design and evaluate machine "
|
| 177 |
+
"learning models, run statistical analyses, and build clear "
|
| 178 |
+
"visualizations that guide product and business decisions. "
|
| 179 |
+
"You will collaborate closely with engineering and product "
|
| 180 |
+
"teams to take models from prototype to production."
|
| 181 |
+
),
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
'role': 'Data Engineer',
|
| 185 |
+
'seniority': 'Senior',
|
| 186 |
+
'company': 'Cloudbyte Systems',
|
| 187 |
+
'skills': ['Python', 'SQL', 'Apache Spark', 'Airflow',
|
| 188 |
+
'ETL', 'AWS', 'Data Warehousing'],
|
| 189 |
+
'description': (
|
| 190 |
+
"Join us as a Senior Data Engineer to design, build and "
|
| 191 |
+
"maintain the data pipelines that power our analytics and "
|
| 192 |
+
"machine learning platforms. You will own scalable ETL "
|
| 193 |
+
"workflows, optimize our data warehouse, and ensure data "
|
| 194 |
+
"quality and reliability across the organization."
|
| 195 |
+
),
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
'role': 'Machine Learning Engineer',
|
| 199 |
+
'seniority': 'Mid-level',
|
| 200 |
+
'company': 'Vantix AI',
|
| 201 |
+
'skills': ['Python', 'TensorFlow', 'PyTorch', 'Machine Learning',
|
| 202 |
+
'MLOps', 'Model Deployment', 'Docker'],
|
| 203 |
+
'description': (
|
| 204 |
+
"As a Machine Learning Engineer you will take models from "
|
| 205 |
+
"research into reliable, production-grade services. You will "
|
| 206 |
+
"train and fine-tune models, build robust deployment and "
|
| 207 |
+
"monitoring pipelines, and work with data scientists to "
|
| 208 |
+
"ship features that delight our users."
|
| 209 |
+
),
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
'role': 'NLP Engineer',
|
| 213 |
+
'seniority': 'Senior',
|
| 214 |
+
'company': 'Lingua Labs',
|
| 215 |
+
'skills': ['Python', 'NLP', 'Transformers', 'spaCy',
|
| 216 |
+
'PyTorch', 'LLMs', 'Hugging Face'],
|
| 217 |
+
'description': (
|
| 218 |
+
"We are hiring a Senior NLP Engineer to build state-of-the-art "
|
| 219 |
+
"language understanding systems. You will develop models for "
|
| 220 |
+
"text classification, information extraction and conversational "
|
| 221 |
+
"AI, fine-tune large language models, and deploy them at scale "
|
| 222 |
+
"to power our products."
|
| 223 |
+
),
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
'role': 'Computer Vision Engineer',
|
| 227 |
+
'seniority': 'Mid-level',
|
| 228 |
+
'company': 'VisionWorks AI',
|
| 229 |
+
'skills': ['Python', 'OpenCV', 'Deep Learning', 'PyTorch',
|
| 230 |
+
'Image Processing', 'CNNs', 'Model Optimization'],
|
| 231 |
+
'description': (
|
| 232 |
+
"Join our team as a Computer Vision Engineer to develop "
|
| 233 |
+
"image and video understanding systems. You will design and "
|
| 234 |
+
"train deep learning models for detection, segmentation and "
|
| 235 |
+
"recognition, optimize them for real-time performance, and "
|
| 236 |
+
"integrate them into our production pipeline."
|
| 237 |
+
),
|
| 238 |
+
},
|
| 239 |
+
]
|
| 240 |
+
|
| 241 |
+
for spec in demo_jobs:
|
| 242 |
+
db.session.add(Job(
|
| 243 |
+
role=spec['role'],
|
| 244 |
+
description=spec['description'],
|
| 245 |
+
seniority=spec['seniority'],
|
| 246 |
+
skills=json.dumps(spec['skills']),
|
| 247 |
+
company=spec['company'],
|
| 248 |
+
recruiter_id=recruiter.id,
|
| 249 |
+
num_questions=4,
|
| 250 |
+
))
|
| 251 |
+
db.session.commit()
|
| 252 |
+
except Exception as exc:
|
| 253 |
+
# Never let seeding break startup.
|
| 254 |
+
db.session.rollback()
|
| 255 |
+
print(f"Demo data seeding skipped due to error: {exc}")
|
backend/templates/interview.html
CHANGED
|
@@ -698,22 +698,30 @@
|
|
| 698 |
}
|
| 699 |
|
| 700 |
displayQuestion(question, audioUrl = null) {
|
| 701 |
-
// Remove loading message
|
| 702 |
const loadingMsg = document.getElementById('loadingMessage');
|
| 703 |
if (loadingMsg) {
|
|
|
|
|
|
|
|
|
|
| 704 |
loadingMsg.remove();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
}
|
| 706 |
-
|
| 707 |
-
// Create question message
|
| 708 |
-
const messageDiv = document.createElement('div');
|
| 709 |
-
messageDiv.className = 'ai-message';
|
| 710 |
-
messageDiv.innerHTML = `
|
| 711 |
-
<div class="ai-avatar">AI</div>
|
| 712 |
-
<div class="message-bubble">
|
| 713 |
-
<p>${question}</p>
|
| 714 |
-
</div>
|
| 715 |
-
`;
|
| 716 |
-
this.chatArea.appendChild(messageDiv);
|
| 717 |
this.chatArea.scrollTop = this.chatArea.scrollHeight;
|
| 718 |
|
| 719 |
// Update question counter
|
|
|
|
| 698 |
}
|
| 699 |
|
| 700 |
displayQuestion(question, audioUrl = null) {
|
|
|
|
| 701 |
const loadingMsg = document.getElementById('loadingMessage');
|
| 702 |
if (loadingMsg) {
|
| 703 |
+
// First question: reuse the existing avatar + bubble placeholder
|
| 704 |
+
// so only ONE LUNA avatar shows. Swap the spinner for the text.
|
| 705 |
+
const bubble = loadingMsg.parentElement; // .message-bubble
|
| 706 |
loadingMsg.remove();
|
| 707 |
+
const p = document.createElement('p');
|
| 708 |
+
p.textContent = question;
|
| 709 |
+
bubble.appendChild(p);
|
| 710 |
+
} else {
|
| 711 |
+
// Follow-up questions: append a new AI message bubble.
|
| 712 |
+
const messageDiv = document.createElement('div');
|
| 713 |
+
messageDiv.className = 'ai-message';
|
| 714 |
+
const avatar = document.createElement('div');
|
| 715 |
+
avatar.className = 'ai-avatar';
|
| 716 |
+
const bubble = document.createElement('div');
|
| 717 |
+
bubble.className = 'message-bubble';
|
| 718 |
+
const p = document.createElement('p');
|
| 719 |
+
p.textContent = question;
|
| 720 |
+
bubble.appendChild(p);
|
| 721 |
+
messageDiv.appendChild(avatar);
|
| 722 |
+
messageDiv.appendChild(bubble);
|
| 723 |
+
this.chatArea.appendChild(messageDiv);
|
| 724 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
this.chatArea.scrollTop = this.chatArea.scrollHeight;
|
| 726 |
|
| 727 |
// Update question counter
|