Update src/streamlit_app.py

#1
by Viper51 - opened
Files changed (1) hide show
  1. src/streamlit_app.py +89 -223
src/streamlit_app.py CHANGED
@@ -4,7 +4,7 @@ try:
4
  except Exception:
5
  PdfReader = None
6
 
7
- # Optional AI SDKs - guarded imports so the app can still run without them
8
  try:
9
  import google.generativeai as genai
10
  except Exception:
@@ -19,33 +19,7 @@ except Exception:
19
 
20
  from pydantic import BaseModel, Field
21
  from typing import Optional
22
-
23
- # Optional TTS / speech libs
24
- try:
25
- from gtts import gTTS
26
- except Exception:
27
- gTTS = None
28
-
29
- try:
30
- import speech_recognition as sr
31
- except Exception:
32
- sr = None
33
-
34
  import os
35
- import io
36
- import tempfile
37
- try:
38
- from streamlit_mic_recorder import mic_recorder # Key component for browser audio
39
- except Exception as e:
40
- # --- THIS WILL SHOW US THE REAL ERROR ---
41
- st.error(f"❌ FAILED TO IMPORT MIC RECORDER: {e}")
42
-
43
- # Fallback dummy recorder function that always returns None
44
- def mic_recorder(*args, **kwargs):
45
- return None
46
-
47
- # --- Configuration & Secrets ---
48
-
49
 
50
  # --- Pydantic Models (from your code) ---
51
 
@@ -62,17 +36,16 @@ class evaluation(BaseModel):
62
  followup: Optional[str] = Field(description="The followup question")
63
  review: Optional[str] = Field(description="Short Review of the answer")
64
 
65
- # --- AI & Logic Functions (from your code, slightly modified) ---
66
 
67
  @st.cache_resource
68
  def get_llm(api_key):
69
  """Cached function to initialize the LLM."""
70
  return ChatGoogleGenerativeAI(
71
- model="gemini-2.5-flash",
72
  temperature=1.0,
73
- google_api_key=api_key # <-- Explicitly pass the key here
74
  )
75
-
76
 
77
  @st.cache_resource
78
  def get_models(_llm_model):
@@ -89,11 +62,10 @@ def read_resume(uploaded_file):
89
  if PdfReader is None:
90
  st.warning("PyPDF2 is not installed; resume text extraction disabled.")
91
  return None
92
- # PdfReader accepts a file-like object
93
  reader = PdfReader(uploaded_file)
94
  text = ""
95
  for page in reader.pages:
96
- text += page.extract_text() or "" # Add check for None
97
  return text
98
  except Exception as e:
99
  st.error(f"Error reading PDF: {e}")
@@ -101,19 +73,9 @@ def read_resume(uploaded_file):
101
 
102
  def generate_questions_from_resume(resume_text, model):
103
  """Generates interview questions from resume text."""
104
- # If LangChain PromptTemplate or LLM wrapper is not available, or LLM not enabled,
105
- # return simple heuristic questions
106
  if PromptTemplate is None or model is None or not st.session_state.get('enable_llm', False):
107
- # Simple fallback: create questions from lines with 'Project'/'Experience' keywords
108
- lines = resume_text.splitlines()
109
- candidates = [l.strip() for l in lines if l and ('project' in l.lower() or 'experience' in l.lower())]
110
- questions = []
111
- for c in candidates:
112
- if len(questions) >= 6:
113
- break
114
- questions.append(f"Tell me more about: {c}")
115
- if not questions:
116
- questions = ["Tell me about your most significant project.", "Describe a challenging bug you fixed.", "How do you design for scalability?", "Which technologies are you most comfortable with?"]
117
  return questions
118
 
