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 CHANGED
@@ -112,12 +112,144 @@ def init_db(app):
112
  # ``num_questions`` column.
113
  pass
114
 
115
- # Database tables are created on application start. We intentionally do not
116
- # seed any sample data here. Previously a block of code inserted dummy
117
- # job listings whenever the jobs table was empty. In production, jobs
118
- # should only be added by authenticated recruiters via the job posting
119
- # interface. Leaving the seeding logic in place would result in fake
120
- # positions appearing every time the application starts, which is
121
- # undesirable for a live recruitment platform. If your environment
122
- # requires initial data for testing, insert it manually via the
123
- # database or through the new recruiter job posting page.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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