Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| from groq import Groq | |
| import re | |
| from fpdf import FPDF | |
| import datetime | |
| import os | |
| # ββ Page Config βββββββββββββββββββββββββββββββββββ | |
| st.set_page_config( | |
| page_title = "InterviewGen AI", | |
| page_icon = "π―", | |
| layout = "wide" | |
| ) | |
| # ββ Custom CSS ββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <style> | |
| .main-header { | |
| font-size: 2.8rem; | |
| font-weight: 900; | |
| background: linear-gradient(90deg, #667eea, #764ba2); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-align: center; | |
| padding: 1rem 0; | |
| } | |
| .question-card { | |
| background: #f8f9fa; | |
| border-left: 5px solid #667eea; | |
| padding: 1.2rem; | |
| margin: 0.8rem 0; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .answer-card { | |
| background: linear-gradient(135deg, #e8f4f8, #f0fff4); | |
| border-left: 5px solid #2ecc71; | |
| padding: 1.2rem; | |
| margin: 0.8rem 0; | |
| border-radius: 10px; | |
| } | |
| .score-card { | |
| background: linear-gradient(135deg, #fff3cd, #ffeaa7); | |
| border-left: 5px solid #f39c12; | |
| padding: 1.2rem; | |
| margin: 0.8rem 0; | |
| border-radius: 10px; | |
| } | |
| .metric-card { | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| color: white; | |
| padding: 1rem; | |
| border-radius: 10px; | |
| text-align: center; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββ Groq Client βββββββββββββββββββββββββββββββββββ | |
| GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") | |
| client = Groq(api_key=GROQ_API_KEY) | |
| # ββ Helper Functions ββββββββββββββββββββββββββββββ | |
| def generate_questions(role, difficulty, q_type, num, job_desc=""): | |
| job_context = f"Job Description: {job_desc[:500]}" if job_desc else "" | |
| prompt = f"""You are a senior technical interviewer at a top tech company. | |
| {job_context} | |
| Generate exactly {num} {difficulty} level {q_type} interview questions for a {role}. | |
| Format EXACTLY like this: | |
| Q1: [question] | |
| A1: [detailed answer] | |
| Q2: [question] | |
| A2: [detailed answer] | |
| Only output questions and answers. Nothing else.""" | |
| response = client.chat.completions.create( | |
| model = "llama-3.3-70b-versatile", | |
| messages = [{"role": "user", "content": prompt}], | |
| temperature = 0.7, | |
| max_tokens = 2000 | |
| ) | |
| return response.choices[0].message.content | |
| def evaluate_answer(question, user_answer, correct_answer): | |
| prompt = f"""You are a technical interviewer evaluating a candidate answer. | |
| Question: {question} | |
| Candidate Answer: {user_answer} | |
| Expected Answer: {correct_answer} | |
| Evaluate the candidate answer and provide: | |
| 1. Score: X/10 | |
| 2. Strengths: what they got right | |
| 3. Improvements: what they missed | |
| 4. Verdict: Pass/Fail | |
| Be concise and professional.""" | |
| response = client.chat.completions.create( | |
| model = "llama-3.3-70b-versatile", | |
| messages = [{"role": "user", "content": prompt}], | |
| temperature = 0.3, | |
| max_tokens = 500 | |
| ) | |
| return response.choices[0].message.content | |
| def parse_questions(text): | |
| qa_pairs = [] | |
| blocks = re.split(r"Q\d+:", text) | |
| blocks = [b.strip() for b in blocks if b.strip()] | |
| for block in blocks: | |
| if re.search(r"A\d+:", block): | |
| parts = re.split(r"A\d+:", block, maxsplit=1) | |
| question = parts[0].strip() | |
| answer = parts[1].strip() if len(parts) > 1 else "N/A" | |
| else: | |
| question = block.strip() | |
| answer = "N/A" | |
| qa_pairs.append({"question": question, "answer": answer}) | |
| return qa_pairs | |
| # ββ Session State Init ββββββββββββββββββββββββββββ | |
| if "history" not in st.session_state: st.session_state.history = [] | |
| if "total_generated" not in st.session_state: st.session_state.total_generated = 0 | |
| if "parsed_qa" not in st.session_state: st.session_state.parsed_qa = [] | |
| if "mock_index" not in st.session_state: st.session_state.mock_index = 0 | |
| if "mock_scores" not in st.session_state: st.session_state.mock_scores = [] | |
| if "mock_active" not in st.session_state: st.session_state.mock_active = False | |
| # ββ Header ββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown("<p class=\'main-header\'>π― InterviewGen AI</p>", unsafe_allow_html=True) | |
| st.markdown("<p style=\'text-align:center;color:gray;font-size:1.1rem;\'>Professional Interview Preparation Powered by LLaMA-3.3 & Groq</p>", unsafe_allow_html=True) | |
| st.divider() | |
| # ββ Top Metrics βββββββββββββββββββββββββββββββββββ | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: st.metric("Questions Generated", st.session_state.total_generated) | |
| with col2: st.metric("Sessions", len(st.session_state.history)) | |
| with col3: st.metric("Mock Interviews", len(st.session_state.mock_scores)) | |
| with col4: | |
| avg = sum(st.session_state.mock_scores) / len(st.session_state.mock_scores) if st.session_state.mock_scores else 0 | |
| st.metric("Avg Mock Score", f"{avg:.1f}/10") | |
| st.divider() | |
| # ββ Sidebar βββββββββββββββββββββββββββββββββββββββ | |
| with st.sidebar: | |
| st.markdown("## Settings") | |
| role = st.selectbox( | |
| "Select Role", | |
| ["Python Developer", "Data Scientist", | |
| "Software Engineer", "ML Engineer", | |
| "DevOps Engineer", "Full Stack Developer", | |
| "Data Analyst", "Backend Developer", | |
| "Frontend Developer", "AI Engineer"] | |
| ) | |
| difficulty = st.select_slider( | |
| "Difficulty Level", | |
| options=["Junior", "Mid-Level", "Senior"] | |
| ) | |
| num_questions = st.slider( | |
| "Number of Questions", | |
| min_value=1, max_value=10, value=5 | |
| ) | |
| show_answers = st.toggle("Show Answers", value=True) | |
| st.divider() | |
| st.markdown("### Paste Job Description (Optional)") | |
| job_desc = st.text_area( | |
| "Job Description", | |
| placeholder="Paste job description here for targeted questions...", | |
| height=150 | |
| ) | |
| # ββ Tabs ββββββββββββββββββββββββββββββββββββββββββ | |
| tab1, tab2, tab3 = st.tabs([ | |
| "π Generate Questions", | |
| "π― Mock Interview Mode", | |
| "π History" | |
| ]) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 1 β Generate Questions | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab1: | |
| q_type = st.radio( | |
| "Question Type", | |
| ["Technical", "Behavioral", "Mixed"], | |
| horizontal=True | |
| ) | |
| generate_btn = st.button( | |
| "π Generate Interview Questions", | |
| use_container_width=True | |
| ) | |
| if generate_btn: | |
| with st.spinner("LLaMA-3.3 is generating questions..."): | |
| raw = generate_questions(role, difficulty, q_type, num_questions, job_desc) | |
| parsed = parse_questions(raw) | |
| st.session_state.parsed_qa = parsed | |
| st.markdown(f"### {role} | {difficulty} | {q_type}") | |
| st.divider() | |
| questions = [] | |
| answers = [] | |
| for i, qa in enumerate(parsed): | |
| q = qa["question"] | |
| a = qa["answer"] | |
| questions.append(q) | |
| answers.append(a) | |
| st.info(f"**Q{i+1}.** {q}") | |
| if show_answers: | |
| st.success(f"**Answer:** {a}") | |
| st.write("") | |
| st.session_state.total_generated += len(parsed) | |
| st.session_state.history.append({ | |
| "time" : datetime.datetime.now().strftime("%H:%M:%S"), | |
| "role" : role, | |
| "difficulty": difficulty, | |
| "type" : q_type, | |
| "questions" : questions, | |
| "answers" : answers | |
| }) | |
| # ββ PDF Export ββββββββββββββββββββββββββββ | |
| try: | |
| pdf = FPDF() | |
| pdf.add_page() | |
| pdf.set_font("Arial", "B", 14) | |
| pdf.cell(190, 10, f"Interview Questions - {role}", ln=True, align="C") | |
| pdf.set_font("Arial", "", 9) | |
| pdf.cell(190, 8, f"Type: {q_type} | Difficulty: {difficulty}", ln=True, align="C") | |
| pdf.ln(4) | |
| for i, (q, a) in enumerate(zip(questions, answers)): | |
| q_c = q.encode("latin-1", "replace").decode("latin-1") | |
| a_c = a.encode("latin-1", "replace").decode("latin-1") | |
| pdf.set_font("Arial", "B", 10) | |
| pdf.multi_cell(190, 7, f"Q{i+1}. {q_c}") | |
| if show_answers: | |
| pdf.set_font("Arial", "", 9) | |
| pdf.multi_cell(190, 6, f"Answer: {a_c}") | |
| pdf.ln(2) | |
| pdf_path = "/tmp/interview_questions.pdf" | |
| pdf.output(pdf_path) | |
| with open(pdf_path, "rb") as f: | |
| st.download_button( | |
| label = "π₯ Download as PDF", | |
| data = f, | |
| file_name = f"interview_{role.replace(' ','_')}.pdf", | |
| mime = "application/pdf" | |
| ) | |
| except Exception as e: | |
| st.warning(f"PDF error: {e}") | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 2 β Mock Interview Mode | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab2: | |
| st.markdown("### π― Mock Interview Mode") | |
| st.markdown("Answer questions one by one β AI will evaluate your answers!") | |
| st.divider() | |
| if not st.session_state.parsed_qa: | |
| st.info("First generate questions in Tab 1, then come back here!") | |
| else: | |
| total_q = len(st.session_state.parsed_qa) | |
| idx = st.session_state.mock_index | |
| if idx < total_q: | |
| current_qa = st.session_state.parsed_qa[idx] | |
| st.markdown(f"**Question {idx+1} of {total_q}**") | |
| st.progress((idx) / total_q) | |
| st.markdown(f""" | |
| <div class="question-card"> | |
| <strong>Q{idx+1}. {current_qa["question"]}</strong> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| user_answer = st.text_area( | |
| "Your Answer", | |
| placeholder="Type your answer here...", | |
| height=150, | |
| key=f"answer_{idx}" | |
| ) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| submit_btn = st.button("Submit Answer", use_container_width=True) | |
| with col2: | |
| skip_btn = st.button("Skip Question", use_container_width=True) | |
| if submit_btn and user_answer: | |
| with st.spinner("AI is evaluating your answer..."): | |
| evaluation = evaluate_answer( | |
| current_qa["question"], | |
| user_answer, | |
| current_qa["answer"] | |
| ) | |
| st.markdown(f""" | |
| <div class="score-card"> | |
| <strong>AI Evaluation:</strong><br>{evaluation} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Extract score | |
| score_match = re.search(r"(\d+)/10", evaluation) | |
| if score_match: | |
| score = int(score_match.group(1)) | |
| st.session_state.mock_scores.append(score) | |
| st.session_state.mock_index += 1 | |
| st.rerun() | |
| if skip_btn: | |
| st.session_state.mock_index += 1 | |
| st.rerun() | |
| else: | |
| st.success("Mock Interview Complete!") | |
| if st.session_state.mock_scores: | |
| avg = sum(st.session_state.mock_scores) / len(st.session_state.mock_scores) | |
| st.markdown(f"### Your Final Score: {avg:.1f}/10") | |
| if avg >= 8: | |
| st.balloons() | |
| st.success("Excellent! You are ready for the interview!") | |
| elif avg >= 6: | |
| st.warning("Good performance! A little more practice needed.") | |
| else: | |
| st.error("Keep practicing! Review the answers carefully.") | |
| if st.button("Restart Mock Interview"): | |
| st.session_state.mock_index = 0 | |
| st.session_state.mock_scores = [] | |
| st.rerun() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 3 β History | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab3: | |
| st.markdown("### Question History") | |
| if not st.session_state.history: | |
| st.info("No history yet! Generate some questions first.") | |
| else: | |
| for session in reversed(st.session_state.history): | |
| with st.expander(f"{session['time']} - {session['role']} | {session['difficulty']} | {session['type']}"): | |
| for i, (q, a) in enumerate(zip(session["questions"], session["answers"])): | |
| st.markdown(f"**Q{i+1}.** {q}") | |
| if show_answers: | |
| st.markdown(f"*A: {a[:200]}...*") | |
| st.write("") | |