Skip to content

Commit 046b7ba

Browse files
committed
feat: add MCQ mode functionality and enhance state management
- Introduced a new MCQ mode in the application, allowing users to analyze multiple choice questions. - Updated state management to include MCQ responses and processing events. - Modified existing components to support the new MCQ view and navigation between modes. - Enhanced IPC handlers to process MCQ requests and return responses. - Updated dependencies in package.json for compatibility with new features. - Added a new MCQView component to handle user interactions and display analysis results. - Improved keyboard shortcuts and view management to accommodate the new mode.
1 parent 3cd4575 commit 046b7ba

File tree

14 files changed

+2300
-9217
lines changed

14 files changed

+2300
-9217
lines changed

bun.lock

Lines changed: 1779 additions & 9169 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,51 +21,52 @@
2121
"build:linux": "electron-vite build && electron-builder --linux"
2222
},
2323
"dependencies": {
24-
"@ai-sdk/google": "^1.2.22",
25-
"@ai-sdk/groq": "^1.2.9",
26-
"@ai-sdk/openai": "^1.3.22",
24+
"@ai-sdk/cerebras": "^1.0.0",
25+
"@ai-sdk/google": "^2.0.0",
26+
"@ai-sdk/groq": "^2.0.0",
27+
"@ai-sdk/openai": "^2.0.0",
2728
"@electron-toolkit/preload": "^3.0.2",
2829
"@electron-toolkit/utils": "^4.0.0",
2930
"@radix-ui/react-dialog": "^1.1.14",
3031
"@radix-ui/react-toast": "^1.2.14",
3132
"@tailwindcss/postcss": "^4.1.11",
32-
"@tanstack/react-query": "^5.81.5",
33+
"@tanstack/react-query": "^5.84.1",
3334
"@types/react-syntax-highlighter": "^15.5.13",
34-
"ai": "^4.3.16",
35+
"ai": "^5.0.0",
3536
"class-variance-authority": "^0.7.1",
3637
"clsx": "^2.1.1",
37-
"lucide-react": "^0.513.0",
38-
"openai": "^4.104.0",
38+
"lucide-react": "^0.536.0",
39+
"openai": "^5.11.0",
3940
"postcss": "^8.5.6",
4041
"react-markdown": "^10.1.0",
4142
"react-syntax-highlighter": "^15.6.1",
4243
"screenshot-desktop": "^1.15.1",
4344
"tailwind-merge": "^3.3.1",
4445
"tailwindcss": "^4.1.11",
4546
"uuid": "^11.1.0",
46-
"zod": "^3.25.74"
47+
"zod": "^4.0.14"
4748
},
4849
"devDependencies": {
4950
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
5051
"@electron-toolkit/eslint-config-ts": "^3.1.0",
5152
"@electron-toolkit/tsconfig": "^1.0.1",
5253
"@electron/notarize": "^3.0.1",
53-
"@types/node": "^22.16.0",
54-
"@types/react": "^19.1.8",
55-
"@types/react-dom": "^19.1.6",
56-
"@vitejs/plugin-react": "^4.6.0",
57-
"electron": "36.4.0",
54+
"@types/node": "^22.17.0",
55+
"@types/react": "^19.1.9",
56+
"@types/react-dom": "^19.1.7",
57+
"@vitejs/plugin-react": "^4.7.0",
58+
"electron": "37.2.5",
5859
"electron-builder": "^26.0.12",
59-
"electron-vite": "^3.1.0",
60-
"eslint": "^9.30.1",
60+
"electron-vite": "^4.0.0",
61+
"eslint": "^9.32.0",
6162
"eslint-plugin-react": "^7.37.5",
6263
"eslint-plugin-react-hooks": "^5.2.0",
6364
"eslint-plugin-react-refresh": "^0.4.20",
6465
"prettier": "^3.6.2",
65-
"react": "^19.1.0",
66-
"react-dom": "^19.1.0",
67-
"typescript": "^5.8.3",
68-
"vite": "^6.3.5"
66+
"react": "^19.1.1",
67+
"react-dom": "^19.1.1",
68+
"typescript": "^5.9.2",
69+
"vite": "^7.0.6"
6970
},
7071
"pnpm": {
7172
"onlyBuiltDependencies": [

src/main/index.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,17 @@ export const state = {
2222
screenshotManager: null as ScreenshotManager | null,
2323
processingManager: null as ProcessingManager | null,
2424

25-
view: 'queue' as 'queue' | 'solutions' | 'debug' | 'question',
25+
view: 'queue' as 'queue' | 'solutions' | 'debug' | 'question' | 'mcq',
2626
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2727
problemInfo: null as any,
2828
hasDebugged: false,
2929
questionResponse: null as { answer: string; timestamp: number } | null,
30+
mcqResponse: null as {
31+
answer: string
32+
explanation: string
33+
incorrectOptions: string[]
34+
timestamp: number
35+
} | null,
3036

3137
PROCESSING_EVENTS: {
3238
NO_SCREENSHOTS: 'processing-no-screenshots',
@@ -39,7 +45,9 @@ export const state = {
3945
DEBUG_SUCCESS: 'debug-success',
4046
DEBUG_ERROR: 'debug-error',
4147
QUESTION_RESPONSE: 'question-response',
42-
QUESTION_ERROR: 'question-error'
48+
QUESTION_ERROR: 'question-error',
49+
MCQ_RESPONSE: 'mcq-response',
50+
MCQ_ERROR: 'mcq-error'
4351
}
4452
}
4553

@@ -189,12 +197,12 @@ async function getImagePreview(filePath: string): Promise<string> {
189197
return state.screenshotManager?.getImagePreview(filePath) || ''
190198
}
191199

192-
function setView(view: 'queue' | 'solutions' | 'debug' | 'question'): void {
200+
function setView(view: 'queue' | 'solutions' | 'debug' | 'question' | 'mcq'): void {
193201
state.view = view
194202
state.screenshotManager?.setView(view)
195203
}
196204

197-
function getView(): 'queue' | 'solutions' | 'debug' | 'question' {
205+
function getView(): 'queue' | 'solutions' | 'debug' | 'question' | 'mcq' {
198206
return state.view
199207
}
200208

@@ -345,6 +353,26 @@ function setQuestionResponse(response: { answer: string; timestamp: number } | n
345353
state.questionResponse = response
346354
}
347355

356+
function getMcqResponse(): {
357+
answer: string
358+
explanation: string
359+
incorrectOptions: string[]
360+
timestamp: number
361+
} | null {
362+
return state.mcqResponse
363+
}
364+
365+
function setMcqResponse(
366+
response: {
367+
answer: string
368+
explanation: string
369+
incorrectOptions: string[]
370+
timestamp: number
371+
} | null
372+
): void {
373+
state.mcqResponse = response
374+
}
375+
348376
function initializeHelpers() {
349377
state.screenshotManager = new ScreenshotManager(state.view)
350378
state.processingManager = new ProcessingManager({
@@ -364,6 +392,8 @@ function initializeHelpers() {
364392
getScreenshotManager,
365393
getQuestionResponse,
366394
setQuestionResponse,
395+
getMcqResponse,
396+
setMcqResponse,
367397
PROCESSING_EVENTS: state.PROCESSING_EVENTS
368398
})
369399
state.keyboardShortcutHelper = new KeyboardShortcutHelper({
@@ -445,7 +475,8 @@ async function initializeApp() {
445475
PROCESSING_EVENTS: state.PROCESSING_EVENTS,
446476
processingManager: state.processingManager,
447477
setWindowDimensions: setWindowDimensions,
448-
getQuestionResponse: getQuestionResponse
478+
getQuestionResponse: getQuestionResponse,
479+
getMcqResponse: getMcqResponse
449480
})
450481

451482
await createWindow()

src/main/lib/ipc-handler.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ export interface IIPCHandler {
77
takeScreenshot: () => Promise<string>
88
getImagePreview: (filePath: string) => Promise<string>
99
clearQueues: () => void
10-
setView: (view: 'queue' | 'solutions' | 'debug' | 'question') => void
11-
getView: () => 'queue' | 'solutions' | 'debug' | 'question'
10+
setView: (view: 'queue' | 'solutions' | 'debug' | 'question' | 'mcq') => void
11+
getView: () => 'queue' | 'solutions' | 'debug' | 'question' | 'mcq'
1212
getScreenshotQueue: () => string[]
1313
getExtraScreenshotQueue: () => string[]
1414
moveWindowLeft: () => void
@@ -22,6 +22,12 @@ export interface IIPCHandler {
2222
processingManager: ProcessingManager | null
2323
setWindowDimensions: (width: number, height: number) => void
2424
getQuestionResponse: () => { answer: string; timestamp: number } | null
25+
getMcqResponse: () => {
26+
answer: string
27+
explanation: string
28+
incorrectOptions: string[]
29+
timestamp: number
30+
} | null
2531
}
2632

2733
export function initializeIpcHandler(deps: IIPCHandler): void {
@@ -212,4 +218,26 @@ export function initializeIpcHandler(deps: IIPCHandler): void {
212218
ipcMain.handle('get-question-response', async () => {
213219
return deps.getQuestionResponse()
214220
})
221+
222+
// MCQ mode handlers
223+
ipcMain.handle('process-mcq', async () => {
224+
try {
225+
if (!configManager.hasApiKey()) {
226+
const mainWindow = deps.getMainWindow()
227+
if (mainWindow) {
228+
mainWindow.webContents.send(deps.PROCESSING_EVENTS.API_KEY_INVALID)
229+
}
230+
return { success: false, error: 'No API key found' }
231+
}
232+
await deps.processingManager?.processMCQ()
233+
return { success: true }
234+
} catch (error) {
235+
console.error('Error processing MCQ:', error)
236+
return { success: false, error: 'Failed to process MCQ' }
237+
}
238+
})
239+
240+
ipcMain.handle('get-mcq-response', async () => {
241+
return deps.getMcqResponse()
242+
})
215243
}

src/main/lib/keyboard-shortcut.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface IKeyboardShortcutHelper {
1212
takeScreenshot: () => Promise<string>
1313
getImagePreview: (filePath: string) => Promise<string>
1414
clearQueues: () => void
15-
setView: (view: 'queue' | 'solutions' | 'debug' | 'question') => void
15+
setView: (view: 'queue' | 'solutions' | 'debug' | 'question' | 'mcq') => void
1616
processingManager: ProcessingManager | null
1717
}
1818

@@ -137,7 +137,7 @@ export class KeyboardShortcutHelper {
137137
}
138138
})
139139
globalShortcut.register('CommandOrControl+M', () => {
140-
console.log('Toggle between screenshot and question mode')
140+
console.log('Cycle through modes: Coder -> Question -> MCQ')
141141
const mainWindow = this.deps.getMainWindow()
142142
if (mainWindow) {
143143
// Send event to toggle mode in the renderer

src/main/lib/processing-manager.ts

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { z } from 'zod'
1313
export interface IProcessingManager {
1414
getMainWindow: () => BrowserWindow | null
1515
getScreenshotManager: () => ScreenshotManager | null
16-
getView: () => 'queue' | 'solutions' | 'debug' | 'question'
17-
setView: (view: 'queue' | 'solutions' | 'debug' | 'question') => void
16+
getView: () => 'queue' | 'solutions' | 'debug' | 'question' | 'mcq'
17+
setView: (view: 'queue' | 'solutions' | 'debug' | 'question' | 'mcq') => void
1818
getProblemInfo: () => any
1919
setProblemInfo: (problemInfo: any) => void
2020
getScreenshotQueue: () => string[]
@@ -27,6 +27,20 @@ export interface IProcessingManager {
2727
getHasDebugged: () => boolean
2828
getQuestionResponse: () => { answer: string; timestamp: number } | null
2929
setQuestionResponse: (response: { answer: string; timestamp: number } | null) => void
30+
getMcqResponse: () => {
31+
answer: string
32+
explanation: string
33+
incorrectOptions: string[]
34+
timestamp: number
35+
} | null
36+
setMcqResponse: (
37+
response: {
38+
answer: string
39+
explanation: string
40+
incorrectOptions: string[]
41+
timestamp: number
42+
} | null
43+
) => void
3044
PROCESSING_EVENTS: typeof state.PROCESSING_EVENTS
3145
}
3246

@@ -39,6 +53,15 @@ const problemInfoSchema = z.object({
3953
})
4054
type ProblemInfo = z.infer<typeof problemInfoSchema>
4155

56+
// Define Zod schema for MCQ response
57+
const mcqResponseSchema = z.object({
58+
question: z.string().min(1, 'Question is required.'),
59+
options: z.array(z.string()).min(2, 'At least 2 options are required.'),
60+
correct_answer: z.string().min(1, 'Correct answer is required.'),
61+
explanation: z.string().min(1, 'Explanation is required.'),
62+
incorrect_explanations: z.array(z.string()).optional()
63+
})
64+
4265
export class ProcessingManager {
4366
private deps: IProcessingManager
4467
private screenshotManager: ScreenshotManager | null = null
@@ -851,4 +874,111 @@ Tell answers in details of about 200 words minimum.
851874
mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.QUESTION_ERROR, errorMessage)
852875
}
853876
}
877+
878+
public async processMCQ(): Promise<void> {
879+
const mainWindow = this.deps.getMainWindow()
880+
if (!mainWindow) return
881+
882+
const llmProvider = this.getActiveLLMProvider()
883+
if (!llmProvider) {
884+
console.error('Failed to initialize AI provider.')
885+
mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.API_KEY_INVALID)
886+
return
887+
}
888+
889+
try {
890+
const existingScreenshots = this.deps.getScreenshotQueue()
891+
if (existingScreenshots.length === 0) {
892+
mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS)
893+
return
894+
}
895+
896+
mainWindow.webContents.send('processing-status', {
897+
message: 'Analyzing MCQ screenshot...',
898+
progress: 20
899+
})
900+
901+
const screenshotsData = await Promise.all(
902+
existingScreenshots.map(async (path) => {
903+
try {
904+
return {
905+
path,
906+
data: fs.readFileSync(path)
907+
}
908+
} catch (error) {
909+
console.error('Error reading screenshot:', error)
910+
return null
911+
}
912+
})
913+
)
914+
915+
const validScreenshots = screenshotsData.filter(Boolean) as Array<{
916+
path: string
917+
data: Buffer
918+
}>
919+
920+
if (validScreenshots.length === 0) {
921+
throw new Error('No valid screenshots to process')
922+
}
923+
924+
const userMessagesContent = validScreenshots.map((screenshot) => ({
925+
type: 'image' as const,
926+
image: screenshot.data
927+
}))
928+
929+
mainWindow.webContents.send('processing-status', {
930+
message: 'Extracting MCQ information...',
931+
progress: 50
932+
})
933+
934+
const { object: mcqData } = await generateObject({
935+
model: llmProvider,
936+
schema: mcqResponseSchema,
937+
messages: [
938+
{
939+
role: 'system',
940+
content: `You are an expert MCQ analyzer. Analyze the screenshot of the multiple choice question and extract all relevant information.
941+
942+
Your task is to:
943+
1. Identify the question text
944+
2. Extract all answer options (A, B, C, D, etc.)
945+
3. Determine the correct answer
946+
4. Provide a clear explanation for why the correct answer is right
947+
5. Optionally provide brief explanations for why other options are incorrect
948+
949+
Return the information in JSON format matching the schema fields: question, options, correct_answer, explanation, incorrect_explanations.`
950+
},
951+
{ role: 'user', content: userMessagesContent }
952+
],
953+
temperature: 0.2,
954+
maxTokens: llmProvider.provider == 'openai' ? 4000 : 6000,
955+
mode: 'json'
956+
})
957+
958+
mainWindow.webContents.send('processing-status', {
959+
message: 'Generating detailed explanation...',
960+
progress: 80
961+
})
962+
963+
// Format the response for the UI
964+
const response = {
965+
answer: mcqData.correct_answer,
966+
explanation: mcqData.explanation,
967+
incorrectOptions: mcqData.incorrect_explanations || [],
968+
timestamp: Date.now()
969+
}
970+
971+
this.deps.setMcqResponse(response)
972+
mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.MCQ_RESPONSE, response)
973+
974+
mainWindow.webContents.send('processing-status', {
975+
progress: 100,
976+
message: 'MCQ analysis complete.'
977+
})
978+
} catch (error: any) {
979+
console.error('Error processing MCQ:', error)
980+
const errorMessage = error.message || 'Failed to process MCQ'
981+
mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.MCQ_ERROR, errorMessage)
982+
}
983+
}
854984
}

0 commit comments

Comments
 (0)