119
  parse_resume_prompt_template = PromptTemplate(
@@ -122,32 +84,20 @@ Try to cover all projects and experience. Generate some conceptual questions too
122
  Resume:\n{text}""",
123
  input_variables=['text']
124
  )
125
- # Use the LangChain pipeline if available and enabled
126
  try:
127
  if not st.session_state.get('enable_llm', False):
128
  raise RuntimeError('LLM disabled')
129
  generate_question_from_resume_chain = parse_resume_prompt_template | model
130
  output = generate_question_from_resume_chain.invoke({'text': resume_text})
131
- # attempt to coerce into a list
132
  return getattr(output, 'questions', output)
133
  except Exception as e:
134
  st.warning(f"LLM question generation failed or disabled, using fallback: {e}")
135
- # fallback similar to above
136
- lines = resume_text.splitlines()
137
- candidates = [l.strip() for l in lines if l and ('project' in l.lower() or 'experience' in l.lower())]
138
- questions = []
139
- for c in candidates:
140
- if len(questions) >= 6:
141
- break
142
- questions.append(f"Tell me more about: {c}")
143
- if not questions:
144
- questions = ["Tell me about your most significant project.", "Describe a challenging bug you fixed.", "How do you design for scalability?", "Which technologies are you most comfortable with?"]
145
  return questions
146
 
147
  def get_introduction(model):
148
  """Gets the AI's intro and first question."""
149
  if PromptTemplate is None or model is None or not st.session_state.get('enable_llm', False):
150
- # Return a simple dict-like fallback
151
  return type('O', (), {'intro': "Hello, I'm Interviewer.AI. Please introduce yourself.", 'question': "Can you briefly introduce yourself?"})()
152
 
153
  introduction_prompt = PromptTemplate(template="""Introduce yourself to the user telling the user that you are a AI agent. And ask the user to give introduction""")
@@ -185,7 +135,6 @@ def evaluate_answer(question, answer, model):
185
  score = 50
186
  review = "Thank you for your answer. Provide more details next time."
187
  followup = None
188
- # small heuristic: longer answers get better score
189
  if answer and len(answer.split()) > 50:
190
  score = 80
191
  review = "Good answer β€” you covered several points."
@@ -206,95 +155,41 @@ If a good followup question can be asked generate it but only if it is a genuine
206
  return output
207
  except Exception as e:
208
  st.warning(f"LLM evaluation failed or disabled: {e}")
209
- # fallback heuristic
210
  score = 50
211
  review = "Thank you for your answer. Provide more details next time."
212
  followup = None
213
  if answer and len(answer.split()) > 50:
214
  score = 80
215
- review = "Good answer β€” you covered several points."
216
  elif answer and len(answer.split()) > 20:
217
  score = 65
218
- review = "Decent answer; add more concrete examples."
219
  return type('O', (), {'marks': score, 'review': review, 'followup': followup})()
220
 
221
- # --- Streamlit Audio/Visual Functions ---
222
 
223
  def text_to_speech_and_display(text, autoplay=True):
224
- """Converts text to speech, displays text, and plays audio."""
 
 
 
225
  if not text:
226
  return
227
 
228
  try:
229
- # Display the caption
230
  if 'chat_history' not in st.session_state:
231
  st.session_state.chat_history = []
232
  st.session_state.chat_history.append(f"**Interviewer:** {text}")
233
-
234
- # Generate audio if gTTS available
235
- if gTTS is None:
236
- # No TTS available; just show text
237
- return
238
-
239
- tts = gTTS(text=text, lang='en', slow=False)
240
- audio_fp = io.BytesIO()
241
- tts.write_to_fp(audio_fp)
242
- audio_fp.seek(0)
243
-
244
- # Display audio player
245
- st.audio(audio_fp, format='audio/mp3', autoplay=autoplay)
246
-
247
  except Exception as e:
248
- st.error(f"Error in text-to-speech: {e}")
249
-
250
- def speech_to_text(audio_bytes):
251
- """Converts recorded audio bytes to text using SpeechRecognition."""
252
- if not audio_bytes:
253
- return "No audio recorded."
254
-
255
- if sr is None:
256
- st.warning("speech_recognition is not installed; microphone input unavailable.")
257
- return None
258
 
259
- r = sr.Recognizer()
260
-
261
- # Need to save bytes to a temporary WAV file
262
- try:
263
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_wav:
264
- temp_wav.write(audio_bytes)
265
- temp_wav_path = temp_wav.name
266
 
267
- with sr.AudioFile(temp_wav_path) as source:
268
- audio_data = r.record(source)
269
-
270
- text = r.recognize_google(audio_data)
271
- if 'chat_history' not in st.session_state:
272
- st.session_state.chat_history = []
273
- st.session_state.chat_history.append(f"**You:** {text}")
274
- return text
275
-
276
- except sr.UnknownValueError:
277
- st.warning("Could not understand audio.")
278
- return None
279
- except sr.RequestError as e:
280
- st.error(f"Speech recognition service error: {e}")
281
- return None
282
- except Exception as e:
283
- st.error(f"Error processing audio: {e}")
284
- return None
285
- finally:
286
- if 'temp_wav_path' in locals() and os.path.exists(temp_wav_path):
287
- try:
288
- os.remove(temp_wav_path)
289
- except Exception:
290
- pass
291
 
292
  # --- Main Streamlit App ---
293
 
294
  st.set_page_config(page_title="AI Interviewer", layout="wide")
295
  st.title("Interviewer.AI")
296
 
297
-
298
  # Initialize LLM and models
299
  llm = None
300
  gen_q_model = None
@@ -305,12 +200,9 @@ eval_model = None
305
  if genai is None or ChatGoogleGenerativeAI is None:
306
  st.warning("Google GenAI or LangChain wrappers not available. App will use deterministic fallbacks.")
307
 
308
- # Explicit per-session toggle to enable LLM features. This prevents accidental
309
- # LLM calls (and remote 403 errors) unless the user explicitly opts in.
310
  if 'enable_llm' not in st.session_state:
311
  st.session_state.enable_llm = False
312
 
313
-
314
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
315
  api_key_exists = bool(GOOGLE_API_KEY)
316
 
@@ -352,7 +244,6 @@ if st.button("Test Google API Connection"):
352
  else:
353
  try:
354
  with st.spinner("Testing API connection..."):
355
- # Simple test call
356
  test_response = llm.invoke("Say 'Hello' if you can hear me.")
357
  st.success("βœ… SUCCESS! API is working correctly.")
358
  st.info(f"Response: {test_response.content if hasattr(test_response, 'content') else str(test_response)}")
@@ -362,9 +253,7 @@ if st.button("Test Google API Connection"):
362
 
363
  st.divider()
364
 
365
-
366
  # --- Session State Initialization ---
367
- # This is crucial for making the app work step-by-step
368
  if 'stage' not in st.session_state:
369
  st.session_state.stage = 'start'
370
  if 'chat_history' not in st.session_state:
@@ -395,33 +284,26 @@ if st.session_state.stage == 'start':
395
  st.error("Could not extract text from the resume. Please try another file.")
396
  st.session_state.stage = 'start'
397
  else:
398
- # 1. Generate Questions (guarded inside function)
399
  st.session_state.questions = generate_questions_from_resume(resume_text, gen_q_model)
400
  if not st.session_state.questions:
401
  st.warning("No AI-generated questions returned; using fallback questions.")
402
  st.session_state.questions = generate_questions_from_resume(resume_text, None)
403
 
404
- # 2. Get AI Introduction (guarded)
405
  intro_output = get_introduction(intro_model)
406
  st.session_state.current_question = getattr(intro_output, 'question', "Can you introduce yourself?")
407
 
408
  # 3. Move to next stage and display intro
409
  st.session_state.stage = 'awaiting_intro'
410
- #************************
411
- #************************
412
- #************************
413
 
414
- # text_to_speech_and_display(getattr(intro_output, 'intro', "Hello, I'm Interviewer.AI. Please introduce yourself."))
415
- # text_to_speech_and_display(getattr(intro_output, 'question', "Can you introduce yourself?"))
416
-
417
- #************************
418
- #************************
419
- #************************
420
  st.rerun()
421
  except Exception as e:
422
- # Catch-all to prevent any LLM/network exceptions from surfacing to the client
423
  st.error(f"An error occurred while processing the resume. Using fallback behaviour. Error: {e}")
424
- # Fallback simple questions
425
  fallback_qs = ["Tell me about your most significant project.", "Describe a challenging bug you fixed.", "How do you design for scalability?", "Which technologies are you most comfortable with?"]
426
  st.session_state.questions = fallback_qs
427
  st.session_state.stage = 'asking_question'
@@ -432,115 +314,99 @@ if st.session_state.stage != 'start':
432
 
433
  # --- Chat History Display ---
434
  st.subheader("Interview Transcript")
435
- chat_container = st.container()
436
  with chat_container:
437
  for entry in st.session_state.chat_history:
438
  st.markdown(entry)
439
 
440
- # visual divider
441
  try:
442
  st.divider()
443
  except Exception:
444
  st.markdown('---')
445
 
446
- # --- Audio Recorder ---
447
- # This component returns audio bytes when the user stops recording
448
- st.write("Your turn to speak:")
449
- audio_bytes = mic_recorder(
450
- start_prompt="Start Recording",
451
- stop_prompt="Stop Recording",
452
- key='recorder'
453
- )
454
-
455
  # --- End Interview Button ---
456
  if st.button("End Interview", type="primary"):
457
  st.session_state.stage = 'finished'
458
  st.rerun()
459
 
460
- # --- Process Recorded Audio ---
461
- # mic_recorder may return None, bytes, or a dict with a 'bytes' key depending on implementation
462
- def _extract_audio_bytes(rec):
463
- if rec is None:
464
- return None
465
- if isinstance(rec, dict):
466
- # some implementations return {'bytes': b'...', 'start':..., ...}
467
- return rec.get('bytes') or rec.get('audio') or None
468
- if isinstance(rec, (bytes, bytearray)):
469
- return bytes(rec)
470
- return None
471
 
472
- extracted_audio = _extract_audio_bytes(audio_bytes)
473
- if extracted_audio:
474
- with st.spinner("Transcribing your answer..."):
475
- user_text = speech_to_text(extracted_audio)
476
 
477
- if user_text:
478
- # --- STAGE 1: Process User's Introduction ---
479
- if st.session_state.stage == 'awaiting_intro':
480
- with st.spinner("Thinking of a followup..."):
481
- followup = ask_followup(user_text, intro_model)
482
- st.session_state.current_question = followup
483
- # text_to_speech_and_display(followup)
484
- st.session_state.stage = 'awaiting_intro_followup'
485
- st.rerun()
486
-
487
- # --- STAGE 2: Process Followup to Intro ---
488
- elif st.session_state.stage == 'awaiting_intro_followup':
489
- # text_to_speech_and_display("OK, Great. Let's start the interview with questions from your resume.")
490
- st.session_state.stage = 'asking_question' # Move to main questions
 
491
  st.rerun()
 
 
 
 
 
 
492
 
493
- # --- STAGE 4: Process Answer to a Main Question ---
494
- elif st.session_state.stage == 'awaiting_answer':
495
- with st.spinner("Evaluating your answer..."):
496
- question_asked = st.session_state.current_question
497
- output = evaluate_answer(question_asked, user_text, eval_model)
498
-
499
- st.session_state.total_marks += output.marks
500
- st.session_state.num_questions += 1
501
-
502
- if output.review:
503
- # text_to_speech_and_display(output.review)
504
- bs_variable=5
505
-
506
- if output.followup:
507
- # Ask followup question
508
- st.session_state.current_question = output.followup
509
- # text_to_speech_and_display(output.followup)
510
- st.session_state.stage = 'awaiting_followup_answer'
511
- else:
512
- # Move to next question
513
- st.session_state.q_index += 1
514
- st.session_state.stage = 'asking_question'
515
- st.rerun()
516
-
517
- # --- STAGE 5: Process Answer to a Followup Question ---
518
- elif st.session_state.stage == 'awaiting_followup_answer':
519
- with st.spinner("Evaluating your answer..."):
520
- question_asked = st.session_state.current_question
521
- output = evaluate_answer(question_asked, user_text, eval_model)
522
-
523
- st.session_state.total_marks += output.marks
524
- st.session_state.num_questions += 1
525
-
526
- if output.review:
527
- # text_to_speech_and_display(output.review)
528
- bs_variable=6
529
- # Always move to the next main question after a followup
530
  st.session_state.q_index += 1
531
  st.session_state.stage = 'asking_question'
532
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
 
534
  # --- STAGE 3: Ask a New Question ---
 
535
  if st.session_state.stage == 'asking_question':
536
  if st.session_state.q_index < len(st.session_state.questions):
537
- # Ask the next question
538
  question = st.session_state.questions[st.session_state.q_index]
539
  st.session_state.current_question = question
540
- # text_to_speech_and_display(question)
541
  st.session_state.stage = 'awaiting_answer'
542
  else:
543
- # No more questions
544
  st.session_state.stage = 'finished'
545
  st.rerun()
546
 
@@ -557,12 +423,12 @@ if st.session_state.stage != 'start':
557
  st.markdown(f"**Total Questions Answered:** {st.session_state.num_questions}")
558
  st.markdown(f"**Average Score:** {final_score:.2f} / 100")
559
 
 
560
  st.subheader("Full Transcript")
561
  for entry in st.session_state.chat_history:
562
  st.markdown(entry)
563
 
564
  if st.button("Start New Interview"):
565
- # Clear all session state
566
  for key in st.session_state.keys():
567
  del st.session_state[key]
568
- st.rerun()
 
4
  except Exception:
5
  PdfReader = None
6
 
7
+ # Optional AI SDKs
8
  try:
9
  import google.generativeai as genai
10
  except Exception:
 
19
 
20
  from pydantic import BaseModel, Field
21
  from typing import Optional
 
 
 
 
 
 
 
 
 
 
 
 
22
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # --- Pydantic Models (from your code) ---
25
 
 
36
  followup: Optional[str] = Field(description="The followup question")
37
  review: Optional[str] = Field(description="Short Review of the answer")
38
 
39
+ # --- AI & Logic Functions (from your code) ---
40
 
41
  @st.cache_resource
42
  def get_llm(api_key):
43
  """Cached function to initialize the LLM."""
44
  return ChatGoogleGenerativeAI(
45
+ model="gemini-2.5-flash",
46
  temperature=1.0,
47
+ google_api_key=api_key
48
  )
 
49
 
50
  @st.cache_resource
51
  def get_models(_llm_model):
 
62
  if PdfReader is None:
63
  st.warning("PyPDF2 is not installed; resume text extraction disabled.")
64
  return None
 
65
  reader = PdfReader(uploaded_file)
66
  text = ""
67
  for page in reader.pages:
68
+ text += page.extract_text() or ""
69
  return text
70
  except Exception as e:
71
  st.error(f"Error reading PDF: {e}")
 
73
 
74
  def generate_questions_from_resume(resume_text, model):
75
  """Generates interview questions from resume text."""
 
 
76
  if PromptTemplate is None or model is None or not st.session_state.get('enable_llm', False):
77
+ # Simple fallback
78
+ questions = ["Tell me about your most significant project.", "Describe a challenging bug you fixed.", "How do you design for scalability?", "Which technologies are you most comfortable with?"]
 
 
 
 
 
 
 
 
79
  return questions
80
 
81
  parse_resume_prompt_template = PromptTemplate(
 
84
  Resume:\n{text}""",
85
  input_variables=['text']
86
  )
 
87
  try:
88
  if not st.session_state.get('enable_llm', False):
89
  raise RuntimeError('LLM disabled')
90
  generate_question_from_resume_chain = parse_resume_prompt_template | model
91
  output = generate_question_from_resume_chain.invoke({'text': resume_text})
 
92
  return getattr(output, 'questions', output)
93
  except Exception as e:
94
  st.warning(f"LLM question generation failed or disabled, using fallback: {e}")
95
+ questions = ["Tell me about your most significant project.", "Describe a challenging bug you fixed.", "How do you design for scalability?", "Which technologies are you most comfortable with?"]
 
 
 
 
 
 
 
 
 
96
  return questions
97
 
98
  def get_introduction(model):
99
  """Gets the AI's intro and first question."""
100
  if PromptTemplate is None or model is None or not st.session_state.get('enable_llm', False):
 
101
  return type('O', (), {'intro': "Hello, I'm Interviewer.AI. Please introduce yourself.", 'question': "Can you briefly introduce yourself?"})()
102
 
103
  introduction_prompt = PromptTemplate(template="""Introduce yourself to the user telling the user that you are a AI agent. And ask the user to give introduction""")
 
135
  score = 50
136
  review = "Thank you for your answer. Provide more details next time."
137
  followup = None
 
138
  if answer and len(answer.split()) > 50:
139
  score = 80
140
  review = "Good answer β€” you covered several points."
 
155
  return output
156
  except Exception as e:
157
  st.warning(f"LLM evaluation failed or disabled: {e}")
 
158
  score = 50
159
  review = "Thank you for your answer. Provide more details next time."
160
  followup = None
161
  if answer and len(answer.split()) > 50:
162
  score = 80
 
163
  elif answer and len(answer.split()) > 20:
164
  score = 65
 
165
  return type('O', (), {'marks': score, 'review': review, 'followup': followup})()
166
 
167
+ # --- MODIFIED Streamlit Audio/Visual Function ---
168
 
169
  def text_to_speech_and_display(text, autoplay=True):
170
+ """
171
+ MODIFIED: This function no longer plays audio.
172
+ It just displays the text in the chat history.
173
+ """
174
  if not text:
175
  return
176
 
177
  try:
 
178
  if 'chat_history' not in st.session_state:
179
  st.session_state.chat_history = []
180
  st.session_state.chat_history.append(f"**Interviewer:** {text}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  except Exception as e:
182
+ st.error(f"Error in text_to_speech_and_display: {e}")
 
 
 
 
 
 
 
 
 
183
 
184
+ # --- DELETED speech_to_text function ---
185
+ # We are replacing it with a text_input
 
 
 
 
 
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
  # --- Main Streamlit App ---
189
 
190
  st.set_page_config(page_title="AI Interviewer", layout="wide")
191
  st.title("Interviewer.AI")
192
 
 
193
  # Initialize LLM and models
194
  llm = None
195
  gen_q_model = None
 
200
  if genai is None or ChatGoogleGenerativeAI is None:
201
  st.warning("Google GenAI or LangChain wrappers not available. App will use deterministic fallbacks.")
202
 
 
 
203
  if 'enable_llm' not in st.session_state:
204
  st.session_state.enable_llm = False
205
 
 
206
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
207
  api_key_exists = bool(GOOGLE_API_KEY)
208
 
 
244
  else:
245
  try:
246
  with st.spinner("Testing API connection..."):
 
247
  test_response = llm.invoke("Say 'Hello' if you can hear me.")
248
  st.success("βœ… SUCCESS! API is working correctly.")
249
  st.info(f"Response: {test_response.content if hasattr(test_response, 'content') else str(test_response)}")
 
253
 
254
  st.divider()
255
 
 
256
  # --- Session State Initialization ---
 
257
  if 'stage' not in st.session_state:
258
  st.session_state.stage = 'start'
259
  if 'chat_history' not in st.session_state:
 
284
  st.error("Could not extract text from the resume. Please try another file.")
285
  st.session_state.stage = 'start'
286
  else:
287
+ # 1. Generate Questions
288
  st.session_state.questions = generate_questions_from_resume(resume_text, gen_q_model)
289
  if not st.session_state.questions:
290
  st.warning("No AI-generated questions returned; using fallback questions.")
291
  st.session_state.questions = generate_questions_from_resume(resume_text, None)
292
 
293
+ # 2. Get AI Introduction
294
  intro_output = get_introduction(intro_model)
295
  st.session_state.current_question = getattr(intro_output, 'question', "Can you introduce yourself?")
296
 
297
  # 3. Move to next stage and display intro
298
  st.session_state.stage = 'awaiting_intro'
 
 
 
299
 
300
+ # --- MODIFIED: Display text directly ---
301
+ text_to_speech_and_display(getattr(intro_output, 'intro', "Hello, I'm Interviewer.AI. Please introduce yourself."))
302
+ text_to_speech_and_display(getattr(intro_output, 'question', "Can you introduce yourself?"))
303
+
 
 
304
  st.rerun()
305
  except Exception as e:
 
306
  st.error(f"An error occurred while processing the resume. Using fallback behaviour. Error: {e}")
 
307
  fallback_qs = ["Tell me about your most significant project.", "Describe a challenging bug you fixed.", "How do you design for scalability?", "Which technologies are you most comfortable with?"]
308
  st.session_state.questions = fallback_qs
309
  st.session_state.stage = 'asking_question'
 
314
 
315
  # --- Chat History Display ---
316
  st.subheader("Interview Transcript")
317
+ chat_container = st.container(height=400) # Added height for scrolling
318
  with chat_container:
319
  for entry in st.session_state.chat_history:
320
  st.markdown(entry)
321
 
 
322
  try:
323
  st.divider()
324
  except Exception:
325
  st.markdown('---')
326
 
 
 
 
 
 
 
 
 
 
327
  # --- End Interview Button ---
328
  if st.button("End Interview", type="primary"):
329
  st.session_state.stage = 'finished'
330
  st.rerun()
331
 
332
+ # --- REPLACEMENT: Text Input Area ---
333
+ user_text = None # Initialize user_text
334
+ is_disabled = (st.session_state.stage == 'finished')
 
 
 
 
 
 
 
 
335
 
336
+ with st.form(key="answer_form", clear_on_submit=True):
337
+ answer = st.text_input("Your answer:", disabled=is_disabled)
338
+ submit_button = st.form_submit_button(label="Submit Answer", disabled=is_disabled)
 
339
 
340
+ if submit_button and answer:
341
+ user_text = answer
342
+ st.session_state.chat_history.append(f"**You:** {user_text}")
343
+ # --- END OF REPLACEMENT ---
344
+
345
+
346
+ # --- Process Submitted Text ---
347
+ if user_text:
348
+ # --- STAGE 1: Process User's Introduction ---
349
+ if st.session_state.stage == 'awaiting_intro':
350
+ with st.spinner("Thinking of a followup..."):
351
+ followup = ask_followup(user_text, intro_model)
352
+ st.session_state.current_question = followup
353
+ text_to_speech_and_display(followup) # This now just displays text
354
+ st.session_state.stage = 'awaiting_intro_followup'
355
  st.rerun()
356
+
357
+ # --- STAGE 2: Process Followup to Intro ---
358
+ elif st.session_state.stage == 'awaiting_intro_followup':
359
+ text_to_speech_and_display("OK, Great. Let's start the interview with questions from your resume.")
360
+ st.session_state.stage = 'asking_question' # Move to main questions
361
+ st.rerun()
362
 
363
+ # --- STAGE 4: Process Answer to a Main Question ---
364
+ elif st.session_state.stage == 'awaiting_answer':
365
+ with st.spinner("Evaluating your answer..."):
366
+ question_asked = st.session_state.current_question
367
+ output = evaluate_answer(question_asked, user_text, eval_model)
368
+
369
+ st.session_state.total_marks += output.marks
370
+ st.session_state.num_questions += 1
371
+
372
+ if output.review:
373
+ text_to_speech_and_display(output.review) # This now just displays text
374
+
375
+ if output.followup:
376
+ st.session_state.current_question = output.followup
377
+ text_to_speech_and_display(output.followup) # This now just displays text
378
+ st.session_state.stage = 'awaiting_followup_answer'
379
+ else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  st.session_state.q_index += 1
381
  st.session_state.stage = 'asking_question'
382
+ st.rerun()
383
+
384
+ # --- STAGE 5: Process Answer to a Followup Question ---
385
+ elif st.session_state.stage == 'awaiting_followup_answer':
386
+ with st.spinner("Evaluating your answer..."):
387
+ question_asked = st.session_state.current_question
388
+ output = evaluate_answer(question_asked, user_text, eval_model)
389
+
390
+ st.session_state.total_marks += output.marks
391
+ st.session_state.num_questions += 1
392
+
393
+ if output.review:
394
+ text_to_speech_and_display(output.review) # This now just displays text
395
+
396
+ st.session_state.q_index += 1
397
+ st.session_state.stage = 'asking_question'
398
+ st.rerun()
399
 
400
  # --- STAGE 3: Ask a New Question ---
401
+ # This runs when the page loads into this state, *before* user input
402
  if st.session_state.stage == 'asking_question':
403
  if st.session_state.q_index < len(st.session_state.questions):
 
404
  question = st.session_state.questions[st.session_state.q_index]
405
  st.session_state.current_question = question
406
+ text_to_speech_and_display(question) # This now just displays text
407
  st.session_state.stage = 'awaiting_answer'
408
  else:
409
+ text_to_speech_and_display("That's all the questions I have. Thank you!")
410
  st.session_state.stage = 'finished'
411
  st.rerun()
412
 
 
423
  st.markdown(f"**Total Questions Answered:** {st.session_state.num_questions}")
424
  st.markdown(f"**Average Score:** {final_score:.2f} / 100")
425
 
426
+ # Transcript is already shown above, but we can show it again
427
  st.subheader("Full Transcript")
428
  for entry in st.session_state.chat_history:
429
  st.markdown(entry)
430
 
431
  if st.button("Start New Interview"):
 
432
  for key in st.session_state.keys():
433
  del st.session_state[key]
434
+ st.rerun()