Train smarter. Interview sharper.
A React Native + Expo mobile app that runs realistic AI-driven mock interviews end to end. Pick a track, get questions generated by Gemini, record spoken answers (transcribed by Gemini), and finish with a scored AI feedback report — all on device, with sessions persisted locally.
| Home | Live Interview | Recording & Transcript |
![]() |
![]() |
![]() |
| Score & Subscores | Strengths & Improvements | |
![]() |
![]() |
- 4 interview tracks — React Native, HR / Behavioral, DSA, System Design.
- AI-generated questions — Gemini produces fresh, schema-validated questions per session, with a built-in fallback to a curated sample set if the API is unreachable.
- Voice answers + AI transcription — Tap to record, Gemini transcribes the audio, and you can still edit the text before submitting.
- Live session timer with pause / resume.
- Holistic AI feedback — overall score (0–100) plus subscores for technical knowledge, communication, and confidence, a written summary, 3–5 strengths and 3–5 concrete improvements.
- Local session history — every completed interview is saved to AsyncStorage so you can revisit past feedback.
- Polished dark UI with NativeWind, gradient backgrounds, animated transitions, and a score ring that adapts its color to performance.
- Cancellable AI requests — leaving a screen or retrying aborts in-flight Gemini calls cleanly.
- Resilient networking — automatic retries with exponential backoff on 429 / 5xx responses.
| Layer | Choice |
|---|---|
| Runtime | Expo SDK 56, React Native 0.85, React 19 |
| Language | TypeScript (strict mode, @/* path alias) |
| Styling | NativeWind v4 (Tailwind CSS for RN) |
| Navigation | @react-navigation/native-stack v7 |
| State | Zustand + persist middleware |
| Storage | @react-native-async-storage/async-storage |
| Audio | expo-audio (new SDK 56 API) |
| File I/O | expo-file-system (new File API) |
| AI | Google Gemini (gemini-2.5-flash by default) via axios |
| Animations | react-native-reanimated v4 |
| Icons | @expo/vector-icons (Ionicons) |
- Node.js 20+
- npm (or your preferred package manager)
- Expo Go on a physical device, or Xcode (iOS Simulator) / Android Studio (Emulator)
- A Google Gemini API key from Google AI Studio
Note: Audio recording does not work on the iOS Simulator. Use a real device or an Android emulator with a working mic for the recording flow.
git clone <repo-url>
cd InterviewSimAI
npm installCreate a .env file in the project root (use .env.example as a template):
EXPO_PUBLIC_GEMINI_API_KEY=your-gemini-api-key
EXPO_PUBLIC_GEMINI_MODEL=gemini-2.5-flashEXPO_PUBLIC_* variables are inlined into the JS bundle at build time, so restart Expo after changing .env.
npm start # Expo dev server (pick a target from the menu)
npm run ios # iOS Simulator
npm run android # Android Emulator
npm run web # Web buildHome ──▶ Interview ──▶ Feedback
│ │ │
│ ├─ Gemini: generateInterviewQuestions
│ ├─ expo-audio: record answer
│ ├─ Gemini: transcribeAudioRecording
│ └─ Gemini: evaluateSession (on finish)
│
└─ Recent session from local history
- Home — choose a category and tap Start.
- Interview — on mount, the app requests 3 fresh questions from Gemini. The "AI interviewer" speaks the question, then you tap the mic, record your answer, and Gemini transcribes it. You can edit the transcript before submitting.
- Feedback — after the last question, the full Q&A is sent to Gemini for a holistic evaluation. The session is saved to local history and you land on the feedback screen.
.
├── App.tsx # Root: providers + navigator + history hydration
├── index.ts # registerRootComponent(App)
├── app.json # Expo config (incl. expo-audio mic permission)
├── babel.config.js # babel-preset-expo + nativewind/babel
├── metro.config.js # withNativeWind(...)
├── tailwind.config.js # Design tokens (brand / accent / ink palettes)
├── global.css # Tailwind directives
├── .env.example # Required env vars
├── assets/ # App icons, splash, favicon
├── docs/screenshots/ # README screenshots
└── src/
├── components/ # 13 reusable UI components (cards, buttons, waveform, …)
├── constants/ # theme, layout helpers, mockQuestions fallback
├── hooks/
│ └── useAudioRecorder.ts # expo-audio wrapper (permissions, mode, lifecycle)
├── navigation/
│ └── AppNavigator.tsx # Native stack: Home → Interview → Feedback
├── screens/
│ ├── HomeScreen.tsx
│ ├── InterviewScreen.tsx
│ └── FeedbackScreen.tsx
├── services/
│ ├── aiService.ts # Gemini client (questions, transcription, feedback)
│ ├── historyService.ts # Persisted session history
│ └── storage.ts # Typed AsyncStorage wrapper
├── store/
│ └── interviewStore.ts # Zustand store + selectors
├── types/ # Domain + navigation types
└── utils/
└── score.ts # Score tone → color/label maps
The AI service is a single, dependency-light module that:
- Reads
EXPO_PUBLIC_GEMINI_API_KEY/EXPO_PUBLIC_GEMINI_MODELand throws a descriptiveGeminiServiceErrorif the key is missing. - Uses Gemini's JSON response schemas (
responseMimeType: 'application/json'+responseSchema) so questions and feedback come back as strict, typed JSON — no fragile regex parsing. - Sends audio inline via
inlineData(base64), capped at 14 MB to stay within the inline-data limit. - Retries on
429 / 500 / 502 / 503 / 504with exponential backoff (up to 3 retries). - Supports
AbortSignalfor cancel-on-unmount and retry semantics.
Exposed helpers:
| Function | Purpose |
|---|---|
generateInterviewQuestions |
Topic + difficulty → array of questions |
transcribeAudioRecording |
Audio file URI → plain-text transcript |
evaluateAnswer |
Single Q/A → scored feedback |
evaluateSession |
Full Q/A list → overall + subscored feedback |
isGeminiConfigured |
Quick env-var check |
A single Zustand store (src/store/interviewStore.ts) owns:
- Selection:
selectedCategoryId,selectedDifficulty - Live session:
currentSessionId,currentQuestions,currentQuestionIndex,currentAnswers,transcript - History:
history,historyHydrated(mirrored tohistoryService) - Loading flags:
questions | transcription | feedback - Timer:
{ isRunning, elapsedMs, startedAt }withstart / pause / tick / reset
Only the small selection slice is persisted by the store middleware (partialize). The heavier history blob is owned by historyService, which serializes through a typed, namespaced AsyncStorage wrapper (@interviewsim:interview-history/v1).
Exported selectors: selectCurrentQuestion, selectProgress, selectElapsedMs, selectAverageScore, plus a formatElapsed(ms) helper.
npm start # expo start
npm run ios # expo start --ios
npm run android # expo start --android
npm run web # expo start --web- "Missing EXPO_PUBLIC_GEMINI_API_KEY" — Add the key to
.envand restartexpo start(Metro caches env vars). - No questions appear / "Couldn't load AI questions" — Tap Use sample set in the error banner to fall back to bundled questions, or check your API key / quota.
- "Recording is not supported on the iOS Simulator" — Use a real device or an Android emulator.
- "Recording is too large to transcribe inline" — Keep individual answers under ~14 MB of audio; the app prompts you to record a shorter answer.
- History feels stale — The store hydrates history once on app start (
App.tsx). Fully reload the app or clear history from within the store to reset.
EXPO_PUBLIC_* env vars are bundled into the client app. This is fine for local development and personal use, but if you ship this anywhere public you should:
- Proxy Gemini calls through a backend you control.
- Rotate the key in Google AI Studio if it ever leaks.
- Restrict the key (HTTP referrers / API restrictions) wherever possible.
This project is provided as-is for learning and personal use. Add a license of your choice before distributing.




