Interview Simulator: Personalized Mock Interviews Tool

Jun 15, 2026
14 min read

AI Insights

Powered by GPT-4o-mini

Verified Context: interview-simulator-personalized-mock-interviews-tool
Quick Answer

AI-powered mock interviews that generate questions from a user's reading history, scale difficulty based on performance, and provide structured feedback with scoring.

Quick Summary

Discover the Interview Simulator that adapts to your reading history, generating tailored mock interviews to boost your coding skills.

The Interview Simulator

The Interview Simulator generates personalized mock interviews based on what the user has read. If you've completed posts about Python classes and FastAPI routes, the simulator asks you Python OOP questions and API design questions — at a difficulty level that adapts to your performance.


Data Model

python
class InterviewSession(Base):
    __tablename__ = "interview_sessions"

    id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
    user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"))
    status: Mapped[str] = mapped_column(default="in_progress")
    overall_score: Mapped[float | None]
    question_count: Mapped[int] = mapped_column(default=0)
    current_question: Mapped[int] = mapped_column(default=0)
    difficulty: Mapped[str] = mapped_column(default="medium")
    topics: Mapped[list[str]] = mapped_column(JSONB, default=list)
    created_at: Mapped[datetime] = mapped_column(default=func.now())
    completed_at: Mapped[datetime | None]

Question Generation

When a user starts an interview, the system selects topics based on their reading history:

python
async def generate_interview(user_id: str) -> InterviewSession:
    completed_posts = await db.execute(
        select(Post.title, Post.tags).join(
            UserProgress, UserProgress.post_id == Post.id
        ).where(
            UserProgress.user_id == user_id,
            UserProgress.completed_at.isnot(None),
        )
    )

    topic_counts: dict[str, int] = {}
    for title, tags in completed_posts:
        for tag in tags:
            topic_counts[tag.name] = topic_counts.get(tag.name, 0) + 1

    # Select top 3 topics the user has read most
    top_topics = sorted(topic_counts, key=topic_counts.get, reverse=True)[:3]

    session = InterviewSession(
        user_id=user_id,
        topics=top_topics,
        difficulty="medium",
    )
    db.add(session)
    await generate_next_question(session, db)
    return session

Adaptive Difficulty

After each answer, the difficulty adjusts based on the answer's quality score:

python
async def adjust_difficulty(session: InterviewSession, score: float):
    diffs = ["easy", "medium", "hard"]
    current_idx = diffs.index(session.difficulty)

    if score >= 0.8:
        session.difficulty = diffs[min(current_idx + 1, len(diffs) - 1)]
    elif score <= 0.4:
        session.difficulty = diffs[max(current_idx - 1, 0)]
    # Otherwise, stay at current difficulty

The scoring rubric is simple:

ScoreCriteria
0.0 - 0.3No relevant concepts mentioned, major errors
0.3 - 0.5Some relevant concepts but incomplete
0.5 - 0.7Correct concepts, partial explanation
0.7 - 0.9Complete, correct answer with good explanation
0.9 - 1.0Comprehensive answer with examples and edge cases

The Prompt Chain

Instead of one large prompt, the system uses a chain of smaller prompts:

Step 1: Generate Question

text
Context:
The user is being interviewed on {topic} at {difficulty} level.
They have read: {list_of_posts}
Generate one technical interview question.
Output: {{"question": "..."}}

Step 2: Evaluate Answer

text
Question: {question}
User's answer: {answer}
Expected knowledge areas: {topic_keywords}

Evaluate the answer on:
1. Correctness (0-1): Is the answer technically correct?
2. Completeness (0-1): Does it cover all relevant aspects?  
3. Clarity (0-1): Is it well-structured and explained?

Output: {{"correctness": 0.8, "completeness": 0.6, "clarity": 0.9, "feedback": "..."}}

Step 3: Generate Follow-Up

text
Context: The user answered a {difficulty} question about {topic}.
They scored {score}. 
Generate a follow-up question that tests deeper understanding.
Output: {{"question": "..."}}

Each prompt is independent and stateless. The session state (current question, difficulty, topic) is stored in the database, not in the prompt.


Frontend

The interview UI is a multi-step form:

tsx
function InterviewSimulator() {
    const [session, setSession] = useState<InterviewSession | null>(null);
    const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null);
    const [answer, setAnswer] = useState("");
    const [feedback, setFeedback] = useState<Feedback | null>(null);

    if (!session) return <StartInterview onStart={handleStart} />;

    return (
        <div className="max-w-2xl mx-auto space-y-8">
            <ProgressBar current={session.current_question} total={session.question_count} />

            {currentQuestion && !feedback && (
                <div className="card p-6">
                    <p className="text-[10px] font-bold text-amber-500 uppercase tracking-widest mb-2">
                        Question {session.current_question} of {session.question_count}
                    </p>
                    <p className="text-lg font-bold text-white/90 mb-6">
                        {currentQuestion.text}
                    </p>
                    <textarea
                        value={answer}
                        onChange={(e) => setAnswer(e.target.value)}
                        className="w-full h-32 p-4 rounded-xl bg-white/[0.03] border border-white/[0.08]
                                   text-sm text-white/80 resize-none focus:outline-none focus:border-amber-500/40"
                        placeholder="Type your answer..."
                    />
                    <div className="flex justify-end mt-4 gap-3">
                        <Button variant="secondary" onClick={handleSkip}>
                            Skip
                        </Button>
                        <Button onClick={handleSubmit}>
                            Submit Answer
                        </Button>
                    </div>
                </div>
            )}

            {feedback && (
                <FeedbackCard feedback={feedback} onNext={handleNext} />
            )}
        </div>
    );
}

