A newer version of the Gradio SDK is available: 6.19.0
Main Code Backup:
import gradio as gr
import requests
import json
import datetime
import os
OLLAMA_URL = "http://localhost:11434/api/generate"
HISTORY_FILE = "interview_history.json"
# ββ Persistent History Helpers βββββββββββββββββββββββββββββββββββββββββββββββββ
def load_history():
"""Load history from JSON file on disk"""
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, "r") as f:
return json.load(f)
return []
def save_history(history):
"""Save history to JSON file on disk"""
with open(HISTORY_FILE, "w") as f:
json.dump(history, f, indent=2)
# ββ Ollama helper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def ask_ollama(prompt, model="mistral:7b", temperature=0.7):
try:
payload = {
"model": model,
"prompt": prompt,
"stream": False,
"temperature": temperature
}
r = requests.post(OLLAMA_URL, json=payload, timeout=300)
return r.json().get("response", "No response")
except Exception as e:
return f"β Error: {str(e)}"
# ββ Question generation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
QUESTION_PROMPTS = [
"Generate ONE interview question about the candidate's most recent project experience. Question:",
"Generate ONE follow-up interview question about the specific technologies or tools used. Question:",
"Generate ONE interview question about a challenge they faced and how they overcame it. Question:",
]
def clean_question(raw):
if "Question:" in raw:
raw = raw.split("Question:")[-1].strip()
if "?" in raw:
raw = raw.split("?")[0] + "?"
return raw.strip()
def generate_all_questions(job_desc, history_state):
if not job_desc or len(job_desc.strip()) < 20:
return "Please paste a job description (20+ characters).", "", "Ready", history_state, gr.update()
job_desc = job_desc[:500]
questions = []
for p in QUESTION_PROMPTS:
full_prompt = f"Based on this job description:\n{job_desc}\n\n{p}"
q = clean_question(ask_ollama(full_prompt, temperature=0.8))
questions.append(q)
session = {
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
"job_snippet": job_desc[:60] + "...",
"questions": questions,
"answers": ["", "", ""],
"scores": ["", "", ""],
"numeric_scores": [],
}
history_state = history_state or []
history_state.append(session)
save_history(history_state) # πΎ Save to disk
tips_md = build_tips(job_desc)
return questions[0], "0", "Question 1 / 3", history_state, gr.update(value=tips_md)
# ββ Answer scoring βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def score_answer(answer, q_index_str, history_state):
if not answer or len(answer.strip()) < 15:
return "Please write a longer answer (15+ characters).", history_state
idx = int(q_index_str) if q_index_str else 0
answer = answer[:500]
prompt = f"""You are a strict interview coach evaluating a candidate's spoken interview answer.
Candidate's answer: {answer}
STEP 1 β Relevance check:
Is this a genuine attempt at answering an interview question?
It is NOT relevant if it is: random text, mix of random text, code, gibberish, a single word, copy-pasted content, or completely off-topic, only partially meaningful or relevant.
STEP 2 -
If NOT relevant, respond with ONLY these two lines and nothing else:
Relevant: NO
Score: NIL/10
Warning: β οΈ Irrelevant response detected. Please answer the interview question properly using the STAR format.
If it IS a genuine interview answer, respond with ALL of the following lines and NOTHING else:
Relevant: YES
Score: X/10
Strength: (one sentence about what was done well)
Weakness: (one sentence about the biggest gap)
Fix: (one specific, actionable improvement the candidate can make to their spoken answer β no code)"""
feedback = ask_ollama(prompt, temperature=0.5)
# Save to history
if history_state:
last = history_state[-1]
if idx < len(last["answers"]):
last["answers"][idx] = answer[:80] + "..."
for line in feedback.splitlines():
if line.startswith("Score:"):
score_str = line.replace("Score:", "").strip()
last["scores"][idx] = score_str
# Extract numeric score
try:
numeric = float(score_str.split("/")[0].strip())
last["numeric_scores"].append(numeric)
except:
pass
break
history_state[-1] = last
save_history(history_state) # πΎ Save to disk
return feedback, history_state
# ββ Navigation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def next_question(q_index_str, answer, history_state):
"""Move to next question, returning previous Q+A for the review panel."""
idx = int(q_index_str) if q_index_str else 0
if not history_state:
return "Start an interview first.", "", str(idx), "No session", history_state, "", "", ""
session = history_state[-1]
questions = session["questions"]
next_idx = idx + 1
# Capture what the user just answered (for the prev panel)
prev_q = questions[idx]
prev_a = answer or "(no answer given)"
prev_score = session["scores"][idx] if session["scores"][idx] else "(no feedback yet)"
if next_idx >= len(questions):
session_log = render_session_log(session, up_to=idx)
return (
"β All 3 questions complete! Check your history.",
"", # clear answer box
str(idx),
"Interview Complete π",
history_state,
prev_q,
prev_a,
session_log,
)
session_log = render_session_log(session, up_to=idx)
return (
questions[next_idx],
"", # clear answer box
str(next_idx),
f"Question {next_idx + 1} / 3",
history_state,
prev_q,
prev_a,
session_log,
)
def render_session_log(session, up_to):
"""Render completed Q+A+Score pairs for the current session (up to index up\_to)."""
if up_to < 0:
return "No completed questions yet."
lines = []
for i in range(up_to + 1):
q = session["questions"][i]
a = session["answers"][i] or "(no answer saved)"
sc = session["scores"][i] or "(no feedback yet)"
lines.append(f"**Q{i+1}:** {q}")
lines.append(f"*Your answer:* {a}")
lines.append(f"*Score:* {sc}")
lines.append("")
return "\n".join(lines)
# ββ History rendering ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def compute_stats(history):
"""Compute overall stats across all sessions"""
all_scores = []
for s in history:
for score in s.get("numeric_scores", []):
all_scores.append(score)
if not all_scores:
return None
avg = sum(all_scores) / len(all_scores)
best = max(all_scores)
# Trend: compare first half vs second half
trend = ""
if len(all_scores) >= 4:
mid = len(all_scores) // 2
first_avg = sum(all_scores[:mid]) / mid
second_avg = sum(all_scores[mid:]) / (len(all_scores) - mid)
diff = second_avg - first_avg
if diff > 0.5:
trend = "π Improving!"
elif diff < -0.5:
trend = "π Declining β practice more"
else:
trend = "β‘οΈ Consistent"
return {
"total_sessions": len(history),
"total_answers": len(all_scores),
"avg_score": round(avg, 1),
"best_score": round(best, 1),
"trend": trend
}
def render_history(history_state):
"""Render full history with stats"""
history = history_state or []
# Load from disk too (in case state and disk diverge)
disk_history = load_history()
if len(disk_history) > len(history):
history = disk_history
if not history:
return "No sessions yet. Start your first interview above!"
lines = []
# Stats block
stats = compute_stats(history)
if stats:
lines.append("## π Your Progress")
lines.append(f"| Metric | Value |")
lines.append(f"|--------|-------|")
lines.append(f"| Total Sessions | {stats['total_sessions']} |")
lines.append(f"| Total Answers | {stats['total_answers']} |")
lines.append(f"| Average Score | {stats['avg_score']}/10 |")
lines.append(f"| Best Score | {stats['best_score']}/10 |")
if stats['trend']:
lines.append(f"| Trend | {stats['trend']} |")
lines.append("")
# Sessions
lines.append("## π Session History")
for i, s in enumerate(reversed(history), 1):
scores_display = " | ".join(s["scores"]) if any(s["scores"]) else "No feedback yet"
avg = ""
if s.get("numeric_scores"):
avg = f" Β· Avg: {round(sum(s['numeric_scores'])/len(s['numeric_scores']), 1)}/10"
lines.append(f"### Session {len(history) - i + 1} β {s['timestamp']}{avg}")
lines.append(f"**Role:** {s['job_snippet']}")
lines.append(f"**Scores:** {scores_display}")
for j, (q, a, sc) in enumerate(zip(s["questions"], s["answers"], s["scores"]), 1):
lines.append(f"\n### **Q{j}:** {q}\n")
if a:
lines.append(f"**Answer:** {a}\n")
if sc:
lines.append(f"**Score:** {sc}\n")
lines.append("\n---")
return "\n".join(lines)
# ββ Tips & Resources βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
TIPS_DB = {
"python": {
"label": "Python / Backend",
"leetcode": [
("Two Sum", "https://leetcode.com/problems/two-sum/", "Easy"),
("LRU Cache", "https://leetcode.com/problems/lru-cache/", "Medium"),
("Word Search II", "https://leetcode.com/problems/word-search-ii/", "Hard"),
],
"concepts": ["OOP principles", "Decorators & generators", "Async / await", "REST API design"],
},
"react": {
"label": "React / Frontend",
"leetcode": [
("Valid Parentheses", "https://leetcode.com/problems/valid-parentheses/", "Easy"),
("Flatten Nested List", "https://leetcode.com/problems/flatten-nested-list-iterator/", "Medium"),
],
"concepts": ["Virtual DOM", "Hooks & state management", "Component lifecycle", "Web performance"],
},
"machine learning": {
"label": "Machine Learning",
"leetcode": [
("Find Peak Element", "https://leetcode.com/problems/find-peak-element/", "Medium"),
("Kth Largest Element", "https://leetcode.com/problems/kth-largest-element-in-an-array/", "Medium"),
],
"concepts": ["Bias-variance tradeoff", "Overfitting & regularisation", "Gradient descent", "Model evaluation metrics"],
},
"sql": {
"label": "SQL / Databases",
"leetcode": [
("Employees Earning More Than Manager", "https://leetcode.com/problems/employees-earning-more-than-their-managers/", "Easy"),
("Department Top 3 Salaries", "https://leetcode.com/problems/department-top-three-salaries/", "Hard"),
],
"concepts": ["JOINs & subqueries", "Indexing strategies", "Transactions & ACID", "Query optimisation"],
},
}
DEFAULT_TIPS = {
"label": "General Software Engineering",
"leetcode": [
("Two Sum", "https://leetcode.com/problems/two-sum/", "Easy"),
("Merge Intervals", "https://leetcode.com/problems/merge-intervals/", "Medium"),
("Trapping Rain Water", "https://leetcode.com/problems/trapping-rain-water/", "Hard"),
],
"concepts": ["STAR answer format", "System design basics", "Time & space complexity", "Behavioural questions"],
}
def build_tips(job_desc=""):
jd_lower = job_desc.lower()
matched = DEFAULT_TIPS
for key, data in TIPS_DB.items():
if key in jd_lower:
matched = data
break
lc_rows = "\n".join(
f"| [{p}]({url}) | {diff} |"
for p, url, diff in matched["leetcode"]
)
concept_rows = "\n".join(f"- β {c}" for c in matched["concepts"])
return f"""## π― Tips for: {matched['label']}
### π Key Concepts to Revise
{concept_rows}
### π» Recommended LeetCode Problems
| Problem | Difficulty |
|---------|-----------|
{lc_rows}
### π§ General Interview Advice
- Use the **STAR format** (Situation β Task β Action β Result) for behavioural questions
- Always quantify impact: *"reduced load time by 40%"* beats *"made it faster"*
- It's okay to take 30 seconds to think before answering
- Ask clarifying questions β interviewers reward curiosity
### π Useful Resources
- [Grokking the System Design Interview](https://www.educative.io/courses/grokking-the-system-design-interview)
- [NeetCode Roadmap](https://neetcode.io/roadmap)
- [Tech Interview Handbook](https://www.techinterviewhandbook.org/)
- [Pramp β Free Mock Interviews](https://www.pramp.com/)
"""
# ββ Custom CSS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700\&display=swap');
* { font-family: 'Google Sans', 'Product Sans', sans-serif !important; }
.gr-button-primary {
background: linear-gradient(135deg, #6366f1, #8b5cf6) !important;
border: none !important;
color: white !important;
font-weight: 600 !important;
transition: transform 0.15s, box-shadow 0.15s !important;
}
.gr-button-primary:hover {
transform: translateY(-2px) !important;
box-shadow: 0 6px 20px rgba(99,102,241,0.4) !important;
}
.gr-button-secondary {
border: 2px solid #6366f1 !important;
color: #6366f1 !important;
font-weight: 600 !important;
transition: all 0.15s !important;
}
.gr-button-secondary:hover {
background: #6366f1 !important;
color: white !important;
}
#progress_box textarea {
font-weight: 700 !important;
font-size: 1rem !important;
color: #6366f1 !important;
text-align: center !important;
}
#feedback_box textarea {
font-size: 0.95rem !important;
line-height: 1.6 !important;
}
.tab-nav button { font-weight: 600 !important; }
/* ββ Animated blob background ββ */
body {
background: #0d0d1f !important;
}
gradio-app {
background: transparent !important;
}
.gradio-container {
background: transparent !important;
}
"""
# ββ Build UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Blocks(
title="AI Interview Coach",
theme=gr.themes.Soft(
primary_hue=gr.themes.colors.violet,
secondary_hue=gr.themes.colors.purple,
neutral_hue=gr.themes.colors.slate,
font=[gr.themes.GoogleFont("Google Sans"), "ui-sans-serif", "sans-serif"],
),
css=CUSTOM_CSS
) as demo:
# Load history from disk on startup
history_state = gr.State(load_history())
q_index = gr.State("0")
gr.HTML("""
<div style="text-align:center; padding: 1.8rem 0 0.8rem;">
<h1 style="font-size:2.2rem; font-weight:800; margin:0; font-size: 2.2rem;
background: linear-gradient(135deg,#6366f1,#a78bfa);
-webkit-background-clip:text; -webkit-text-fill-color:transparent;">
AI Interview Coach
</h1>
<p style="color:#64748b; margin-top:0.4rem; font-size:1rem;">
<b>Practice Β· Get Feedback Β· Improve </b><br>
<span style="font-size:0.8rem; color:#808080;">Powered by <span style="color:#f97316; font-weight:600;">Mistral 7B
</p>
</div>
""")
gr.HTML("""
<style>
@keyframes floatBlob1 {
0% { transform: translate(0px, 0px); }
33% { transform: translate(-30px, -40px); }
66% { transform: translate(40px, -20px); }
100% { transform: translate(0px, 0px); }
}
@keyframes floatBlob2 {
0% { transform: translate(0px, 0px); }
33% { transform: translate(50px, 30px); }
66% { transform: translate(-30px, 50px); }
100% { transform: translate(0px, 0px); }
}
@keyframes floatBlob3 {
0% { transform: translate(0px, 0px); }
33% { transform: translate(-40px, 50px); }
66% { transform: translate(30px, -40px); }
100% { transform: translate(0px, 0px); }
}
</style>
<div style="
position: fixed; width: 400px; height: 300px;
border-radius: 50%; background: #6366f1;
filter: blur(80px); opacity: 0.35;
top: -100px; left: -100px;
z-index: 0; pointer-events: none;
animation: floatBlob1 20s ease-in-out infinite;
"></div>
<div style="
position: fixed; width: 500px; height: 400px;
border-radius: 50%; background: #8b5cf6;
filter: blur(100px); opacity: 0.25;
top: 50%; right: -200px;
z-index: 0; pointer-events: none;
animation: floatBlob2 25s ease-in-out infinite;
"></div>
<div style="
position: fixed; width: 600px; height: 350px;
border-radius: 50%; background: #f97316;
filter: blur(90px); opacity: 0.2;
bottom: -100px; left: 100px;
z-index: 0; pointer-events: none;
animation: floatBlob3 15s ease-in-out infinite;
"></div>
""")
with gr.Tabs():
# ββ Tab 1: Practice βββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Tab("π― Practice"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### π Step 1 β Paste the Job Description")
job_desc_box = gr.Textbox(
label="Job Description",
lines=6,
placeholder="Paste from LinkedIn, company careers page, etc.",
)
start_btn = gr.Button("π Start Interview", variant="primary", size="lg")
with gr.Column(scale=1):
gr.Markdown("### π¬ Step 2 β Answer Questions")
progress_box = gr.Textbox(
label="Progress",
value="Ready to start",
interactive=False,
elem_id="progress_box",
)
question_box = gr.Textbox(
label="Interview Question",
lines=3,
interactive=False,
)
answer_box = gr.Textbox(
label="Your Answer",
lines=5,
placeholder="Be specific β use the STAR format (Situation, Task, Action, Result).",
)
with gr.Row():
feedback_btn = gr.Button("π Get Feedback", variant="primary")
next_btn = gr.Button("β‘οΈ Next Question", variant="secondary")
# ββ Previous answer review panel ββββββββββββββββββββββββββββββββββ
with gr.Accordion("π Previous Question Review", open=False) as prev_accordion:
with gr.Row():
prev_question_box = gr.Textbox(
label="Previous Question",
interactive=False,
lines=2,
placeholder="Will show the last question after you click Next Question...",
)
prev_answer_box = gr.Textbox(
label="Your Previous Answer",
interactive=False,
lines=3,
placeholder="Will show your last answer here...",
)
# ββ Full session log accordion βββββββββββββββββββββββββββββββββββββ
with gr.Accordion("π This Session's Log", open=False):
session_log_display = gr.Markdown("Complete questions to see your session log here.")
gr.Markdown("### π Step 3 β Coach Feedback")
feedback_box = gr.Textbox(
label="AI Coach Feedback",
interactive=False,
lines=7,
elem_id="feedback_box",
)
# ββ Tab 2: History ββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Tab("π History & Progress"):
gr.Markdown("""
### Your Interview Sessions
History is **saved to disk** β persists across restarts! πΎ
""")
with gr.Row():
refresh_btn = gr.Button("π Refresh History", variant="secondary")
clear_btn = gr.Button("ποΈ Clear History", variant="secondary")
history_display = gr.Markdown(
render_history(load_history()) # Load from disk on startup
)
# ββ Tab 3: Tips βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Tab("π‘ Tips & Resources"):
tips_display = gr.Markdown(build_tips())
# ββ Wire up events βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
start_btn.click(
fn=generate_all_questions,
inputs=[job_desc_box, history_state],
outputs=[question_box, q_index, progress_box, history_state, tips_display],
).then(
fn=render_history,
inputs=[history_state],
outputs=[history_display],
)
feedback_btn.click(
fn=score_answer,
inputs=[answer_box, q_index, history_state],
outputs=[feedback_box, history_state],
).then(
fn=render_history,
inputs=[history_state],
outputs=[history_display],
)
next_btn.click(
fn=next_question,
inputs=[q_index, answer_box, history_state],
outputs=[question_box, answer_box, q_index, progress_box, history_state, prev_question_box, prev_answer_box, session_log_display],
)
refresh_btn.click(
fn=render_history,
inputs=[history_state],
outputs=[history_display],
)
def clear_history(history_state):
"""Clear all history"""
if os.path.exists(HISTORY_FILE):
os.remove(HISTORY_FILE)
return [], "History cleared! Start a new interview above."
clear_btn.click(
fn=clear_history,
inputs=[history_state],
outputs=[history_state, history_display],
)
demo.launch(share = True)