Interviews built for neurodivergent job seekers
WebMoti-Employ is an app that uses AI and eyetracking to deliver real-time feedback during virtual interviews, helping neurodivergent candidates stay focused.
- Stack
- Real-time feedback pipeline
- Setup
- CI/CD
- Renovate
- Desktop App
- Deploying
- VSCode Auto Formatting Code
- Possible errors and solutions
- PNPM - Package manager
- TypeScript
- Zod - Validation
- ESLint - Formatting and linting (@antfu/eslint-config)
- Vitest - Unit testing
Client:
- React
- Vite
- Mantine - UI components
- Tanstack Router - Routing
- Tanstack Query - Server state
- Zustand - Client state
- Better-Auth - Authentication
- Zoom Video
- Speechmatics - Transcription
- Mediapipe - Face detection
- Playwright - E2E testing
Server:
- Hono
- Kysely - SQL query builder
- Groq - AI analysis
- Cloudflare Durable Objects - Stateful websockets
- Cloudflare Hyperdrive
Desktop App:
- Electron
- Electron-Builder
- Python
- Socket.IO
- Tobii Pro SDK - Eye tracking
Hosting:
- Neon - Postgres database
- Cloudflare Workers - Serverless backend
- Vercel - Client hosting
Real-time feedback is done using eye tracking and AI analysis.
Our system provides 4 distinct feedback notifications:
- Eyetracking: If the candidate is not looking at the interviewer for a while, we progressively blur the video background to shift focus back to the interviewer. There is also a text notification.
- Question hints: Two short AI generated hints to help answer the current question. Both the candidate and interviewer will see these hints.
- Off topic feedback: If the candidate goes off topic, we show a text notification to remind them.
- Stopwatch: When a question is asked, a stopwatch starts counting up to show how much time has passed for the current question.
- Electron spawns a Python server that runs eyetracking continuously using the Tobii SDK
- Mediapipe face detection runs in the browser to determine the dynamic area of interest (AOI)
- The AOI bounding box is sent through IPC to Electron
- Electron sends the AOI to the Python server through Socket.IO
- The Python server checks if the user is looking at the AOI using the Tobii eyetracking data
- The Python server sends this eyetracking feedback to Electron
- Electron sends this to the browser
- Repeat from step 2
- User starts talking
- Browser sends speech to Speechmatics over websocket
- Speechmatics sends back transcribed words
- Words are buffered in two cases to avoid spamming the AI with single words:
- 5 or more words are accumulated in the buffer before sending
- The user stops talking (all words in buffer are sent)
- Transcript words are sent through a websocket to a Cloudflare Durable Object that allows multiple persistent connections
- The durable object sends transcripts to Groq for AI analysis
- Groq sends back a JSON with feedback
- The durable object broadcasts this feedback to all connected clients
- Repeat from step 1
-
Install pnpm
npm install -g pnpm
-
(optional) Update pnpm
# run this when you want to update pnpm to the latest version pnpm self-update -
Install dependencies
pnpm install
-
Setup ESLint
This project uses ESLint to have better code quality and prevent bugs. This should be configured depending on your IDE so you can see issues as you write code. If you use VSCode, follow these steps to set it up.
-
Run the project locally
# this will run both the client and server in parallel pnpm run dev
We use Github actions for CI/CD automation. There are five main workflows:
Build and Deploy- Lints code
- Checks for Typescript errors
- Builds code
- Runs tests
- Activates the deploy workflows
Vercel Deploy- Checks for changes in
client/ - If changed, deploys client to Vercel
- Creates a Github deployment
- Checks for changes in
Cloudflare Deploy- Checks for changes in
server/ - If changed, deploys server to Cloudflare
- Checks for changes in
Electron Package- Checks for changes in
electron/package.jsonversionfield - If changed, packages the electron app for Windows, Linux, and MacOS
- Creates a Github release and uploads the packaged files
- Checks for changes in
Python CI- Runs when files in
electron/python/change - Lints Python code with Ruff
- Runs Python tests with pytest
- Runs when files in
GitHub Actions Security Analysis- Checks GitHub Actions workflows for security issues using
zizmor - You can run zizmor locally too:
uvx zizmor .\.github\
- Checks GitHub Actions workflows for security issues using
Renovate checks for new dependencies for all package.json files in the project. It also handles Python dependencies in electron/python/pyproject.toml.
npm updates are grouped into PRs by type:
minor updates: all non-eslint minor bumps grouped togetherpatch updates: all non-eslint patch bumps grouped togethereslint: eslint-related minor and patch bumps grouped together- Major updates are separate PRs since they might break the code
Steps to update dependencies:
- Go to
Pull requests - Click one
- Check that all checks passed
- Use the dropdown to switch to
Squash and merge - Click
Squash and merge
The desktop app is a cross platform Electron app that loads the hosted React client and runs a Python server in the background. This allows us to connect to the eyetracker using the Tobii Pro Python SDK.
More info about desktop app here
The client and server are deployed separately.
Note
After the eslint extension server runs for a while, it gets slower when formatting code. To fix this, run ctrl + shift + p > ESLint: Restart ESLint Server
https://github.com/antfu/eslint-config?tab=readme-ov-file#ide-support-auto-fix-on-save
-
Install VS Code ESLint extension
-
Add the following settings to your
.vscode/settings.json:
When you run the Vite dev server in Firefox and you join an interview, it will fail with this error in the console:
WebRTC: ICE failed, add a STUN server and see about:webrtc for more details
Solution: Use Chrome instead for developing.
This problem could manifest in many different ways, for example zoom video not working or a strange error in the console. This could happen after the dependencies were changed, maybe with Renovate or manually.
Solution: Delete pnpm-lock.yaml, run pnpm clean-modules, and run pnpm i to regenerate the lockfile.


{ // Disable the default formatter, use eslint instead "prettier.enable": false, "editor.formatOnSave": false, // Auto fix "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.organizeImports": "never" }, // Only warn for stylistic rules "eslint.rules.customizations": [ { "rule": "style/*", "severity": "warn" }, { "rule": "format/*", "severity": "warn" }, { "rule": "*-indent", "severity": "warn" }, { "rule": "*-spacing", "severity": "warn" }, { "rule": "*-spaces", "severity": "warn" }, { "rule": "*-order", "severity": "warn" }, { "rule": "*-dangle", "severity": "warn" }, { "rule": "*-newline", "severity": "warn" }, { "rule": "*quotes", "severity": "warn" }, { "rule": "*semi", "severity": "warn" } ], // Enable eslint for all supported languages "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "html", "markdown", "json", "jsonc", "yaml", "toml", "xml", "gql", "graphql", "astro", "svelte", "css", "less", "scss", "pcss", "postcss" ] }