The feedback card shows the user how they performed:

tsx
function FeedbackCard({ feedback, onNext }: { feedback: Feedback; onNext: () => void }) {
    const overall = (feedback.correctness + feedback.completeness + feedback.clarity) / 3;

    return (
        <div className="card p-6 space-y-4">
            <div className="flex items-center justify-between">
                <h3 className="font-bold text-white/90">Feedback</h3>
                <span className={`text-lg font-bold ${
                    overall >= 0.7 ? "text-emerald-400" :
                    overall >= 0.4 ? "text-amber-400" : "text-rose-400"
                }`}>
                    {Math.round(overall * 100)}%
                </span>
            </div>

            <ScoreBar label="Correctness" score={feedback.correctness} />
            <ScoreBar label="Completeness" score={feedback.completeness} />
            <ScoreBar label="Clarity" score={feedback.clarity} />

            <p className="text-sm text-white/60 bg-white/[0.03] p-4 rounded-xl">
                {feedback.feedback}
            </p>

            <Button onClick={onNext}>
                {isLastQuestion ? "See Results" : "Next Question"}
            </Button>
        </div>
    );
}

Results & Scoring

After all questions are answered, the system computes an overall score and stores it:

python
async def complete_interview(session_id: str, db):
    session = await db.get(InterviewSession, session_id)
    session.status = "completed"
    session.completed_at = datetime.utcnow()

    answers = await db.execute(
        select(InterviewAnswer).where(
            InterviewAnswer.session_id == session_id
        )
    )
    scores = [a.score for a in answers.scalars()]
    session.overall_score = sum(scores) / len(scores) if scores else 0

    if session.overall_score >= 0.8:
        badge_name = "Interview Master"
    elif session.overall_score >= 0.5:
        badge_name = "Interview Ready"

    await award_badge_if_not_owned(session.user_id, badge_name, db)

Explanation

  • Updates an interview session status to "completed" and records completion timestamp
  • Retrieves all answers for the session and calculates the average score across all responses
  • Assigns a badge name based on the calculated overall score (Interview Master for 80%+, Interview Ready for 50%+)
  • Awards the appropriate badge to the user if they haven't already earned it
  • Uses async database operations to fetch session data and execute queries efficiently

The results page shows:

tsx
function InterviewResults({ session, answers }: Props) {
    return (
        <div className="space-y-6">
            <div className="text-center py-8">
                <span className="text-5xl font-bold text-amber-500">
                    {Math.round(session.overall_score * 100)}%
                </span>
                <p className="text-sm text-white/50 mt-2">
                    Across {answers.length} questions
                </p>
            </div>
            <div className="space-y-4">
                {answers.map((a, i) => (
                    <div key={a.id} className="card p-4">
                        <div className="flex justify-between items-start">
                            <p className="text-sm font-bold text-white/80">{a.question}</p>
                            <span className="text-xs font-bold text-amber-500">
                                {Math.round(a.score * 100)}%
                            </span>
                        </div>
                        <p className="text-xs text-white/40 mt-2">{a.feedback}</p>
                    </div>
                ))}
            </div>
        </div>
    );
}

What's Next

The next post covers the Knowledge Graph — how D3.js renders a force-directed graph of reading history, how topics are extracted from completed posts, and how edge weights are computed from co-occurrence.


Built with FastAPI, GPT-4o-mini, and zero third-party assessment tools.


Next in this series: Creating a Dynamic Knowledge Graph from Reading History →

Frequently Asked Questions

How does the Interview Simulator select topics for mock interviews?
The system selects topics based on the user's reading history, focusing on the top three topics the user has read the most.
What happens to the difficulty level after each answer in the Interview Simulator?
The difficulty adjusts based on the quality score of the answer, increasing if the score is high and decreasing if the score is low.
What is the purpose of the FeedbackCard in the Interview Simulator?
The FeedbackCard shows the user how they performed by providing feedback on correctness, completeness, and clarity, and allows them to proceed to the next question or see results.
What data model is used for an interview session in the Interview Simulator?
The data model for an interview session includes fields like id, userid, status, overallscore, questioncount, currentquestion, difficulty, topics, createdat, and completedat.

Related Work

See how this thinking shows up in shipped systems.