|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +**Interview Coder CN** (编码面试解题助手) is a desktop application that captures screenshots of coding problems and uses AI (vision models) to generate solutions in real-time. The window is invisible to screen-sharing software, making it suitable for use during coding interviews and online assessments. |
| 6 | + |
| 7 | +Key capabilities: |
| 8 | +- Global shortcuts trigger screenshot capture → AI analysis → streamed solution display |
| 9 | +- Frameless, transparent, always-on-top overlay window invisible to screen-sharing |
| 10 | +- Mouse passthrough mode (window ignores mouse events) |
| 11 | +- Multi-screenshot conversation continuity (append screenshots to existing context) |
| 12 | +- Follow-up questions within the same conversation |
| 13 | +- Configurable AI provider (OpenAI, SiliconFlow, OpenRouter, or any OpenAI-compatible API) |
| 14 | + |
| 15 | +## Tech Stack |
| 16 | + |
| 17 | +| Layer | Technology | |
| 18 | +|-------|-----------| |
| 19 | +| Framework | Electron 37 (electron-vite 4) | |
| 20 | +| Frontend | React 19, TypeScript 5.8 | |
| 21 | +| Styling | Tailwind CSS v4, shadcn/ui (New York style), Radix primitives | |
| 22 | +| State | Zustand 5 (4 stores, 2 with localStorage persistence) | |
| 23 | +| Routing | react-router v7 (HashRouter, 3 routes) | |
| 24 | +| AI | Vercel AI SDK (`ai` + `@ai-sdk/openai`), streaming via `streamText()` | |
| 25 | +| Build | electron-vite (Vite 7), electron-builder 25 | |
| 26 | +| Linting | ESLint 9 (flat config), Prettier | |
| 27 | + |
| 28 | +## Directory Structure |
| 29 | + |
| 30 | +``` |
| 31 | +src/ |
| 32 | +├── main/ # Electron main process |
| 33 | +│ ├── index.ts # App entry: lifecycle, error handling, app.whenReady() |
| 34 | +│ ├── main-window.ts # BrowserWindow creation (frameless, transparent, always-on-top) |
| 35 | +│ ├── shortcuts.ts # Global shortcuts registration + AI streaming orchestration (largest file) |
| 36 | +│ ├── ai.ts # Vercel AI SDK integration, 3 streaming functions |
| 37 | +│ ├── settings.ts # App settings object + IPC handlers |
| 38 | +│ ├── state.ts # App state object + IPC handlers |
| 39 | +│ ├── take-screenshot.ts # desktopCapturer → base64 PNG |
| 40 | +│ ├── auto-updater.ts # electron-updater (non-macOS only) |
| 41 | +│ ├── prompts.md # System prompt for AI (copied to build output via vite-plugin-static-copy) |
| 42 | +│ └── index.d.ts # global.mainWindow type declaration |
| 43 | +├── preload/ |
| 44 | +│ ├── index.ts # contextBridge API: exposes window.api to renderer |
| 45 | +│ └── index.d.ts # Type declarations for window.electron and window.api |
| 46 | +└── renderer/ |
| 47 | + ├── index.html # SPA entry |
| 48 | + └── src/ |
| 49 | + ├── main.tsx # React root render |
| 50 | + ├── App.tsx # Router + settings sync + shortcut init + Toaster |
| 51 | + ├── coder/ # Main page: screenshot display + AI solution stream |
| 52 | + │ ├── index.tsx # CoderPage layout + state sync |
| 53 | + │ ├── AppHeader.tsx # Draggable title bar with nav buttons |
| 54 | + │ ├── AppContent.tsx# Screenshots gallery + markdown solution + error banner |
| 55 | + │ ├── AppStatusBar.tsx # Loading indicator, follow-up dialog, shortcut hints |
| 56 | + │ └── PrerequisitesChecker.tsx # Modal for API key setup |
| 57 | + ├── settings/ # Settings page |
| 58 | + │ ├── index.tsx # AI config, coding, appearance, shortcuts, privacy |
| 59 | + │ ├── SelectLanguage.tsx # Combobox with custom language input |
| 60 | + │ ├── SelectModel.tsx # Combobox with custom model input |
| 61 | + │ └── CustomShortcuts.tsx # Shortcut key recorder |
| 62 | + ├── help/ # Help page |
| 63 | + │ ├── index.tsx # Quick start guide, shortcuts, FAQ |
| 64 | + │ ├── Shortcuts.tsx |
| 65 | + │ ├── FAQ.tsx |
| 66 | + │ └── components/index.tsx # HelpSection wrapper |
| 67 | + ├── components/ |
| 68 | + │ ├── MarkdownRenderer.tsx # react-markdown + remark-gfm + rehype-highlight |
| 69 | + │ ├── ShortcutRenderer.tsx # Platform-aware shortcut key badges |
| 70 | + │ └── ui/ # shadcn/ui primitives (button, dialog, select, etc.) |
| 71 | + ├── lib/ |
| 72 | + │ ├── store/ # Zustand stores |
| 73 | + │ │ ├── app.ts # ignoreMouse state, synced from main process |
| 74 | + │ │ ├── settings.ts # API config, model, language, opacity (persisted v4) |
| 75 | + │ │ ├── shortcuts.ts # Shortcut bindings (persisted v3, with migration) |
| 76 | + │ │ └── solution.ts # Loading state, solution chunks, screenshots, errors |
| 77 | + │ └── utils/ |
| 78 | + │ ├── index.ts # cn() helper, getCloneableFields() |
| 79 | + │ ├── env.ts # isMac, platformAlt |
| 80 | + │ └── keyboard.ts # Accelerator string conversion |
| 81 | + └── assets/ |
| 82 | + ├── base.css # Tailwind @import, CSS variables, app layout styles |
| 83 | + └── main.css # Tailwind + typography plugin + theme variables (oklch) |
| 84 | +``` |
| 85 | + |
| 86 | +## Architecture |
| 87 | + |
| 88 | +### Process Model |
| 89 | + |
| 90 | +``` |
| 91 | +┌─────────────────────────────────────────────────────┐ |
| 92 | +│ Main Process (src/main/) │ |
| 93 | +│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ |
| 94 | +│ │ settings │ │ state │ │ shortcuts.ts │ │ |
| 95 | +│ │ .ts │ │ .ts │ │ (orchestrator) │ │ |
| 96 | +│ └────┬─────┘ └────┬─────┘ │ - global hotkeys │ │ |
| 97 | +│ │ │ │ - AI streaming │ │ |
| 98 | +│ │ │ │ - conversation │ │ |
| 99 | +│ │ │ │ management │ │ |
| 100 | +│ │ │ └──┬───────────┬────┘ │ |
| 101 | +│ │ │ │ │ │ |
| 102 | +│ │ │ ┌─────┴──┐ ┌─────┴────┐ │ |
| 103 | +│ │ │ │ ai.ts │ │take- │ │ |
| 104 | +│ │ │ │ │ │screenshot│ │ |
| 105 | +│ │ │ └────────┘ └──────────┘ │ |
| 106 | +│ └──────────────┼───────────┘ │ |
| 107 | +│ IPC (ipcMain.handle) │ |
| 108 | +├─────────────────────────────────────────────────────┤ |
| 109 | +│ Preload (src/preload/) │ |
| 110 | +│ contextBridge → window.api │ |
| 111 | +├─────────────────────────────────────────────────────┤ |
| 112 | +│ Renderer (src/renderer/) │ |
| 113 | +│ React app with Zustand stores │ |
| 114 | +│ window.api.on*() for events from main │ |
| 115 | +│ window.api.*() for invoke calls to main │ |
| 116 | +└─────────────────────────────────────────────────────┘ |
| 117 | +``` |
| 118 | + |
| 119 | +### Data Flow: Screenshot → Solution |
| 120 | + |
| 121 | +1. User presses global shortcut (e.g., `Alt+Enter` on macOS) |
| 122 | +2. `shortcuts.ts` callback triggers `takeScreenshot()` → `desktopCapturer` → base64 PNG |
| 123 | +3. Main sends `screenshot-taken` and `ai-loading-start` to renderer |
| 124 | +4. Main calls `getSolutionStream(base64Image)` → Vercel AI SDK `streamText()` |
| 125 | +5. Stream chunks sent to renderer via `solution-chunk` IPC events |
| 126 | +6. Renderer accumulates chunks in `useSolutionStore` and renders via `MarkdownRenderer` |
| 127 | +7. On completion: `solution-complete`; on error: `solution-error`; on abort: `solution-stopped` |
| 128 | + |
| 129 | +### IPC Channels |
| 130 | + |
| 131 | +**Renderer → Main (invoke):** |
| 132 | +- `getAppSettings` / `updateAppSettings` — settings CRUD |
| 133 | +- `updateAppState` — sync `inCoderPage`, `ignoreMouse` |
| 134 | +- `initShortcuts` / `getShortcuts` / `updateShortcuts` — shortcut management |
| 135 | +- `stopSolutionStream` — abort current AI stream |
| 136 | +- `sendFollowUpQuestion` — follow-up within conversation |
| 137 | + |
| 138 | +**Main → Renderer (send):** |
| 139 | +- `sync-app-state` — push state changes (e.g., mouse ignore toggle) |
| 140 | +- `screenshot-taken` / `screenshots-updated` — screenshot data |
| 141 | +- `solution-clear` / `solution-chunk` / `solution-complete` / `solution-stopped` / `solution-error` — AI streaming lifecycle |
| 142 | +- `ai-loading-start` / `ai-loading-end` — loading state |
| 143 | +- `scroll-page-up` / `scroll-page-down` — keyboard-driven scroll |
| 144 | + |
| 145 | +### Zustand Stores |
| 146 | + |
| 147 | +| Store | File | Persisted | Key State | |
| 148 | +|-------|------|-----------|-----------| |
| 149 | +| `useSettingsStore` | `lib/store/settings.ts` | Yes (v4) | `apiBaseURL`, `apiKey`, `model`, `customModels`, `codeLanguage`, `opacity`, `customPrompt` | |
| 150 | +| `useShortcutsStore` | `lib/store/shortcuts.ts` | Yes (v3) | `shortcuts` (action → key mapping with categories) | |
| 151 | +| `useSolutionStore` | `lib/store/solution.ts` | No | `isLoading`, `solutionChunks`, `screenshotData`, `errorMessage` | |
| 152 | +| `useAppStore` | `lib/store/app.ts` | No | `ignoreMouse` | |
| 153 | + |
| 154 | +Settings are bidirectionally synced: renderer persists to localStorage, and on mount syncs to main process via `updateAppSettings()`. Main process `.env` values serve as initial defaults only. |
| 155 | + |
| 156 | +## Key Patterns & Conventions |
| 157 | + |
| 158 | +### Window Stealth |
| 159 | + |
| 160 | +The app is designed to be invisible to screen-sharing software: |
| 161 | +- `BrowserWindow` options: `transparent: true`, `frame: false`, `skipTaskbar: true` |
| 162 | +- `setContentProtection(true)` prevents screen capture of the window itself |
| 163 | +- `setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true, skipTransformProcessType: true })` |
| 164 | +- `keepWindowInFront()` repeatedly reasserts always-on-top for 5 seconds after show |
| 165 | +- `showInactive()` on macOS/Windows to avoid stealing focus |
| 166 | + |
| 167 | +### AI Integration |
| 168 | + |
| 169 | +- All AI calls go through `src/main/ai.ts` using Vercel AI SDK's `streamText()` |
| 170 | +- Provider: `@ai-sdk/openai` with custom `baseURL` (works with any OpenAI-compatible API) |
| 171 | +- Model fallback: `Qwen/Qwen3-VL-32B-Instruct` for SiliconFlow, `gpt-5-mini` otherwise |
| 172 | +- System prompt is loaded from `prompts.md` at build time (bundled via `vite-plugin-static-copy`) |
| 173 | +- Three streaming functions: `getSolutionStream` (first screenshot), `getFollowUpStream` (follow-up), `getGeneralStream` (multi-screenshot) |
| 174 | +- Conversation history (`conversationMessages`) is maintained in `shortcuts.ts` as `ModelMessage[]` |
| 175 | + |
| 176 | +### Stream Abort Pattern |
| 177 | + |
| 178 | +- `StreamContext` with `AbortController` and `reason` (`'user'` | `'new-request'`) |
| 179 | +- New requests automatically abort previous streams |
| 180 | +- User can manually stop via shortcut or UI button |
| 181 | +- Abort reason determines which IPC event to send (`solution-stopped` for user, silent for new-request) |
| 182 | + |
| 183 | +### Shortcut System |
| 184 | + |
| 185 | +- Global shortcuts registered via Electron's `globalShortcut` API |
| 186 | +- Renderer stores shortcut config in Zustand (persisted); sends to main on init |
| 187 | +- On Windows, `Alt`-based shortcuts also register `Ctrl+Alt` variant for compatibility |
| 188 | +- Shortcut actions are string-keyed callbacks in `shortcuts.ts` |
| 189 | +- Default shortcuts use `platformAlt` (`Alt` on macOS, `CommandOrControl` on Windows/Linux) |
| 190 | + |
| 191 | +### UI Component Patterns |
| 192 | + |
| 193 | +- shadcn/ui components in `src/renderer/src/components/ui/` — do NOT edit these directly, use the shadcn CLI to add/update |
| 194 | +- `cn()` utility (clsx + tailwind-merge) for conditional class merging |
| 195 | +- `getCloneableFields()` strips functions from store state before sending over IPC |
| 196 | +- Platform-aware shortcut display via `ShortcutRenderer` (⌘, ⌥, ⇧ on Mac; Ctrl, Alt, Shift on Windows) |
| 197 | + |
| 198 | +## Development |
| 199 | + |
| 200 | +### Commands |
| 201 | + |
| 202 | +```bash |
| 203 | +npm install # Install dependencies |
| 204 | +npm run dev # Start in dev mode (electron-vite dev) |
| 205 | +npm run build # Typecheck + build (electron-vite build) |
| 206 | +npm run build:mac # Build macOS distributable |
| 207 | +npm run build:win # Build Windows distributable |
| 208 | +npm run typecheck # Run TypeScript type checking (node + web) |
| 209 | +npm run lint # Run ESLint |
| 210 | +npm run format # Run Prettier |
| 211 | +``` |
| 212 | + |
| 213 | +### Configuration |
| 214 | + |
| 215 | +The `.env` file at project root configures the AI provider: |
| 216 | + |
| 217 | +```env |
| 218 | +API_BASE_URL="https://openrouter.ai/api/v1" # OpenAI-compatible API endpoint |
| 219 | +API_KEY="sk-..." # API key |
| 220 | +MODEL="gpt-5-mini" # Optional: override default model |
| 221 | +``` |
| 222 | + |
| 223 | +These are read by dotenv in the main process and merged with renderer-side settings (renderer settings take priority when set). |
| 224 | + |
| 225 | +### Path Aliases |
| 226 | + |
| 227 | +- `@renderer/*` and `@/*` both resolve to `src/renderer/src/*` |
| 228 | +- Configured in `tsconfig.web.json` and `electron.vite.config.ts` |
| 229 | + |
| 230 | +### Code Style |
| 231 | + |
| 232 | +- Prettier: single quotes, no semicolons, 100 char print width, no trailing commas |
| 233 | +- ESLint: TypeScript + React + React Hooks + React Refresh rules |
| 234 | +- UI text and user-facing strings are in **Chinese** (中文) |
| 235 | +- Code comments and variable names are in **English** |
| 236 | + |
| 237 | +## Important Notes for AI Agents |
| 238 | + |
| 239 | +1. **Three TypeScript configs**: `tsconfig.node.json` (main + preload), `tsconfig.web.json` (renderer). The root `tsconfig.json` is a project references file only. |
| 240 | + |
| 241 | +2. **`prompts.md` is bundled**: It lives in `src/main/` but gets copied to the build output via `vite-plugin-static-copy`. Loaded at runtime with `readFileSync(join(import.meta.dirname, 'prompts.md'))`. |
| 242 | + |
| 243 | +3. **`global.mainWindow`**: The main window reference is stored as a global variable, declared in `src/main/index.d.ts`. |
| 244 | + |
| 245 | +4. **Settings flow**: `.env` → main process `settings` object → renderer reads on mount via IPC → renderer persists to localStorage via Zustand. Renderer-side changes are sent back to main via `updateAppSettings`. |
| 246 | + |
| 247 | +5. **No shared types directory**: Main process types (`AppSettings`, `AppState`) are imported directly by the preload script from `../main/settings` and `../main/state`. This works because preload shares the Node.js tsconfig. |
| 248 | + |
| 249 | +6. **Streaming orchestration is in `shortcuts.ts`**: Despite the filename, this 580+ line file is the central orchestrator for both global shortcuts AND AI streaming logic. It manages conversation history, abort controllers, and IPC communication for the entire AI workflow. |
| 250 | + |
| 251 | +7. **Window movement**: The window can be moved via keyboard shortcuts in 200px steps (up/down/left/right). |
| 252 | + |
| 253 | +8. **macOS auto-update is disabled**: `publish: null` in electron-builder.yml for mac target. Auto-update only works on Windows. |
0 commit comments