"""
interview_coach.py — Version Alpha
────────────────────────────────────────────────────────────────────────────
Pure Gradio UI layer. Imports all logic from engine.py and config.py.
All business logic lives in engine.py and agents/.
UI Structure (4 Tabs):
Tab 1 — 🎯 Practice : JD input + mode selector + Q&A + feedback
Tab 2 — 📈 History : Session history + PDF download
Tab 3 — 💡 Prep Sheet : AI-generated role-specific preparation guide
Tab 4 — ℹ️ About : How scoring works, STAR guide
"""
import gradio as gr
import os
from config import CUSTOM_CSS, INTERVIEW_MODES
from engine import (
render_history, generate_all_questions,
score_answer, next_question, generate_pdf_report,
)
#Custom UI Theme
custom_theme = gr.themes.Soft(
primary_hue=gr.themes.colors.purple,
secondary_hue=gr.themes.colors.red,
neutral_hue=gr.themes.colors.slate,
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "sans-serif"],
)
# ── Animated background HTML ──────────────────────────────────────────────────
_BLOBS_HTML = """
"""
_HEADER_HTML = """
AI Interview Coach
Practice · Get Feedback · Improve
Powered by Qwen/Qwen2.5-7B-Instruct
"""
def _score_badge_html(score_result: dict) -> str:
"""Render keyword coverage badges cleanly as inline HTML without any percentage bubbles."""
if not score_result:
return ""
hit = score_result.get("hit_keywords", [])
missed = score_result.get("missed_keywords", [])
badge_style = "display:inline-block;padding:3px 9px;border-radius:20px;font-size:0.78rem;font-weight:600;margin:3px 3px;"
hit_badges = "".join(f'✅ {k}' for k in hit)
missed_badges = "".join(f'❌ {k}' for k in missed)
# ── FIXED: Completely stripped out {color} and {cov:.0f}% to prevent any NameErrors ──
return f"""
🔑 KEYWORD SUMMARY
{hit_badges}{missed_badges}
"""
def _progress_bar_html(current: int, total: int) -> str:
"""Render an HTML progress bar for question progress."""
if total == 0:
return ""
pct = int((current / total) * 100)
return f"""
Progress
Q{current} of {total}
"""
_ABOUT_MD = """
## 🎙️ About AI Interview Coach
**AI Interview Coach** is an intelligent interview preparation tool powered by **Mistral 7B** via the HuggingFace Inference API.
---
### 🔄 How It Works
1. **Paste** your target job description
2. **Choose** your interview depth (Quick / Standard / Deep Dive)
3. **Answer** AI-generated questions tailored to that specific role
4. **Get feedback** with scores, strengths, weaknesses, and keyword analysis
5. **Download** a PDF report of your session
---
### 📊 How Scoring Works
| Score | Meaning |
|-------|---------|
| 8–10 | Excellent — strong STAR structure, good keyword coverage |
| 5–7 | Good — solid effort, but missing some key terms or depth |
| 1–4 | Needs work — expand your answers and use role-specific language |
| NIL | Irrelevant — answer must be a genuine attempt at the question |
> **Keyword Coverage Rule:** The AI extracts 5–7 key terms from the job description. If your answer uses fewer than 40% of them, your score is capped at **5/10**. Use the Prep Sheet to learn which terms to include.
---
### ⭐ The STAR Format
Use this structure for behavioral and situational answers:
| Part | What to Say | Example |
|------|-------------|---------|
| **S**ituation | Set the context | "At my previous company, we had a critical deadline..." |
| **T**ask | Your role/responsibility | "I was responsible for..." |
| **A**ction | What YOU did | "I specifically implemented..." |
| **R**esult | Outcome (quantified if possible) | "This reduced load time by 40%..." |
---
### 🚀 Interview Modes
| Mode | Questions | Best For |
|------|-----------|---------|
| ⚡ Quick | 3 | Final-hour revision, confidence check |
| 📋 Standard | 5 | Regular practice sessions |
| 🔬 Deep Dive | 7 | Thorough preparation for important interviews |
---
*Built for the HuggingFace Hackathon 2026 · Model: Qwen/Qwen2.5-7B-Instruct (8B params)*
"""
"""
import gradio as gr
with gr.Blocks(theme=gr.Theme.from_hub("theme-repo/STONE_Theme")) as demo:
...
"""
# ── Build UI ──────────────────────────────────────────────────────────────────
with gr.Blocks(title="AI Interview Coach", fill_width=True) as demo:
# ── Shared State ──────────────────────────────────────────────────────────
history_state = gr.State([])
q_index = gr.State("0")
job_profile_state = gr.State({
"valid": False, "industry": "General",
"role_level": "Mid-Level", "keywords": [],
"tips": "", "interview_style": "Mixed",
})
score_result_state = gr.State({}) # Latest ScorerAgent result for badge rendering
n_questions_state = gr.State(3) # Resolved question count for progress bar
# ── Background + Header ───────────────────────────────────────────────────
gr.HTML(_BLOBS_HTML, elem_id="bg-blobs")
gr.HTML(_HEADER_HTML, elem_id="app-header")
with gr.Tabs():
# ══════════════════════════════════════════════════════════════════════
# TAB 1: PRACTICE
# ══════════════════════════════════════════════════════════════════════
with gr.Tab("🎯 Practice"):
with gr.Row(equal_height=False):
# ── LEFT COLUMN: Job Input + Mode ─────────────────────────────
with gr.Column(scale=1, min_width=360):
with gr.Group(elem_id="step-1-group"):
gr.HTML('''
Step 1:
📋 Paste the Job Description
''')
job_desc_box = gr.Textbox(
label="Job Description",
show_label=False,
lines=9,
placeholder="Paste from LinkedIn, a company's careers page, etc.\n\nTip: A longer, more detailed JD = better tailored questions.",
)
with gr.Group(elem_id="step-2-group"):
gr.HTML('''
Step 2
🎚️ Choose Interview Depth
How many questions do you want to answer?
''')
mode_selector = gr.Radio(
choices=list(INTERVIEW_MODES.keys()),
value="⚡ Quick (3 Questions)",
label="Interview Mode",
show_label=False,
)
start_btn = gr.Button(
"🚀 Start Interview", variant="primary", size="lg",
)
gr.HTML("""
💡 Tips for a better session:
• Paste the full job posting (350+ characters)
• Answer in complete sentences
• Use STAR format for behavioral questions
""")
# ── RIGHT COLUMN: Q&A ─────────────────────────────────────────
with gr.Column(scale=1, min_width=360):
with gr.Group(elem_id="step-3-group"):
gr.HTML('''
Step 3:
💬 Answer the Questions
''')
progress_bar_display = gr.HTML('Session Progress-->
')
"""gr.HTML('''
Interview Question
''')
"""
question_box = gr.Textbox(
label="Interview Question",
show_label=True,
lines=3,
interactive=False,
placeholder="Your question will appear here after you click Start Interview...",
)
answer_box = gr.Textbox(
label="Your Answer",
show_label=True,
lines=5,
placeholder=(
"Answer in complete sentences.\n\n"
"💡 STAR Format: Situation → Task → Action → Result"
),
)
with gr.Row():
feedback_btn = gr.Button("📊 Get Feedback", variant="primary", size="lg")
next_btn = gr.Button("➡️ Next Question", variant="secondary", size="lg")
# ── Keyword Coverage Badge Area ───────────────────────────────────
keyword_badges_display = gr.HTML('Keyword Coverage Badges-->
')
# ── Feedback ──────────────────────────────────────────────────────
with gr.Group(elem_id="step-4-group"):
gr.HTML('''
Step 4:
📋 Get feedback
''')
feedback_box = gr.Textbox(
label="AI Coach Feedback",
show_label=False,
interactive=False,
lines=8,
elem_id="feedback_box",
placeholder="Feedback will appear here after you click Get Feedback...",
)
# ── Review Panels ─────────────────────────────────────────────────
with gr.Accordion("🔁 Previous Question Review", open=False):
with gr.Row():
prev_question_box = gr.Textbox(
label="Previous Question", interactive=False, lines=2,
placeholder="Appears after clicking Next Question...",
)
prev_answer_box = gr.Textbox(
label="Your Previous Answer", interactive=False, lines=3,
placeholder="Your last answer...",
)
with gr.Accordion("📋 This Session's Log", open=False):
session_log_display = gr.Markdown("Complete questions to see your session log here.")
# ══════════════════════════════════════════════════════════════════════
# TAB 2: HISTORY & PDF
# ══════════════════════════════════════════════════════════════════════
with gr.Tab("📈 History & Report") as history_tab:
gr.HTML("""
⬇️ Download Your Interview Report
Generate a professionally formatted PDF containing your questions, answers, AI feedback, keyword analysis, and coaching recommendations.
""")
with gr.Row(elem_id="report-actions"):
download_report_btn = gr.DownloadButton("📥 Download Report", variant="primary", size="lg")
refresh_btn = gr.Button("🔄 Refresh Preview", variant="secondary")
clear_btn = gr.Button("🗑️ Clear Session", variant="secondary")
history_display = gr.Markdown("### 📝 Session Summary\n\n" + render_history([]), elem_classes=["padded-markdown"])
# ══════════════════════════════════════════════════════════════════════
# TAB 3: PREP SHEET
# ══════════════════════════════════════════════════════════════════════
with gr.Tab("💡 Prep Sheet"):
gr.HTML("""
💡 AI-Generated Preparation Guide
This guide is generated specifically for your target role after you click Start Interview.
It includes expected keywords, interview style tips, and curated resources.
""")
tips_display = gr.Markdown(
"*Start an interview on the Practice tab to generate your personalised prep sheet.*", elem_classes=["padded-markdown"]
)
# ══════════════════════════════════════════════════════════════════════
# TAB 4: ABOUT
# ══════════════════════════════════════════════════════════════════════
with gr.Tab("ℹ️ About"):
gr.Markdown(_ABOUT_MD, elem_classes=["padded-markdown"])
# ── Event wiring ──────────────────────────────────────────────────────────
def _handle_start(job_desc, mode_label, history_state, job_profile_state):
"""Wrapper: calls engine, then updates progress bar HTML."""
result = generate_all_questions(job_desc, mode_label, history_state, job_profile_state)
# result = (question, "0", progress_str, history_state, tips_md, job_profile, score_result)
first_q, idx_str, _prog_str, new_hist, tips_md, new_profile, new_score_res = result
n = INTERVIEW_MODES.get(mode_label, 3)
prog_html = _progress_bar_html(1, n) if "Please" not in first_q else ''
empty_html = ''
return first_q, idx_str, new_hist, tips_md, new_profile, new_score_res, n, prog_html, empty_html
start_btn.click(
fn=_handle_start,
inputs=[job_desc_box, mode_selector, history_state, job_profile_state],
outputs=[
question_box, q_index, history_state,
tips_display, job_profile_state, score_result_state,
n_questions_state, progress_bar_display, keyword_badges_display,
],
).then(
fn=render_history,
inputs=[history_state],
outputs=[history_display],
)
def _handle_feedback(answer, q_index_str, history_state, job_profile_state, n_total):
"""Wrapper: calls engine scorer, then renders keyword badge HTML."""
feedback_text, new_hist, result = score_answer(
answer, q_index_str, history_state, job_profile_state
)
badges_html = _score_badge_html(result)
idx = int(q_index_str) if q_index_str else 0
prog_html = _progress_bar_html(idx + 1, n_total)
return feedback_text, new_hist, result, badges_html, prog_html
feedback_btn.click(
fn=_handle_feedback,
inputs=[answer_box, q_index, history_state, job_profile_state, n_questions_state],
outputs=[feedback_box, history_state, score_result_state, keyword_badges_display, progress_bar_display],
).then(
fn=render_history,
inputs=[history_state],
outputs=[history_display],
)
def _handle_next(q_index_str, answer, history_state, n_total):
"""Wrapper: calls engine next_question, updates progress bar."""
result = next_question(q_index_str, answer, history_state)
# result = (question, answer, idx_str, progress_str, history, prev_q, prev_a, log)
new_q, new_a, new_idx, prog_str, new_hist, prev_q, prev_a, log = result
idx = int(new_idx) if new_idx else 0
prog_html = _progress_bar_html(idx + 1, n_total) if "Complete" not in prog_str else _progress_bar_html(n_total, n_total)
empty_html = '' # collapse badge area between questions
return new_q, new_a, new_idx, new_hist, prev_q, prev_a, log, prog_html, empty_html
next_btn.click(
fn=_handle_next,
inputs=[q_index, answer_box, history_state, n_questions_state],
outputs=[
question_box, answer_box, q_index, history_state,
prev_question_box, prev_answer_box, session_log_display,
progress_bar_display, keyword_badges_display,
],
)
history_tab.select(
fn=generate_pdf_report,
inputs=[history_state],
outputs=[download_report_btn],
)
refresh_btn.click(
fn=render_history,
inputs=[history_state],
outputs=[history_display],
).then(
fn=generate_pdf_report,
inputs=[history_state],
outputs=[download_report_btn],
)
def clear_history_fn(history_state):
return [], "Session cleared! Start a new interview above."
clear_btn.click(
fn=clear_history_fn,
inputs=[history_state],
outputs=[history_state, history_display],
)
# ── Launch ────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
demo.launch(theme=custom_theme, css=CUSTOM_CSS, share=True)