diff --git a/.gitignore b/.gitignore index c32d9fa..3ae9680 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ release dist .env .env.* +myenv/ **/.DS_Store **/.vscode **/.idea diff --git a/FLOATING_WINDOW_README.md b/FLOATING_WINDOW_README.md new file mode 100644 index 0000000..001822e --- /dev/null +++ b/FLOATING_WINDOW_README.md @@ -0,0 +1,70 @@ +# Conversational Co-Pilot - Floating Window + +This project has been transformed from a screenshot-based Jiminy – The Second Conscience to a clean floating conversational co-pilot application. + +## What We've Done + +### ✅ Completed Changes + +1. **Hidden Screenshot Functionality** + - Screenshot queues are now hidden but kept functional in the background + - Screenshot commands and UI elements are hidden from view + - All backend screenshot functionality remains intact + +2. **Added Floating Navigation Dock** + - Beautiful animated floating dock with relevant co-pilot controls + - Icons for Voice Input, Mute, AI Assistant, Conversations, Reset, and Settings + - Smooth animations and hover effects + - Positioned at the bottom center of the window + +3. **Redesigned UI for Conversational Co-Pilot** + - Clean, modern interface with glass-morphism design + - Placeholder areas for: + - MY SPEECH (voice input detection) + - OTHER SPEECH (voice detection from others) + - AI SUGGESTIONS (AI-generated suggestions) + - AI RESPONSE AREA (where AI responses will appear) + +4. **Updated Window Styling** + - Added gradient background + - Improved spacing and layout + - Made the interface more suitable for a floating window + +### 🎛️ Controls + +- **Ctrl+B** (Windows) or **Cmd+B** (Mac): Toggle window visibility +- **Floating Dock**: Navigation and controls (currently placeholders) + +### 🏗️ Next Steps (As per TASKS.md) + +The foundation is now ready for implementing the actual conversational co-pilot features: + +1. **Audio Capture Implementation** + - Microphone audio capture + - System audio loopback capture + - Integration with Silero VAD for voice activity detection + +2. **WebSocket Integration** + - Connect to FastAPI backend + - Real-time audio streaming + - Bi-directional communication for transcripts and AI responses + +3. **AI Integration** + - Faster-Whisper for speech-to-text + - DeepSeek LLM for AI responses + - ChromaDB for RAG (Retrieval-Augmented Generation) + +4. **Real-time Features** + - Live speech transcription + - Question detection + - Contextual AI suggestions + - Streaming AI responses + +### 🎨 Design Features + +- **Glass-morphism UI**: Semi-transparent panels with backdrop blur +- **Floating Dock**: Animated navigation with smooth hover effects +- **Responsive Layout**: Adapts to different window sizes +- **Dark Theme**: Modern dark interface suitable for a floating window + +The application now provides a clean, professional floating window interface ready for conversational AI features while keeping all existing functionality intact in the background. diff --git a/README.md b/README.md index 2b1a4e3..f4fe68d 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,259 @@ -# Interview Coder +# 🤖 Jiminy – The Second Conscience -Interview Coder is a desktop application designed to help users with technical coding interviews. It allows users to take screenshots of coding problems, process them using AI, and get solutions. +
+ Electron + React + TypeScript + Tailwind +
+A powerful desktop application designed to assist developers with technical coding interviews and problem-solving. Features screenshot-based problem analysis, AI-powered solution generation, and real-time speech-to-text capabilities. -## Features +--- -- Take screenshots of coding problems -- Process screenshots to extract problem statements -- Generate solutions in your preferred programming language -- View time and space complexity analysis -- Toggle window visibility with keyboard shortcuts -- Move the window around the screen with keyboard shortcuts +## ✨ Features -## Keyboard Shortcuts +### 🖼️ Screenshot-Based Problem Analysis +- **Smart Screenshot Capture**: Instantly capture coding problems with global hotkeys +- **Multi-Language Support**: Support for Python, JavaScript, Java, Go, C++, Swift, Kotlin, Ruby, SQL, and R +- **Problem Extraction**: AI-powered extraction of problem statements, constraints, and requirements from screenshots -- **Cmd/Ctrl + B**: Toggle window visibility -- **Cmd/Ctrl + Q**: Quit the application -- **Cmd/Ctrl + H**: Take a screenshot -- **Cmd/Ctrl + Enter**: Process screenshots -- **Arrow keys with Cmd/Ctrl**: Move window around the screen +### 🧠 AI-Powered Solution Generation +- **Intelligent Code Generation**: Generate optimized solutions based on problem analysis +- **Time & Space Complexity Analysis**: Detailed algorithmic complexity breakdowns +- **Multiple Solution Approaches**: Explore different algorithmic strategies +- **Step-by-Step Explanations**: Clear reasoning for each solution approach -## Running the Application +### 🎤 Real-Time Speech-to-Text (STT) +- **Live Transcription**: Real-time speech recognition for hands-free interaction +- **Voice Activity Detection**: Smart detection of speech vs. silence +- **WebSocket Integration**: Low-latency audio streaming and processing +- **Session Management**: Persistent transcription sessions with unique identifiers + +### 🎯 Developer Experience +- **Floating Window**: Unobtrusive overlay that stays accessible while coding +- **Click-Through Transparency**: Interact with applications behind the window +- **Global Keyboard Shortcuts**: Quick access without context switching +- **Auto-Updates**: Seamless application updates with user notifications + +### 🔄 Workflow Integration +- **Queue Management**: Organize and process multiple screenshots +- **Debug Mode**: Advanced analysis and debugging assistance +- **Solution History**: Track and review previous solutions +- **Export Capabilities**: Copy code and explanations to clipboard + +--- + +## 🚀 Quick Start ### Prerequisites -- Node.js (v16 or higher) -- npm or yarn +- **Node.js** (v16 or higher) +- **npm** or **yarn** +- **Git** ### Installation -1. Clone the repository: - ``` - git clone https://github.com/yourusername/interview-coder.git - cd interview-coder +1. **Clone the repository** + ```bash + git clone https://github.com/gamidirohan/Code-Assist-Electron-Frontend.git + cd Code-Assist-Electron-Frontend ``` -2. Install dependencies: - ``` +2. **Install dependencies** + ```bash npm install # or - yarn + yarn install ``` -3. Run the application in development mode: - ``` +3. **Start development server** + ```bash npm run dev # or yarn dev ``` -4. Build the application for production: - ``` +4. **Build for production** + ```bash npm run build # or yarn build ``` -## API Integration +--- + +## ⌨️ Keyboard Shortcuts + +| Shortcut | Action | Description | +|----------|--------|-------------| +| `Ctrl/Cmd + B` | Toggle Window | Show/hide the application window | +| `Ctrl/Cmd + H` | Take Screenshot | Capture a screenshot of the current problem | +| `Ctrl/Cmd + Enter` | Process/Solve | Generate solution from captured screenshots | +| `Ctrl/Cmd + Q` | Quit Application | Close the application | +| `Arrow Keys + Ctrl/Cmd` | Move Window | Reposition the application window | + +--- + +## 🏗️ Architecture + +### Tech Stack +- **Frontend**: React 18, TypeScript, Tailwind CSS +- **Desktop Shell**: Electron with Node.js backend +- **State Management**: TanStack Query (React Query) +- **UI Components**: Radix UI primitives +- **Audio Processing**: Web Audio API with WebSocket streaming +- **Build Tools**: Vite, electron-builder + +### Project Structure +``` +├── electron/ # Electron main process +│ ├── main.ts # Application entry point +│ ├── preload.ts # Preload scripts +│ ├── ProcessingHelper.ts # Screenshot processing logic +│ └── autoUpdater.ts # Auto-update functionality +├── src/ # React application +│ ├── components/ # Reusable UI components +│ ├── pages/ # Application pages/views +│ ├── hooks/ # Custom React hooks +│ ├── types/ # TypeScript type definitions +│ └── utils/ # Utility functions +└── dist-electron/ # Compiled Electron files +``` + +--- + +## 🔧 Configuration + +### Environment Setup +The application includes sensible defaults and doesn't require extensive configuration for basic usage. + +### API Integration +> **Note**: This version includes mock API endpoints. For full functionality, you'll need to implement your own backend service or integrate with existing AI services. + +### Real-Time Speech-to-Text Setup +The STT feature connects to a WebSocket server at `ws://localhost:3000/ws/jiminy/`. To enable this feature: + +1. Set up a compatible WebSocket server +2. Ensure proper audio permissions in your browser/system +3. Configure the WebSocket URL in `src/hooks/useRealtimeSTT.ts` + +--- + +## 🎯 Usage Guide + +### Taking Screenshots +1. Position your coding problem on screen +2. Press `Ctrl/Cmd + H` to capture +3. The screenshot will appear in the queue + +### Generating Solutions +1. Ensure you have at least one screenshot captured +2. Select your preferred programming language +3. Press `Ctrl/Cmd + Enter` to process +4. View the generated solution with complexity analysis + +### Using Speech-to-Text +1. Click the microphone icon (when available) +2. Start speaking your query or problem description +3. The application will transcribe in real-time +4. Use voice commands for hands-free operation + +### Debug Mode +Access advanced debugging and analysis features: +- Multiple solution approaches +- Detailed step-by-step breakdowns +- Error analysis and suggestions +- Performance optimization tips + +--- + +## 🤝 Contributing + +We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. + +### Development Workflow +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +### Code Style +- Use TypeScript for all new code +- Follow the existing code formatting +- Add tests for new features +- Update documentation as needed + +--- + +## 📋 Features Roadmap + +### Current Features ✅ +- Screenshot capture and processing +- Multi-language code generation +- Real-time speech-to-text +- Floating window interface +- Auto-updates + +### Planned Features 🚧 +- IDE integration plugins +- Team collaboration features +- Custom AI model integration +- Advanced debugging tools +- Mobile companion app + +--- + +## 🐛 Troubleshooting + +### Common Issues + +**Application won't start** +- Ensure Node.js v16+ is installed +- Clear `node_modules` and reinstall dependencies +- Check for port conflicts (default: 3000) + +**Screenshots not working** +- Verify screen capture permissions +- Check if other screenshot tools are interfering +- Restart the application + +**Speech-to-text not responding** +- Ensure microphone permissions are granted +- Check WebSocket server connection +- Verify audio input device is working + +### Getting Help +- 📖 Check the [Wiki](../../wiki) for detailed guides +- 🐛 Report bugs in [Issues](../../issues) +- 💬 Join our [Discussions](../../discussions) for questions + +--- + +## 📄 License + +This project is licensed under the **ISC License** - see the [LICENSE](LICENSE) file for details. + +--- + +## 🙏 Acknowledgments -This version of the application still requires an API connection to process screenshots and generate solutions. You'll need to set up your own API service or modify the code to use a different solution generation method. +- Built with [Electron](https://electronjs.org/) +- UI components from [Radix UI](https://radix-ui.com/) +- Speech processing powered by Web Audio API +- Icons from [Lucide React](https://lucide.dev/) -## Disclaimer +--- -This modified version is for educational purposes only. The original Interview Coder application is a commercial product with subscription requirements. +## 📊 Project Stats -## License +![GitHub stars](https://img.shields.io/github/stars/gamidirohan/Code-Assist-Electron-Frontend?style=social) +![GitHub forks](https://img.shields.io/github/forks/gamidirohan/Code-Assist-Electron-Frontend?style=social) +![GitHub issues](https://img.shields.io/github/issues/gamidirohan/Code-Assist-Electron-Frontend) +![GitHub pull requests](https://img.shields.io/github/issues-pr/gamidirohan/Code-Assist-Electron-Frontend) -This project is licensed under the ISC License. +
+

Made with ❤️ for developers, by developers

+

Star ⭐ this repository if you find it helpful!

+
diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..55c4e52 --- /dev/null +++ b/TASKS.md @@ -0,0 +1,87 @@ +# Project Tasks: Real-Time Conversational Co-Pilot + +## Decisions Log + +* **Core Architecture:** Electron Frontend, FastAPI Backend, Silero VAD, Faster-Whisper, DeepSeek LLM, ChromaDB for RAG. +* **Primary Communication:** WebSocket for real-time audio and responses. +* **UI Layout:** Full-screen with main content area, right sidebar (my/other speech), bottom bar (suggestions). + +## To Do + +### Frontend (Electron + React) + +* **UI Layout & Shell:** + * [ ] Implement basic full-screen Electron window. + * [ ] Design and implement the main content area component. + * [ ] Design and implement the right sidebar component with two square widgets for "MY_SPEECH" and "OTHER_SPEECH" transcriptions. + * [ ] Design and implement the bottom bar component with two long text box widgets for suggestions. +* **Audio Capture:** + * [ ] Implement microphone audio capture. + * [ ] Implement system audio loopback capture (OS-specific investigation needed: WASAPI for Windows). +* **WebSocket Client:** + * [ ] Implement WebSocket connection to the FastAPI backend. + * [ ] Implement sending `audio_chunk` ("mic" and "system_output") messages. + * [ ] Implement handling for `my_speech_transcript_update` messages and display in UI. + * [ ] Implement handling for `other_speech_transcript_update` messages and display in UI. + * [ ] Implement handling for `ai_suggestion` messages and display in UI. + * [ ] Implement handling for `ai_response_chunk` / `ai_response_complete` messages and display in main content UI. +* **Knowledge Base Interaction (UI):** + * [ ] UI for adding new content to the knowledge base. + * [ ] UI for displaying knowledge base status (optional). + +### Backend (FastAPI) + +* **Server Setup:** + * [ ] Basic FastAPI server setup with Uvicorn. +* **WebSocket Endpoint (`/api/v1/audio_stream`):** + * [ ] Implement WebSocket endpoint to receive audio chunks. + * [ ] Implement logic to differentiate "mic" and "system_output" streams. + * [ ] Buffer incoming audio for each stream. +* **Audio Processing Pipeline (per stream):** + * [ ] Integrate Silero VAD for utterance detection. + * [ ] Integrate Faster-Whisper for speech-to-text. + * [ ] Tag transcripts as "MY_SPEECH" or "OTHER_SPEECH". + * [ ] Send `my_speech_transcript_update` and `other_speech_transcript_update` back to frontend via WebSocket. +* **Conversation Context & Question Detection:** + * [ ] Implement conversation history management. + * [ ] Implement question detection logic from "OTHER_SPEECH". +* **RAG System:** + * [ ] Offline script: Preprocess knowledge base, generate embeddings (sentence-transformers), store in ChromaDB. + * [ ] Real-time retrieval: Embed question, query ChromaDB. +* **DeepSeek LLM Orchestration:** + * [ ] Construct prompt (history + question + RAG results). + * [ ] Call DeepSeek API (streaming). + * [ ] Stream `ai_response_chunk` / `ai_response_complete` back to frontend. + * [ ] Send `ai_suggestion` (if applicable) back to frontend. +* **Caching:** + * [ ] Implement caching for LLM responses (e.g., `functools.lru_cache`). +* **Knowledge Base Endpoints:** + * [ ] Implement `POST /api/v1/add_to_knowledge_base`. + * [ ] Implement `GET /api/v1/knowledge_base_status`. + +### General / Project Management + +* [ ] Update `README.md` with backend endpoint details as they are implemented. +* [ ] Set up environment variables for API keys (e.g., DeepSeek). + +## In Progress + +* [X] Define initial MVP plan and architecture (details in `README.md`). +* [X] Create `TASKS.md` for project tracking. + + +## Done + +* [X] Initial project setup (based on Interview Coder). +* [X] Switched to `converse-co-pilot` git branch. +* [X] Updated `README.md` with the new "Real-Time Conversational Co-Pilot" MVP plan. + +## Future Ideas / Backlog + +* Automatic context gathering from active IDE/editor. +* Voice input/output for commands to the co-pilot itself. +* Advanced project-wide code analysis. +* Team collaboration features. +* Screen recording/video analysis. +* More sophisticated speaker diarization for multiple "other" speakers. +* Sentiment/personality analysis from audio. diff --git a/assets/icons/win/icon1.ico b/assets/icons/win/icon1.ico new file mode 100644 index 0000000..6d76d5b Binary files /dev/null and b/assets/icons/win/icon1.ico differ diff --git a/electron/ProcessingHelper.ts b/electron/ProcessingHelper.ts index 0797c5e..391bf96 100644 --- a/electron/ProcessingHelper.ts +++ b/electron/ProcessingHelper.ts @@ -7,9 +7,10 @@ import { app } from "electron" import { BrowserWindow } from "electron" const isDev = !app.isPackaged +// Update this URL to your actual backend API endpoint const API_BASE_URL = isDev ? "http://localhost:3000" - : "https://www.interviewcoder.co" + : "http://localhost:3000" // Change this to your production API URL export class ProcessingHelper { private deps: IProcessingHelperDeps @@ -77,7 +78,7 @@ export class ProcessingHelper { if (!mainWindow) return // Credits check is bypassed - we always have enough credits - + const view = this.deps.getView() console.log("Processing screenshots in view:", view) @@ -240,6 +241,10 @@ export class ProcessingHelper { // First API call - extract problem info try { + console.log(`Making API request to: ${API_BASE_URL}/api/extract`) + console.log(`Using language: ${language}`) + console.log(`Number of screenshots: ${imageDataList.length}`) + const extractResponse = await axios.post( `${API_BASE_URL}/api/extract`, { imageDataList, language }, @@ -269,18 +274,31 @@ export class ProcessingHelper { ) // Generate solutions after successful extraction + console.log("Starting solution generation") const solutionsResult = await this.generateSolutionsHelper(signal) if (solutionsResult.success) { + console.log("Solution generation successful", solutionsResult.data) + + // Use the solution data directly without replacing newlines + const formattedData = solutionsResult.data; + + // Ensure window is fully visible and opaque after solution generation + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.setOpacity(1.0); + console.log("Window opacity restored to 1.0 after solution generation"); + } + // Clear any existing extra screenshots before transitioning to solutions view this.screenshotHelper.clearExtraScreenshotQueue() mainWindow.webContents.send( - this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, - solutionsResult.data + this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, + formattedData ) return { success: true, data: solutionsResult.data } } else { + console.error("Solution generation failed", solutionsResult.error) throw new Error( - solutionsResult.error || "Failed to generate solutions" + solutionsResult.error || "Failed to generate solutions" ) } } @@ -297,7 +315,10 @@ export class ProcessingHelper { status: error.response?.status, data: error.response?.data, message: error.message, - code: error.code + code: error.code, + url: `${API_BASE_URL}/api/extract`, + isDev: !app.isPackaged, + networkError: error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' }) // Handle API-specific errors @@ -370,6 +391,8 @@ export class ProcessingHelper { } ) + console.log("Response received:", response) + return { success: true, data: response.data } } catch (error: any) { const mainWindow = this.deps.getMainWindow() @@ -543,7 +566,7 @@ export class ProcessingHelper { this.currentProcessingAbortController.abort(); this.currentProcessingAbortController = null; } - + if (this.currentExtraProcessingAbortController) { this.currentExtraProcessingAbortController.abort(); this.currentExtraProcessingAbortController = null; diff --git a/electron/ScreenshotHelper.ts b/electron/ScreenshotHelper.ts index 2d7edb7..172ef18 100644 --- a/electron/ScreenshotHelper.ts +++ b/electron/ScreenshotHelper.ts @@ -184,7 +184,7 @@ export class ScreenshotHelper { public async getImagePreview(filepath: string): Promise { try { const data = await fs.promises.readFile(filepath) - return `data:image/png;base64,${data.toString("base64")}` + return data.toString("base64") } catch (error) { console.error("Error reading image:", error) throw error diff --git a/electron/autoUpdater.ts b/electron/autoUpdater.ts index a5e600c..b34dc80 100644 --- a/electron/autoUpdater.ts +++ b/electron/autoUpdater.ts @@ -5,32 +5,18 @@ import log from "electron-log" export function initAutoUpdater() { console.log("Initializing auto-updater...") - // For testing purposes, we'll allow the auto-updater to run in development mode - // but log a warning + // Skip update checks in development if (!app.isPackaged) { - console.log("Auto-updater running in development mode (for testing)") + console.log("Skipping auto-updater in development mode") + return } - // If we're in development mode, simulate update events for testing - if (!app.isPackaged || !process.env.GH_TOKEN) { - console.log("Auto updater: Development mode, update notifications disabled") - - // Disable all update events - ipcMain.handle("start-update", async () => { - console.log("Update download requested, but updates are disabled") - return { success: true } - }) - - ipcMain.handle("install-update", () => { - console.log("Update installation requested, but updates are disabled") - }) - - // No simulated update events will be sent - + if (!process.env.GH_TOKEN) { + console.error("GH_TOKEN environment variable is not set") return } - // Configure auto updater for production + // Configure auto updater autoUpdater.autoDownload = true autoUpdater.autoInstallOnAppQuit = true autoUpdater.allowDowngrade = true @@ -120,4 +106,4 @@ export function initAutoUpdater() { console.log("Install update requested") autoUpdater.quitAndInstall() }) -} +} \ No newline at end of file diff --git a/electron/ipcHandlers.ts b/electron/ipcHandlers.ts index 2f1c2b4..f494823 100644 --- a/electron/ipcHandlers.ts +++ b/electron/ipcHandlers.ts @@ -1,34 +1,11 @@ // ipcHandlers.ts import { ipcMain, shell } from "electron" -import { randomBytes } from "crypto" import { IIpcHandlerDeps } from "./main" export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { console.log("Initializing IPC handlers") - // Credits handlers - ipcMain.handle("set-initial-credits", async (_event, credits: number) => { - const mainWindow = deps.getMainWindow() - if (!mainWindow) return - - try { - // Set the credits in a way that ensures atomicity - await mainWindow.webContents.executeJavaScript( - `window.__CREDITS__ = ${credits}` - ) - mainWindow.webContents.send("credits-updated", credits) - } catch (error) { - console.error("Error setting initial credits:", error) - throw error - } - }) - - ipcMain.handle("decrement-credits", async () => { - // No need to decrement credits since we're bypassing the credit system - return - }) - // Screenshot queue handlers ipcMain.handle("get-screenshot-queue", () => { return deps.getScreenshotQueue() @@ -61,6 +38,10 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { } ) + ipcMain.handle("ensure-window-visible", async () => { + deps.ensureWindowVisible() + }) + ipcMain.handle( "set-window-dimensions", (event, width: number, height: number) => { @@ -68,6 +49,11 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { } ) + // Window scaling handler + ipcMain.handle("scale-window", (event, { direction }: { direction: "up" | "down" | "reset" }) => { + deps.scaleWindow(direction) + }) + // Screenshot management handlers ipcMain.handle("get-screenshots", async () => { try { @@ -205,6 +191,8 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { } }) + // Mouse interaction handlers removed - using window sizing approach instead + // Window movement handlers ipcMain.handle("trigger-move-left", () => { try { diff --git a/electron/main.ts b/electron/main.ts index 91d86aa..734dce0 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,5 +1,6 @@ import { app, BrowserWindow, screen, shell, ipcMain } from "electron" import path from "path" +import fs from "fs" import { initializeIpcHandlers } from "./ipcHandlers" import { ProcessingHelper } from "./ProcessingHelper" import { ScreenshotHelper } from "./ScreenshotHelper" @@ -7,8 +8,16 @@ import { ShortcutsHelper } from "./shortcuts" import { initAutoUpdater } from "./autoUpdater" import * as dotenv from "dotenv" +// Set the actual maximum displayable size as our baseline +const BASE_WIDTH = 1344; // Actual maximum width (1.0x scale) +const BASE_HEIGHT = 756; // Actual maximum height (1.0x scale) +const MOBILE_RATIO_THRESHOLD = 0.6; // Scale factor below which mobile ratio is used +const MOBILE_ASPECT_RATIO = 9 / 16; // Mobile aspect ratio (9:16) +const DESKTOP_ASPECT_RATIO = 16 / 9; // Desktop aspect ratio (16:9) +const BASE_MOBILE_WIDTH = 400; // Base width for mobile ratio + // Constants -const isDev = process.env.NODE_ENV === "development" || !app.isPackaged +const isDev = process.env.NODE_ENV === "development" // Application State const state = { @@ -22,6 +31,12 @@ const state = { step: 0, currentX: 0, currentY: 0, + scale: 1.0, + isZooming: false, // Added to manage zoom state + isManuallyScaled: false, // Track if user has manually scaled the window + isTogglingVisibility: false, // Track if we're in the middle of show/hide operation + isMoving: false, // Track if we're in the middle of a movement operation + lastResizeCall: 0, // Prevent resize loops // Application helpers screenshotHelper: null as ScreenshotHelper | null, @@ -38,7 +53,7 @@ const state = { UNAUTHORIZED: "processing-unauthorized", NO_SCREENSHOTS: "processing-no-screenshots", OUT_OF_CREDITS: "out-of-credits", - API_KEY_INVALID: "processing-api-key-invalid", + API_KEY_INVALID: "api-key-invalid", INITIAL_START: "initial-start", PROBLEM_EXTRACTED: "problem-extracted", SOLUTION_SUCCESS: "solution-success", @@ -65,7 +80,7 @@ export interface IProcessingHelperDeps { deleteScreenshot: ( path: string ) => Promise<{ success: boolean; error?: string }> - setHasDebugged: (hasDebugged: boolean) => void + setHasDebugged: (value: boolean) => void getHasDebugged: () => boolean PROCESSING_EVENTS: typeof state.PROCESSING_EVENTS } @@ -88,6 +103,8 @@ export interface IShortcutsHelperDeps { export interface IIpcHandlerDeps { getMainWindow: () => BrowserWindow | null setWindowDimensions: (width: number, height: number) => void + ensureWindowVisible: () => void + scaleWindow: (direction: "up" | "down" | "reset") => void getScreenshotQueue: () => string[] getExtraScreenshotQueue: () => string[] deleteScreenshot: ( @@ -101,7 +118,6 @@ export interface IIpcHandlerDeps { toggleMainWindow: () => void clearQueues: () => void setView: (view: "queue" | "solutions" | "debug") => void - setHasDebugged: (value: boolean) => void moveWindowLeft: () => void moveWindowRight: () => void moveWindowUp: () => void @@ -153,6 +169,43 @@ function initializeHelpers() { } as IShortcutsHelperDeps) } +// Auth callback handler + +// Register the interview-coder protocol +if (process.platform === "darwin") { + app.setAsDefaultProtocolClient("interview-coder") +} else { + app.setAsDefaultProtocolClient("interview-coder", process.execPath, [ + path.resolve(process.argv[1] || "") + ]) +} + +// Handle the protocol. In this case, we choose to show an Error Box. +if (process.defaultApp && process.argv.length >= 2) { + app.setAsDefaultProtocolClient("interview-coder", process.execPath, [ + path.resolve(process.argv[1]) + ]) +} + +// Force Single Instance Lock +const gotTheLock = app.requestSingleInstanceLock() + +if (!gotTheLock) { + app.quit() +} else { + app.on("second-instance", (event, commandLine) => { + // Someone tried to run a second instance, we should focus our window. + if (state.mainWindow) { + if (state.mainWindow.isMinimized()) state.mainWindow.restore() + state.mainWindow.focus() + + // Protocol handler removed - no longer using auth callbacks + } + }) +} + +// Auth callback removed as we no longer use Supabase authentication + // Window management functions async function createWindow(): Promise { if (state.mainWindow) { @@ -162,17 +215,50 @@ async function createWindow(): Promise { } const primaryDisplay = screen.getPrimaryDisplay() - const workArea = primaryDisplay.workAreaSize - state.screenWidth = workArea.width - state.screenHeight = workArea.height + const workAreaSize = primaryDisplay.workAreaSize + const workArea = primaryDisplay.workArea + state.screenWidth = workAreaSize.width + state.screenHeight = workAreaSize.height state.step = 60 - state.currentY = 50 + + // Scale down if needed to fit screen with 10% margin + let scale = 1.0 + let windowWidth = BASE_WIDTH + let windowHeight = BASE_HEIGHT + + if (windowWidth > workAreaSize.width * 0.9) { + scale = (workAreaSize.width * 0.9) / BASE_WIDTH + windowWidth = Math.round(BASE_WIDTH * scale) + windowHeight = Math.round(BASE_HEIGHT * scale) + } + if (windowHeight > workAreaSize.height * 0.9) { + scale = (workAreaSize.height * 0.9) / BASE_HEIGHT + windowHeight = Math.round(BASE_HEIGHT * scale) + windowWidth = Math.round(BASE_WIDTH * scale) + } + + // Store initial scale + state.scale = scale + + // Center the window on screen + const centerX = Math.floor(workArea.x + (workAreaSize.width - windowWidth) / 2) + const centerY = Math.floor(workArea.y + (workAreaSize.height - windowHeight) / 2) + + // Store window state + state.windowPosition = { x: centerX, y: centerY } + state.windowSize = { width: windowWidth, height: windowHeight } + state.currentX = centerX + state.currentY = centerY const windowSettings: Electron.BrowserWindowConstructorOptions = { - height: 600, - - x: state.currentX, - y: 50, + width: windowWidth, + height: windowHeight, + minWidth: 300, // Mobile minimum width + minHeight: 533, // Mobile minimum height (16:9 of 300) + maxWidth: 1382, // Hard maximum width + maxHeight: 777, // Hard maximum height + x: centerX, + y: centerY, alwaysOnTop: true, webPreferences: { nodeIntegration: false, @@ -186,18 +272,143 @@ async function createWindow(): Promise { frame: false, transparent: true, fullscreenable: false, - hasShadow: false, + hasShadow: true, + opacity: 1.0, backgroundColor: "#00000000", focusable: true, - skipTaskbar: true, - type: "panel", + skipTaskbar: true, // Hide from taskbar + type: "panel", // Panel type for floating window behavior paintWhenInitiallyHidden: true, titleBarStyle: "hidden", - enableLargerThanScreen: true, - movable: true + enableLargerThanScreen: false, + movable: true, + resizable: true, + roundedCorners: true } state.mainWindow = new BrowserWindow(windowSettings) + + // Force 16:9 aspect ratio and prevent resizing + const resizeController = preventUnwantedResize(state.mainWindow); + state.mainWindow.setAspectRatio(16/9) + + // Handle zoom commands with strict aspect ratio and bounds checking + state.mainWindow.webContents.on('before-input-event', async (event, input) => { + if (input.control && (input.key === '+' || input.key === '-' || input.key === '=')) { + event.preventDefault(); + + try { + resizeController.startZoom(); + + const scaleStep = 0.1; + const minScale = 0.6; // Minimum scale factor + const maxScale = 1.0; // Maximum scale factor + + // Calculate new scale with bounds checking + const currentScale = state.scale || 1.0; + let newScale = input.key === '-' + ? Math.max(minScale, currentScale - scaleStep) + : Math.min(maxScale, currentScale + scaleStep); + + // Round to 2 decimal places to prevent floating point errors + newScale = Math.round(newScale * 100) / 100; + + // Get screen dimensions + const { workArea } = screen.getPrimaryDisplay(); + + // Determine if we should use mobile aspect ratio (9:16) for smaller sizes + const useMobileRatio = newScale <= MOBILE_RATIO_THRESHOLD; + const aspectRatio = useMobileRatio ? MOBILE_ASPECT_RATIO : DESKTOP_ASPECT_RATIO; + + let newWidth, newHeight; + + if (useMobileRatio) { + // Mobile phone aspect ratio (9:16) + const targetMobileWidth = 400; // Base mobile width + // Ensure minimum width is 300 when in mobile aspect ratio + const effectiveScale = Math.max(newScale, 300 / targetMobileWidth); + newWidth = Math.round(targetMobileWidth * effectiveScale); + newHeight = Math.round(newWidth * MOBILE_ASPECT_RATIO); + } else { + // Desktop aspect ratio (16:9) + newWidth = Math.round(BASE_WIDTH * newScale); + newHeight = Math.round(BASE_HEIGHT * newScale); + } + + // Ensure window fits on screen with margins when scaling down + if (newWidth > workArea.width * 0.9 || newHeight > workArea.height * 0.9) { + const maxWidth = Math.round(workArea.width * 0.9); + const maxHeight = Math.round(workArea.height * 0.9); + const scaleForWidth = maxWidth / newWidth; + const scaleForHeight = maxHeight / newHeight; + const constrainingScale = Math.min(scaleForWidth, scaleForHeight); + newWidth = Math.round(newWidth * constrainingScale); + newHeight = Math.round(newHeight * constrainingScale); + } + + // Ensure we don't go below minimum size + if (useMobileRatio) { + if (newWidth < 300) { + newWidth = 300; + newHeight = Math.round(300 * MOBILE_ASPECT_RATIO); + } + } else { + if (newWidth < 640) { + newWidth = 640; + newHeight = Math.round(640 * (9 / 16)); + } + } + + // Center window on screen + const x = Math.round(workArea.x + (workArea.width - newWidth) / 2); + const y = Math.round(workArea.y + (workArea.height - newHeight) / 2); + + // Update state before resize + state.scale = newScale; + state.windowPosition = { x, y }; + state.windowSize = { width: newWidth, height: newHeight }; + + // Disable aspect ratio temporarily to prevent double-resizing + state.mainWindow.setAspectRatio(0); + + // Update window in one atomic operation + await state.mainWindow.setBounds({ x, y, width: newWidth, height: newHeight }, true); + + // Re-enable aspect ratio based on mode + state.mainWindow.setAspectRatio(aspectRatio); + + // Forcefully reset webContents zoom factor to prevent content scaling + state.mainWindow.webContents.setZoomFactor(1.0); + + const ratioText = useMobileRatio ? "9:16 mobile" : "16:9 desktop"; + console.log(`Window scaled to ${newScale}x: ${newWidth}x${newHeight} (${ratioText})`); + + // Send aspect ratio info to renderer for layout changes + state.mainWindow.webContents.send('window-aspect-changed', { + isMobile: useMobileRatio, + width: newWidth, + height: newHeight + }); + + // Send feedback about reaching scale limits + if (newScale === minScale) { + state.mainWindow.webContents.send('scale-feedback', { + message: "Already at minimum scale", + scale: newScale + }); + } else if (newScale === maxScale) { + state.mainWindow.webContents.send('scale-feedback', { + message: "Already at maximum scale", + scale: newScale + }); + } + } catch (error) { + console.error("Error during window scaling:", error); + } finally { + resizeController.endZoom(); + } + } + }); // Add more detailed logging for window events state.mainWindow.webContents.on("did-finish-load", () => { @@ -207,21 +418,43 @@ async function createWindow(): Promise { "did-fail-load", async (event, errorCode, errorDescription) => { console.error("Window failed to load:", errorCode, errorDescription) - // Always try to load the built files on failure - console.log("Attempting to load built files...") - setTimeout(() => { - state.mainWindow?.loadFile(path.join(__dirname, "../dist/index.html")).catch((error) => { - console.error("Failed to load built files on retry:", error) - }) - }, 1000) + if (isDev) { + // In development, retry loading after a short delay + console.log("Retrying to load development server...") + setTimeout(() => { + state.mainWindow?.loadURL("http://localhost:54321").catch((error) => { + console.error("Failed to load dev server on retry:", error) + }) + }, 1000) + } } ) - // Load the app - always load from built files - console.log("Loading application from built files...") - state.mainWindow?.loadFile(path.join(__dirname, "../dist/index.html")).catch((error) => { - console.error("Failed to load built files:", error) - }) + if (isDev) { + // In development, load from the dev server + console.log("Loading from development server: http://localhost:54321") + state.mainWindow.loadURL("http://localhost:54321").catch((error) => { + console.error("Failed to load dev server, falling back to local file:", error) + // Fallback to local file if dev server is not available + const indexPath = path.join(__dirname, "../dist/index.html") + console.log("Falling back to:", indexPath) + if (fs.existsSync(indexPath)) { + state.mainWindow.loadFile(indexPath) + } else { + console.error("Could not find index.html in dist folder") + } + }) + } else { + // In production, load from the built files + const indexPath = path.join(__dirname, "../dist/index.html") + console.log("Loading production build:", indexPath) + + if (fs.existsSync(indexPath)) { + state.mainWindow.loadFile(indexPath) + } else { + console.error("Could not find index.html in dist folder") + } + } // Configure window behavior state.mainWindow.webContents.setZoomFactor(1) @@ -229,9 +462,12 @@ async function createWindow(): Promise { state.mainWindow.webContents.openDevTools() } state.mainWindow.webContents.setWindowOpenHandler(({ url }) => { - // Allow opening URLs in external browser - shell.openExternal(url) - return { action: "deny" } + console.log("Attempting to open URL:", url) + if (url.includes("google.com") || url.includes("supabase.co")) { + shell.openExternal(url) + return { action: "deny" } + } + return { action: "allow" } }) // Enhanced screen capture resistance @@ -265,6 +501,11 @@ async function createWindow(): Promise { state.mainWindow.on("resize", handleWindowResize) state.mainWindow.on("closed", handleWindowClosed) + // Set up content loaded event + state.mainWindow.webContents.on('dom-ready', () => { + console.log("DOM is ready, window behavior initialized"); + }); + // Initialize window state const bounds = state.mainWindow.getBounds() state.windowPosition = { x: bounds.x, y: bounds.y } @@ -272,6 +513,25 @@ async function createWindow(): Promise { state.currentX = bounds.x state.currentY = bounds.y state.isWindowVisible = true + + // Set opacity based on user preferences or hide initially + // Ensure the window is visible for the first launch or if opacity > 0.1 + + // Always make sure window is shown first + state.mainWindow.showInactive(); // Use showInactive for consistency + + // Check if we should hide the window initially (this logic seems to be missing the condition) + const shouldHideInitially = false; // Changed from always hiding to never hiding initially + + if (shouldHideInitially) { + console.log('Initial opacity too low, setting to 0 and hiding window'); + state.mainWindow.setOpacity(0); + state.isWindowVisible = false; + } else { + // Ensure window is fully visible + state.mainWindow.setOpacity(1); + console.log('Window shown with full opacity'); + } } function handleWindowMove(): void { @@ -298,58 +558,120 @@ function handleWindowClosed(): void { // Window visibility functions function hideMainWindow(): void { if (!state.mainWindow?.isDestroyed()) { - const bounds = state.mainWindow.getBounds() - state.windowPosition = { x: bounds.x, y: bounds.y } - state.windowSize = { width: bounds.width, height: bounds.height } - state.mainWindow.setIgnoreMouseEvents(true, { forward: true }) - state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1) - state.mainWindow.setVisibleOnAllWorkspaces(true, { - visibleOnFullScreen: true - }) - state.mainWindow.setOpacity(0) - state.mainWindow.hide() - state.isWindowVisible = false + // Get the current bounds + const bounds = state.mainWindow.getBounds(); + + // Save the position and size + state.windowPosition = { x: bounds.x, y: bounds.y }; + state.windowSize = { width: bounds.width, height: bounds.height }; + + console.log(`Saving window size: ${bounds.width}x${bounds.height}`); + + // Make the window ignore mouse events and forward them + state.mainWindow.setIgnoreMouseEvents(true, { forward: true }); + + // Hide the window by setting opacity to 0 + state.mainWindow.setOpacity(0); + state.isWindowVisible = false; + console.log('Window hidden, opacity set to 0'); } } function showMainWindow(): void { if (!state.mainWindow?.isDestroyed()) { if (state.windowPosition && state.windowSize) { + // Ensure the window size is applied correctly state.mainWindow.setBounds({ - ...state.windowPosition, - ...state.windowSize - }) + x: state.windowPosition.x, + y: state.windowPosition.y, + width: state.windowSize.width, + height: state.windowSize.height + }); + + console.log(`Applying window size: ${state.windowSize.width}x${state.windowSize.height}`); } - state.mainWindow.setIgnoreMouseEvents(false) - state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1) + + // Make the window interactive for UI elements + state.mainWindow.setIgnoreMouseEvents(false); + + state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1); state.mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true - }) - state.mainWindow.setContentProtection(true) - state.mainWindow.setOpacity(0) - state.mainWindow.showInactive() - state.mainWindow.setOpacity(1) - state.isWindowVisible = true + }); + state.mainWindow.setContentProtection(true); + state.mainWindow.setOpacity(0); // Set opacity to 0 before showing + state.mainWindow.show(); // Use show() instead of showInactive() to get focus + + // Force a repaint before showing + state.mainWindow.webContents.invalidate(); + + state.mainWindow.setOpacity(1); // Then set opacity to 1 after showing + + // Ensure window gets focus + setTimeout(() => { + if (state.mainWindow && !state.mainWindow.isDestroyed()) { + state.mainWindow.focus(); + console.log('Window focused after show'); + } + }, 50); + + state.isWindowVisible = true; + console.log('Window shown with show(), opacity set to 1, and focused'); } } function toggleMainWindow(): void { - state.isWindowVisible ? hideMainWindow() : showMainWindow() + console.log(`Toggling window. Current state: ${state.isWindowVisible ? 'visible' : 'hidden'}`); + + state.isTogglingVisibility = true; // Block dimension updates during toggle + + if (state.isWindowVisible) { + hideMainWindow(); + } else { + showMainWindow(); + } + + // Reset flag after a short delay to allow the toggle operation to complete + setTimeout(() => { + state.isTogglingVisibility = false; + }, 1000); // 1 second should be enough for show/hide to complete } // Window movement functions function moveWindowHorizontal(updateFn: (x: number) => number): void { if (!state.mainWindow) return + + // Get the actual current position from the window + const [actualX, actualY] = state.mainWindow.getPosition() + state.currentX = actualX + state.currentY = actualY + state.currentX = updateFn(state.currentX) + + // Set a flag to prevent dimension updates from overriding position changes + state.isMoving = true + state.mainWindow.setPosition( Math.round(state.currentX), Math.round(state.currentY) ) + + // Clear the flag after a short delay + setTimeout(() => { + state.isMoving = false + }, 100) + + console.log(`Window moved to: (${Math.round(state.currentX)}, ${Math.round(state.currentY)})`) } function moveWindowVertical(updateFn: (y: number) => number): void { if (!state.mainWindow) return + // Get the actual current position from the window + const [actualX, actualY] = state.mainWindow.getPosition() + state.currentX = actualX + state.currentY = actualY + const newY = updateFn(state.currentY) // Allow window to go 2/3 off screen in either direction const maxUpLimit = (-(state.windowSize?.height || 0) * 2) / 3 @@ -358,6 +680,7 @@ function moveWindowVertical(updateFn: (y: number) => number): void { // Log the current state and limits console.log({ + actualY, newY, maxUpLimit, maxDownLimit, @@ -369,52 +692,176 @@ function moveWindowVertical(updateFn: (y: number) => number): void { // Only update if within bounds if (newY >= maxUpLimit && newY <= maxDownLimit) { state.currentY = newY + + // Set a flag to prevent dimension updates from overriding position changes + state.isMoving = true + state.mainWindow.setPosition( Math.round(state.currentX), Math.round(state.currentY) ) + + // Clear the flag after a short delay + setTimeout(() => { + state.isMoving = false + }, 100) + + console.log(`Window moved to: (${Math.round(state.currentX)}, ${Math.round(state.currentY)})`) } } // Window dimension functions function setWindowDimensions(width: number, height: number): void { + if (!state.mainWindow?.isDestroyed() && !state.isZooming && !state.isTogglingVisibility && !state.isMoving) { + // If user has manually scaled, preserve those dimensions and ignore content-based resizing + if (state.isManuallyScaled) { + console.log('Skipping content-based resize - window is manually scaled'); + return; + } + + const [currentX, currentY] = state.mainWindow.getPosition(); + const primaryDisplay = screen.getPrimaryDisplay(); + const workArea = primaryDisplay.workAreaSize; + const maxWidth = Math.floor(workArea.width * 0.5); + + // Add padding to ensure all content is visible + const contentWidth = Math.min(width + 32, maxWidth); + const contentHeight = Math.ceil(height + 16); // Add extra padding for height + + // Only resize if dimensions actually changed significantly (avoid micro-adjustments) + const currentBounds = state.mainWindow.getBounds(); + const widthDiff = Math.abs(currentBounds.width - contentWidth); + const heightDiff = Math.abs(currentBounds.height - contentHeight); + + if (widthDiff < 20 && heightDiff < 20) { // Keep the increased threshold + return; + } + + // Check if we're in a resize loop (same dimensions called repeatedly) + const now = Date.now(); + if (state.lastResizeCall && now - state.lastResizeCall < 500) { // Keep the increased debounce + console.log('Preventing resize loop - too frequent calls'); + return; + } + state.lastResizeCall = now; + + // Only resize, don't change position - preserve current position + state.mainWindow.setSize(contentWidth, contentHeight); + + // Update the window size in state + state.windowSize = { + width: contentWidth, + height: contentHeight + }; + + // Update state with current position to keep tracking accurate + state.currentX = currentX; + state.currentY = currentY; + + // Update screen dimensions for movement bounds + state.screenWidth = workArea.width; + state.screenHeight = workArea.height; + + console.log(`Window dimensions updated: ${contentWidth}x${contentHeight} at (${currentX}, ${currentY})`); + } else if (state.isZooming) { + console.log('Skipping setWindowDimensions during scaling operation'); + } else if (state.isTogglingVisibility) { + console.log('Skipping setWindowDimensions during visibility toggle'); + } else if (state.isMoving) { + console.log('Skipping setWindowDimensions during movement operation'); + } +} + +// Function to ensure window is fully visible and opaque +function ensureWindowVisible(): void { if (!state.mainWindow?.isDestroyed()) { - const [currentX, currentY] = state.mainWindow.getPosition() - const primaryDisplay = screen.getPrimaryDisplay() - const workArea = primaryDisplay.workAreaSize - const maxWidth = Math.floor(workArea.width * 0.5) - - state.mainWindow.setBounds({ - x: Math.min(currentX, workArea.width - maxWidth), - y: currentY, - width: Math.min(width + 32, maxWidth), - height: Math.ceil(height) - }) + console.log('[DEBUG] Ensuring window is fully visible') + + // Set opacity to 1 to ensure visibility + state.mainWindow.setOpacity(1) + + // Make sure the window is not hidden + if (!state.isWindowVisible) { + state.mainWindow.showInactive() + state.isWindowVisible = true + } + + // Ensure mouse events are not ignored + state.mainWindow.setIgnoreMouseEvents(false) + + console.log('[DEBUG] Window visibility ensured - opacity: 1, visible:', state.isWindowVisible) } } +// Function to block unwanted resize events +function preventUnwantedResize(window: BrowserWindow) { + let isZooming = false; + let lastBounds = window.getBounds(); + + window.on('will-resize', (e: Electron.Event) => { + if (isZooming) return; + + const bounds = window.getBounds(); + if (bounds.width !== lastBounds.width || bounds.height !== lastBounds.height) { + e.preventDefault(); + window.setBounds(lastBounds); + } + }); + + return { + startZoom: () => { isZooming = true; }, + endZoom: () => { + isZooming = false; + lastBounds = window.getBounds(); + } + }; +} + + // Environment setup function loadEnvVariables() { - try { - dotenv.config() - console.log("Environment variables loaded:", { - NODE_ENV: process.env.NODE_ENV, - // Remove Supabase references - OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY ? "exists" : "missing" - }) - } catch (error) { - console.error("Error loading environment variables:", error) + if (isDev) { + console.log("Loading env variables from:", path.join(process.cwd(), ".env")) + dotenv.config({ path: path.join(process.cwd(), ".env") }) + } else { + console.log( + "Loading env variables from:", + path.join(process.resourcesPath, ".env") + ) + dotenv.config({ path: path.join(process.resourcesPath, ".env") }) } + console.log("Environment variables loaded for open-source version") } // Initialize application async function initializeApp() { try { + // Set custom cache directory to prevent permission issues + const appDataPath = path.join(app.getPath('appData'), 'interview-coder-v1') + const sessionPath = path.join(appDataPath, 'session') + const tempPath = path.join(appDataPath, 'temp') + const cachePath = path.join(appDataPath, 'cache') + + // Create directories if they don't exist + for (const dir of [appDataPath, sessionPath, tempPath, cachePath]) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + } + + app.setPath('userData', appDataPath) + app.setPath('sessionData', sessionPath) + app.setPath('temp', tempPath) + app.setPath('cache', cachePath) + loadEnvVariables() + initializeHelpers() initializeIpcHandlers({ getMainWindow, setWindowDimensions, + ensureWindowVisible, + scaleWindow, getScreenshotQueue, getExtraScreenshotQueue, deleteScreenshot, @@ -426,7 +873,6 @@ async function initializeApp() { toggleMainWindow, clearQueues, setView, - setHasDebugged, moveWindowLeft: () => moveWindowHorizontal((x) => Math.max(-(state.windowSize?.width || 0) / 2, x - state.step) @@ -443,6 +889,11 @@ async function initializeApp() { }) await createWindow() state.shortcutsHelper?.registerGlobalShortcuts() + + // Since the window starts visible, register window-specific shortcuts + if (state.isWindowVisible) { + state.shortcutsHelper?.registerWindowSpecificShortcuts() + } // Initialize auto-updater regardless of environment initAutoUpdater() @@ -457,6 +908,43 @@ async function initializeApp() { } } +// Auth callback handling removed - no longer needed +app.on("open-url", (event, url) => { + console.log("open-url event received:", url) + event.preventDefault() +}) + +// Handle second instance (removed auth callback handling) +app.on("second-instance", (_event, commandLine) => { + console.log("second-instance event received:", commandLine) + + // Focus or create the main window + if (!state.mainWindow) { + createWindow() + } else { + if (state.mainWindow.isMinimized()) state.mainWindow.restore() + state.mainWindow.focus() + } +}) + +// Prevent multiple instances of the app +if (!app.requestSingleInstanceLock()) { + app.quit() +} else { + app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit() + state.mainWindow = null + } + }) +} + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) + // State getter/setter functions function getMainWindow(): BrowserWindow | null { return state.mainWindow @@ -495,6 +983,11 @@ function clearQueues(): void { state.screenshotHelper?.clearQueues() state.problemInfo = null setView("queue") + + // Notify renderer to clear the screenshot queue UI + if (state.mainWindow) { + state.mainWindow.webContents.send("clear-queue") + } } async function takeScreenshot(): Promise { @@ -557,3 +1050,88 @@ export { } app.whenReady().then(initializeApp) + +// Window scaling function +function scaleWindow(direction: "up" | "down" | "reset"): void { + if (!state.mainWindow || state.isZooming) return; + + const primaryDisplay = screen.getPrimaryDisplay(); + const workArea: Electron.Rectangle = primaryDisplay.workArea; // Explicitly type as Electron.Rectangle + + let targetScale = state.scale; + + switch (direction) { + case "up": + if (state.scale >= 1.0) { + // If already at max scale, show visual feedback + state.mainWindow.webContents.send('scale-feedback', { + message: "Already at maximum scale", + scale: 1.0 + }); + return; + } + targetScale = Math.min(state.scale + 0.1, 1.0); + break; + case "down": + targetScale = Math.max(state.scale - 0.1, 0.6); + break; + case "reset": + targetScale = 1.0; + break; + } + + // Determine if mobile ratio should be used + const useMobileRatio = targetScale <= MOBILE_RATIO_THRESHOLD; + const aspectRatio = useMobileRatio ? MOBILE_ASPECT_RATIO : DESKTOP_ASPECT_RATIO; + + let targetWidth, targetHeight; + + if (useMobileRatio) { + // Calculate mobile dimensions (9:16 ratio) + targetWidth = Math.round(BASE_MOBILE_WIDTH * targetScale); + targetHeight = Math.round(targetWidth * MOBILE_ASPECT_RATIO); + } else { + // Calculate desktop dimensions (16:9 ratio) + targetWidth = Math.round(BASE_WIDTH * targetScale); + targetHeight = Math.round(BASE_HEIGHT * targetScale); + } + + // Apply display constraints + const maxWidth = Math.min(targetWidth, Math.floor(workArea.width * 0.9)); + const maxHeight = Math.min(targetHeight, Math.floor(workArea.height * 0.9)); + + // Calculate actual scale based on constraints + const actualScaleX = maxWidth / (useMobileRatio ? BASE_MOBILE_WIDTH : BASE_WIDTH); + const actualScaleY = maxHeight / (useMobileRatio ? (BASE_MOBILE_WIDTH * MOBILE_ASPECT_RATIO) : BASE_HEIGHT); + const actualScale = Math.min(actualScaleX, actualScaleY); + + // Update dimensions with actual scale + let finalWidth, finalHeight; + if (useMobileRatio) { + finalWidth = Math.round(BASE_MOBILE_WIDTH * actualScale); + finalHeight = Math.round(finalWidth * MOBILE_ASPECT_RATIO); + } else { + finalWidth = Math.round(BASE_WIDTH * actualScale); + finalHeight = Math.round(BASE_HEIGHT * actualScale); + } + + // Update window position to maintain centering + const newX = Math.round(workArea.x + (workArea.width - finalWidth) / 2); + const newY = Math.round(workArea.y + (workArea.height - finalHeight) / 2); + + // Update window bounds + state.mainWindow.setBounds({ + x: newX, + y: newY, + width: finalWidth, + height: finalHeight + }); + + // Update state and send feedback + state.scale = actualScale; + state.mainWindow.webContents.send('scale-feedback', { + message: `Scaled to ${actualScale.toFixed(1)}x: ${finalWidth}x${finalHeight} (${useMobileRatio ? '9:16 mobile' : '16:9 desktop'})`, + scale: actualScale, + isMobile: useMobileRatio + }); +} \ No newline at end of file diff --git a/electron/preload.ts b/electron/preload.ts index 6f6fa1e..277adbc 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -20,6 +20,8 @@ interface ElectronAPI { onScreenshotTaken: ( callback: (data: { path: string; preview: string }) => void ) => () => void + onClearQueue: (callback: () => void) => () => void + processScreenshots: () => Promise<{ success: boolean; error?: string }> onResetView: (callback: () => void) => () => void onSolutionStart: (callback: () => void) => () => void onDebugStart: (callback: () => void) => () => void @@ -39,6 +41,10 @@ interface ElectronAPI { triggerMoveRight: () => Promise<{ success: boolean; error?: string }> triggerMoveUp: () => Promise<{ success: boolean; error?: string }> triggerMoveDown: () => Promise<{ success: boolean; error?: string }> + triggerScaleUp: () => Promise<{ success: boolean; error?: string }> + triggerScaleDown: () => Promise<{ success: boolean; error?: string }> + triggerScaleReset: () => Promise<{ success: boolean; error?: string }> + onScaleWindow: (callback: (data: { direction: "up" | "down" | "reset" }) => void) => () => void startUpdate: () => Promise<{ success: boolean; error?: string }> installUpdate: () => void onUpdateAvailable: (callback: (info: any) => void) => () => void @@ -47,6 +53,8 @@ interface ElectronAPI { onCreditsUpdated: (callback: (credits: number) => void) => () => void onOutOfCredits: (callback: () => void) => () => void getPlatform: () => string + enableMouseInteraction: () => Promise<{ success: boolean; error?: string }> + disableMouseInteraction: () => Promise<{ success: boolean; error?: string }> } export const PROCESSING_EVENTS = { @@ -89,6 +97,7 @@ const electronAPI = { throw error } }, + ensureWindowVisible: () => ipcRenderer.invoke("ensure-window-visible"), // Event listeners onScreenshotTaken: ( callback: (data: { path: string; preview: string }) => void @@ -100,6 +109,14 @@ const electronAPI = { ipcRenderer.removeListener("screenshot-taken", subscription) } }, + onClearQueue: (callback: () => void) => { + const subscription = () => callback() + ipcRenderer.on("clear-queue", subscription) + return () => { + ipcRenderer.removeListener("clear-queue", subscription) + } + }, + processScreenshots: () => ipcRenderer.invoke("process-screenshots"), onResetView: (callback: () => void) => { const subscription = () => callback() ipcRenderer.on("reset-view", subscription) @@ -196,6 +213,9 @@ const electronAPI = { triggerMoveRight: () => ipcRenderer.invoke("trigger-move-right"), triggerMoveUp: () => ipcRenderer.invoke("trigger-move-up"), triggerMoveDown: () => ipcRenderer.invoke("trigger-move-down"), + triggerScaleUp: () => ipcRenderer.invoke("scale-window", { direction: "up" }), + triggerScaleDown: () => ipcRenderer.invoke("scale-window", { direction: "down" }), + triggerScaleReset: () => ipcRenderer.invoke("scale-window", { direction: "reset" }), startUpdate: () => ipcRenderer.invoke("start-update"), installUpdate: () => ipcRenderer.invoke("install-update"), onUpdateAvailable: (callback: (info: any) => void) => { @@ -220,7 +240,27 @@ const electronAPI = { ipcRenderer.removeListener("credits-updated", subscription) } }, - getPlatform: () => process.platform + onWindowAspectChanged: (callback: (data: { isMobile: boolean, width: number, height: number }) => void) => { + const subscription = (_event: any, data: { isMobile: boolean, width: number, height: number }) => callback(data) + ipcRenderer.on("window-aspect-changed", subscription) + return () => { + ipcRenderer.removeListener("window-aspect-changed", subscription) + } + }, + onScaleWindow: (callback: (data: { direction: "up" | "down" | "reset" }) => void) => { + const subscription = (_event: any, data: { direction: "up" | "down" | "reset" }) => callback(data) + ipcRenderer.on("scale-window", subscription) + return () => { + ipcRenderer.removeListener("scale-window", subscription) + } + }, + removeAllListeners: (eventName: string) => { + ipcRenderer.removeAllListeners(eventName) + }, + getPlatform: () => process.platform, + // These functions are kept for API compatibility but don't do anything now + enableMouseInteraction: () => Promise.resolve({ success: true }), + disableMouseInteraction: () => Promise.resolve({ success: true }) } as ElectronAPI // Before exposing the API diff --git a/electron/shortcuts.ts b/electron/shortcuts.ts index 4113828..9594120 100644 --- a/electron/shortcuts.ts +++ b/electron/shortcuts.ts @@ -3,23 +3,165 @@ import { IShortcutsHelperDeps } from "./main" export class ShortcutsHelper { private deps: IShortcutsHelperDeps + private lastScaleTime = 0 + private scaleDebounceMs = 150 // Prevent scaling faster than every 150ms + private windowSpecificShortcutsRegistered = false constructor(deps: IShortcutsHelperDeps) { this.deps = deps } + private adjustOpacity(delta: number): void { + const mainWindow = this.deps.getMainWindow(); + if (!mainWindow) return; + + let currentOpacity = mainWindow.getOpacity(); + let newOpacity = Math.max(0.3, Math.min(1.0, currentOpacity + delta)); // Increased minimum from 0.1 to 0.3 + console.log(`Adjusting opacity from ${currentOpacity} to ${newOpacity}`); + + // Add warning for low opacity + if (newOpacity <= 0.5) { + console.warn(`⚠️ Window opacity is getting low (${newOpacity.toFixed(1)}). Use Ctrl+] to restore full opacity.`); + } + + mainWindow.setOpacity(newOpacity); + + // If we're making the window visible, also make sure it's shown and interaction is enabled + // Only show the window if it was fully transparent and is now becoming visible + if (currentOpacity <= 0.1 && newOpacity > 0.1 && !this.deps.isVisible()) { + this.deps.toggleMainWindow(); + } + } + + private setFullOpacity(): void { + const mainWindow = this.deps.getMainWindow(); + if (!mainWindow) return; + + const startOpacity = mainWindow.getOpacity(); + console.log(`Setting full opacity from ${startOpacity} to 1.0 step by step`); + + // First ensure the window is visible and interaction is enabled + if (!this.deps.isVisible()) { + this.deps.toggleMainWindow(); + // After toggling, we need to wait a bit for the window to become visible + setTimeout(() => this.animateOpacity(), 200); + return; // Exit early as the toggleMainWindow already handles visibility + } + + // If already visible, start the animation directly + this.animateOpacity(); + } + + private animateOpacity(): void { + const mainWindow = this.deps.getMainWindow(); + if (!mainWindow) return; + + // Make sure mouse events are enabled as we're making the window visible + mainWindow.setIgnoreMouseEvents(false); + + // Get the current opacity as our starting point + let currentOpacity = mainWindow.getOpacity(); + const targetOpacity = 1.0; + const step = 0.1; + const interval = 50; // milliseconds between steps + + console.log(`Starting opacity animation from ${currentOpacity} to ${targetOpacity}`); + + const opacityInterval = setInterval(() => { + currentOpacity = Math.min(targetOpacity, currentOpacity + step); + mainWindow.setOpacity(currentOpacity); + console.log(`Opacity step: ${currentOpacity.toFixed(1)}`); + + if (currentOpacity >= targetOpacity) { + clearInterval(opacityInterval); + console.log('Full opacity reached (1.0)'); + } + }, interval); + } + + private lastScaleDirection: "up" | "down" | "reset" | null = null; + + private debouncedScale(direction: "up" | "down" | "reset"): void { + const now = Date.now() + if (now - this.lastScaleTime < this.scaleDebounceMs) { + console.log(`Scale debounced (${now - this.lastScaleTime}ms since last)`) + return + } + this.lastScaleTime = now + this.lastScaleDirection = direction + + const mainWindow = this.deps.getMainWindow() + if (mainWindow) { + mainWindow.webContents.send("scale-window", { direction }) + } + } + public registerGlobalShortcuts(): void { + // Always register the toggle shortcut - this should work regardless of window state + globalShortcut.register("CommandOrControl+B", () => { + console.log("Command/Ctrl + B pressed. Toggling window visibility.") + const wasVisible = this.deps.isVisible() + this.deps.toggleMainWindow() + + // Register or unregister window-specific shortcuts based on new state + if (!wasVisible) { + // Window was hidden, now showing - register shortcuts and focus window + this.registerWindowSpecificShortcuts() + this.focusWindow() + } else { + // Window was visible, now hiding - unregister shortcuts + this.unregisterWindowSpecificShortcuts() + } + }) + + // Always register the quit shortcut + globalShortcut.register("CommandOrControl+Q", () => { + console.log("Command/Ctrl + Q pressed. Quitting application.") + app.quit() + }) + + // Unregister shortcuts when quitting + app.on("will-quit", () => { + globalShortcut.unregisterAll() + }) + } + + private focusWindow(): void { + setTimeout(() => { + const mainWindow = this.deps.getMainWindow() + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.focus() + console.log('Window focused after toggle') + } + }, 100) // Small delay to ensure window is fully shown + } + + public registerWindowSpecificShortcuts(): void { + if (this.windowSpecificShortcutsRegistered) { + console.log('Window-specific shortcuts already registered') + return + } + + console.log('Registering window-specific shortcuts...') + globalShortcut.register("CommandOrControl+H", async () => { const mainWindow = this.deps.getMainWindow() if (mainWindow) { console.log("Taking screenshot...") try { const screenshotPath = await this.deps.takeScreenshot() + console.log("Screenshot saved to:", screenshotPath) + const preview = await this.deps.getImagePreview(screenshotPath) - mainWindow.webContents.send("screenshot-taken", { + console.log("Preview generated, length:", preview ? preview.length : 0) + + const eventData = { path: screenshotPath, preview - }) + } + console.log("Sending screenshot-taken event to renderer") + mainWindow.webContents.send("screenshot-taken", eventData) + console.log("screenshot-taken event sent successfully") } catch (error) { console.error("Error capturing screenshot:", error) } @@ -54,7 +196,7 @@ export class ShortcutsHelper { } }) - // New shortcuts for moving the window + // Window movement shortcuts globalShortcut.register("CommandOrControl+Left", () => { console.log("Command/Ctrl + Left pressed. Moving window left.") this.deps.moveWindowLeft() @@ -75,13 +217,81 @@ export class ShortcutsHelper { this.deps.moveWindowUp() }) - globalShortcut.register("CommandOrControl+B", () => { - this.deps.toggleMainWindow() + // Adjust opacity shortcuts + globalShortcut.register("CommandOrControl+[", () => { + console.log("Command/Ctrl + [ pressed. Decreasing opacity.") + this.adjustOpacity(-0.1) }) - // Unregister shortcuts when quitting - app.on("will-quit", () => { - globalShortcut.unregisterAll() + globalShortcut.register("CommandOrControl+]", () => { + console.log("Command/Ctrl + ] pressed. Setting to full opacity.") + this.setFullOpacity() + }) + + // Scale controls (resize the entire window, not zoom content) + globalShortcut.register("CommandOrControl+-", () => { + console.log("Command/Ctrl + - pressed. Scaling window down.") + this.debouncedScale("down") + }) + + globalShortcut.register("CommandOrControl+0", () => { + console.log("Command/Ctrl + 0 pressed. Resetting window scale.") + this.debouncedScale("reset") + }) + + globalShortcut.register("CommandOrControl+=", () => { + console.log("Command/Ctrl + = pressed. Scaling window up.") + this.debouncedScale("up") }) + + // Delete last screenshot shortcut + globalShortcut.register("CommandOrControl+L", () => { + console.log("Command/Ctrl + L pressed. Deleting last screenshot.") + const mainWindow = this.deps.getMainWindow() + if (mainWindow) { + // Send an event to the renderer to delete the last screenshot + mainWindow.webContents.send("delete-last-screenshot") + } + }) + + this.windowSpecificShortcutsRegistered = true + console.log('Window-specific shortcuts registered successfully') + } + + public unregisterWindowSpecificShortcuts(): void { + if (!this.windowSpecificShortcutsRegistered) { + console.log('Window-specific shortcuts already unregistered') + return + } + + console.log('Unregistering window-specific shortcuts...') + + // Unregister all window-specific shortcuts while keeping B and Q + const shortcutsToUnregister = [ + "CommandOrControl+H", + "CommandOrControl+Enter", + "CommandOrControl+R", + "CommandOrControl+Left", + "CommandOrControl+Right", + "CommandOrControl+Down", + "CommandOrControl+Up", + "CommandOrControl+[", + "CommandOrControl+]", + "CommandOrControl+-", + "CommandOrControl+0", + "CommandOrControl+=", + "CommandOrControl+L" + ] + + shortcutsToUnregister.forEach(shortcut => { + try { + globalShortcut.unregister(shortcut) + } catch (error) { + console.error(`Error unregistering shortcut ${shortcut}:`, error) + } + }) + + this.windowSpecificShortcutsRegistered = false + console.log('Window-specific shortcuts unregistered successfully') } -} +} \ No newline at end of file diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 92d1b07..f0d5375 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -15,4 +15,4 @@ "allowSyntheticDefaultImports": true }, "include": ["*.ts"] -} +} \ No newline at end of file diff --git a/index.html b/index.html index c729adf..a4fa0cc 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,11 @@ - - Interview Coder + + Jiminy – The Second Conscience (999) // Set a high default value - const [currentLanguage, setCurrentLanguage] = useState("python") - const [isInitialized, setIsInitialized] = useState(false) - - // Helper function to safely update credits - const updateCredits = useCallback((newCredits: number) => { - setCredits(newCredits) - window.__CREDITS__ = newCredits - }, []) - - // Helper function to safely update language - const updateLanguage = useCallback((newLanguage: string) => { - setCurrentLanguage(newLanguage) - window.__LANGUAGE__ = newLanguage - }, []) - - // Helper function to mark initialization complete - const markInitialized = useCallback(() => { - setIsInitialized(true) - window.__IS_INITIALIZED__ = true - }, []) - - // Show toast method - const showToast = useCallback( - ( - title: string, - description: string, - variant: "neutral" | "success" | "error" - ) => { - setToastState({ - open: true, - title, - description, - variant - }) - }, - [] - ) - - // Initialize app with default values - useEffect(() => { - // Set default values - updateCredits(999) // High number of credits - updateLanguage("python") - markInitialized() - }, [updateCredits, updateLanguage, markInitialized]) - - // Close toast after delay - useEffect(() => { - if (toastState.open) { - const timer = setTimeout(() => { - setToastState((prev) => ({ ...prev, open: false })) - }, 5000) - return () => clearTimeout(timer) - } - }, [toastState.open]) - - // Render the main app directly without authentication check - return ( - - - -
- - - setToastState((prev) => ({ ...prev, open })) - } - variant={toastState.variant} - > -
- {toastState.title && {toastState.title}} - {toastState.description && ( - {toastState.description} - )} -
-
- -
-
-
-
- ) -} - -export default App +import { useEffect, useRef, useState } from "react" +import { + ChatPage, + HomePage, + CodePage, + QueuePage, + SolutionsPage, + MessagesPage, + BrainPage +} from "./pages" +import { ProblemStatementData } from "./types/solutions" +import { IconBulb, IconBrain, IconCode, IconMessage } from "@tabler/icons-react" +import { NavigationBar } from "./components/Navigation/NavigationBar" + +function App() { + const containerRef = useRef(null) + const [isMobileView, setIsMobileView] = useState(false) + const [isMuted, setIsMuted] = useState(true) + + // Navigation state + const [currentPage, setCurrentPage] = useState('home') + + // Screenshot queue state (only used in code mode) + const [screenshotQueue, setScreenshotQueue] = useState>([]) + const [chatInput, setChatInput] = useState("") + const [selectedLanguage, setSelectedLanguage] = useState("javascript") + + // Conversation history + const [conversationHistory, setConversationHistory] = useState + timestamp: number + solutionData?: any + }>>([]) + + // Solution display state (for structured response handling) + const [problemStatementData, setProblemStatementData] = useState(null) + const [solutionData, setSolutionData] = useState(null) + const [thoughtsData, setThoughtsData] = useState(null) + const [timeComplexityData, setTimeComplexityData] = useState(null) + const [spaceComplexityData, setSpaceComplexityData] = useState(null) + const [timeComplexityExplanation, setTimeComplexityExplanation] = useState(null) + const [spaceComplexityExplanation, setSpaceComplexityExplanation] = useState(null) + const [isProcessingSolution, setIsProcessingSolution] = useState(false) + const [processingStage, setProcessingStage] = useState('') + + // Microphone stream ref + const micStreamRef = useRef(null) + + // Initialize global window properties for electron compatibility + useEffect(() => { + // Type assertion to bypass TypeScript error + ;(window as any).__IS_INITIALIZED__ = true + ;(window as any).__LANGUAGE__ = selectedLanguage + ;(window as any).__CREDITS__ = 999 + }, [selectedLanguage]) + + // Navigation handler + const handleNavigate = (page: string) => { + console.log('[DEBUG] Navigating to:', page) + setCurrentPage(page) + + // Clear screenshot queue when leaving code page + if (page !== 'code') { + setScreenshotQueue([]) + } + + // Prevent dimension updates for a short period after navigation + ;(window as any).__lastScaleTime = Date.now() + } + + // Listen for aspect ratio changes from main process + useEffect(() => { + const handleAspectChange = (data: { isMobile: boolean, width: number, height: number }) => { + setIsMobileView(data.isMobile) + } + + if (window.electronAPI?.onWindowAspectChanged) { + window.electronAPI.onWindowAspectChanged(handleAspectChange) + } + + return () => { + if (window.electronAPI?.removeAllListeners) { + window.electronAPI.removeAllListeners('window-aspect-changed') + } + } + }, []) + + // Listen for scale window events from main process + useEffect(() => { + const handleScaleWindow = (data: { direction: "up" | "down" | "reset" }) => { + console.log('Scale window event received:', data.direction) + // Mark that a scale operation just happened + ;(window as any).__lastScaleTime = Date.now() + // The actual scaling is handled by the main process + // This is just for any UI feedback we might want to add + } + + // Add event listener if it exists + if (window.electronAPI?.onScaleWindow) { + const removeListener = window.electronAPI.onScaleWindow(handleScaleWindow) + return removeListener + } + }, []) + + // Dynamic window sizing with constraints + useEffect(() => { + if (!containerRef.current) return + + let resizeObserver: ResizeObserver | null = null + let isWindowFocused = true // Track focus state + + const updateDimensions = () => { + if (!containerRef.current || !isWindowFocused) return // Only update when focused + + // Check if we're in manual scaling mode - if so, don't override + const now = Date.now() + const lastScaleTime = (window as any).__lastScaleTime || 0 + const isRecentlyScaled = (now - lastScaleTime) < 2000 // 2 seconds + + if (isRecentlyScaled) { + console.log('Skipping dimension update - recent manual scaling detected') + return + } + + const height = Math.max(400, Math.min(containerRef.current.scrollHeight, 1000)) + const width = Math.max(600, Math.min(containerRef.current.scrollWidth, 1400)) + + console.log(`[DEBUG] Updating dimensions: ${width}x${height}`) + + if (window.electronAPI?.updateContentDimensions) { + window.electronAPI.updateContentDimensions({ width, height }) + } + } + + const enableResizeObserver = () => { + if (!containerRef.current || resizeObserver) return + + resizeObserver = new ResizeObserver(() => { + // Debounce the dimension updates + clearTimeout((window as any).__dimensionUpdateTimeout) + ;(window as any).__dimensionUpdateTimeout = setTimeout(updateDimensions, 100) + }) + + resizeObserver.observe(containerRef.current) + } + + const disableResizeObserver = () => { + if (resizeObserver) { + resizeObserver.disconnect() + resizeObserver = null + clearTimeout((window as any).__dimensionUpdateTimeout) + } + } + + // Focus/blur event handlers + const handleFocus = () => { + isWindowFocused = true + enableResizeObserver() + } + + const handleBlur = () => { + isWindowFocused = false + disableResizeObserver() + } + + // Listen to window focus/blur events + window.addEventListener('focus', handleFocus) + window.addEventListener('blur', handleBlur) + + // Initialize based on current focus state + if (document.hasFocus()) { + enableResizeObserver() + } else { + isWindowFocused = false + } + + // Initial dimension update (but only if focused) + if (isWindowFocused) { + setTimeout(updateDimensions, 500) + } + + return () => { + disableResizeObserver() + window.removeEventListener('focus', handleFocus) + window.removeEventListener('blur', handleBlur) + } + }, []) + + // Listen for screenshot events from main process (only in code mode) + useEffect(() => { + const handleScreenshotTaken = (data: { path: string, preview: string }) => { + if (currentPage === 'code') { + setScreenshotQueue((prev) => { + const newQueue = [...prev, data] + return newQueue + }) + } + } + + const handleClearQueue = () => { + setScreenshotQueue([]) + } + + if (window.electronAPI?.onScreenshotTaken) { + window.electronAPI.onScreenshotTaken(handleScreenshotTaken) + } + + if (window.electronAPI?.onClearQueue) { + window.electronAPI.onClearQueue(handleClearQueue) + } // Solution processing event listeners + const handleProblemExtracted = (data: any) => { + setProblemStatementData(data) + setProcessingStage('Problem extracted! Generating solutions...') + } + + const handleResetView = () => { + setIsProcessingSolution(false) + setProblemStatementData(null) + setSolutionData(null) + setThoughtsData(null) + setTimeComplexityData(null) + setSpaceComplexityData(null) + setTimeComplexityExplanation(null) + setSpaceComplexityExplanation(null) + setConversationHistory([]) // Clear conversation history on reset + } + + const handleSolutionStart = () => { + setIsProcessingSolution(true) + setProcessingStage('Analyzing your code screenshots...') + + // Only add processing message if there isn't already one + setConversationHistory(prev => { + const hasProcessingMessage = prev.some(msg => msg.type === 'processing'); + if (hasProcessingMessage) return prev; // Prevent duplicates + + return [...prev, { + type: 'processing', + content: 'Analyzing your code screenshots...', + timestamp: Date.now() + }]; + }); + } + + const handleSolutionSuccess = (data: any) => { + setIsProcessingSolution(false) + setProcessingStage('') + + if (!data) { + return + } + + // Parse the JSON response directly + let parsedSolutionData; + try { + parsedSolutionData = JSON.parse(data.code); + } catch (e) { + parsedSolutionData = { Code: data.code }; // Fallback + } + + // Remove processing message and add AI solution response (prevent duplicates) + setConversationHistory(prev => { + const filtered = prev.filter(msg => msg.type !== 'processing'); + + // Check if we already have a similar AI response to prevent duplicates + const hasRecentAIResponse = filtered.some(msg => + msg.type === 'ai' && + msg.solutionData && + Date.now() - msg.timestamp < 5000 // Within last 5 seconds + ); + + if (hasRecentAIResponse) return prev; + + return [...filtered, { + type: 'ai', + content: 'Here\'s your solution! 🚀', + timestamp: Date.now(), + solutionData: parsedSolutionData + }]; + }); + } + + const handleSolutionError = (error: string) => { + setIsProcessingSolution(false) + setProcessingStage('') + + // Remove processing message and add error message + setConversationHistory(prev => { + const filtered = prev.filter(msg => msg.type !== 'processing'); + return [...filtered, { + type: 'ai', + content: `Sorry, there was an error processing your code: ${error}`, + timestamp: Date.now() + }]; + }); + } + + // Register solution processing listeners only for CodePage + if (currentPage === 'code') { + if (window.electronAPI?.onProblemExtracted) { + window.electronAPI.onProblemExtracted(handleProblemExtracted) + } + + if (window.electronAPI?.onResetView) { + window.electronAPI.onResetView(handleResetView) + } + + if (window.electronAPI?.onSolutionStart) { + window.electronAPI.onSolutionStart(handleSolutionStart) + } + + if (window.electronAPI?.onSolutionSuccess) { + window.electronAPI.onSolutionSuccess(handleSolutionSuccess) + } + + if (window.electronAPI?.onSolutionError) { + window.electronAPI.onSolutionError(handleSolutionError) + } + } + + return () => { + // Remove all listeners when component unmounts + if (window.electronAPI?.removeAllListeners) { + window.electronAPI.removeAllListeners('screenshot-taken') + window.electronAPI.removeAllListeners('clear-queue') + window.electronAPI.removeAllListeners('problem-extracted') + window.electronAPI.removeAllListeners('reset-view') + window.electronAPI.removeAllListeners('solution-start') + window.electronAPI.removeAllListeners('solution-success') + window.electronAPI.removeAllListeners('solution-error') + } + } + }, [currentPage]) + + // Handle sending a message in CodePage + const handleSendMessage = () => { + if (!chatInput.trim() && screenshotQueue.length === 0) return + + // Add user message to conversation history + setConversationHistory(prev => [...prev, { + type: 'user', + content: chatInput.trim(), + screenshots: [...screenshotQueue], + timestamp: Date.now() + }]) + + // Clear input and queue + setChatInput('') + setScreenshotQueue([]) + // Trigger AI processing + // This would be an electron API call in the real implementation + setIsProcessingSolution(true); + console.log('Would process solution with:', { + language: selectedLanguage, + message: chatInput.trim() + }) + } + + return ( +
+ {/* Navigation Bar - Positioned absolutely at the top */} +
+
+ +
+
+ {/* Content - Add top padding to avoid overlap with nav */} +
+ {/* Render the appropriate page based on current view */}{currentPage === 'home' && ( + + )} + {currentPage === 'chat' && ( + + )} + {currentPage === 'code' && ( + + )} {currentPage === 'queue' && ( + + )} + {currentPage === 'solutions' && ( + + )} + {currentPage === 'messages' && ( + + )} + {currentPage === 'brain' && ( + + )}
+
+
+ ) +} + +export default App diff --git a/src/_pages/Debug.tsx b/src/_pages/Debug.tsx index 7ff9e1c..3fbfd91 100644 --- a/src/_pages/Debug.tsx +++ b/src/_pages/Debug.tsx @@ -1,269 +1,517 @@ -// Debug.tsx -import { useQuery, useQueryClient } from "@tanstack/react-query" -import React, { useEffect, useRef, useState } from "react" -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" -import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" -import ScreenshotQueue from "../components/Queue/ScreenshotQueue" -import SolutionCommands from "../components/Solutions/SolutionCommands" -import { Screenshot } from "../types/screenshots" -import { ComplexitySection, ContentSection } from "./Solutions" -import { useToast } from "../contexts/toast" - -const CodeSection = ({ - title, - code, - isLoading, - currentLanguage -}: { - title: string - code: React.ReactNode - isLoading: boolean - currentLanguage: string -}) => ( -
-

- {isLoading ? ( -
-
-

- Loading solutions... -

-
-
- ) : ( -
- - {code as string} - -
- )} -
-) - -async function fetchScreenshots(): Promise { - try { - const existing = await window.electronAPI.getScreenshots() - console.log("Raw screenshot data in Debug:", existing) - return (Array.isArray(existing) ? existing : []).map((p) => ({ - id: p.path, - path: p.path, - preview: p.preview, - timestamp: Date.now() - })) - } catch (error) { - console.error("Error loading screenshots:", error) - throw error - } -} - -interface DebugProps { - isProcessing: boolean - setIsProcessing: (isProcessing: boolean) => void - currentLanguage: string - setLanguage: (language: string) => void -} - -const Debug: React.FC = ({ - isProcessing, - setIsProcessing, - currentLanguage, - setLanguage -}) => { - const [tooltipVisible, setTooltipVisible] = useState(false) - const [tooltipHeight, setTooltipHeight] = useState(0) - const { showToast } = useToast() - - const { data: screenshots = [], refetch } = useQuery({ - queryKey: ["screenshots"], - queryFn: fetchScreenshots, - staleTime: Infinity, - gcTime: Infinity, - refetchOnWindowFocus: false - }) - - const [newCode, setNewCode] = useState(null) - const [thoughtsData, setThoughtsData] = useState(null) - const [timeComplexityData, setTimeComplexityData] = useState( - null - ) - const [spaceComplexityData, setSpaceComplexityData] = useState( - null - ) - - const queryClient = useQueryClient() - const contentRef = useRef(null) - - useEffect(() => { - // Try to get the new solution data from cache first - const newSolution = queryClient.getQueryData(["new_solution"]) as { - new_code: string - thoughts: string[] - time_complexity: string - space_complexity: string - } | null - - // If we have cached data, set all state variables to the cached data - if (newSolution) { - setNewCode(newSolution.new_code || null) - setThoughtsData(newSolution.thoughts || null) - setTimeComplexityData(newSolution.time_complexity || null) - setSpaceComplexityData(newSolution.space_complexity || null) - setIsProcessing(false) - } - - // Set up event listeners - const cleanupFunctions = [ - window.electronAPI.onScreenshotTaken(() => refetch()), - window.electronAPI.onResetView(() => refetch()), - window.electronAPI.onDebugSuccess(() => { - setIsProcessing(false) - }), - window.electronAPI.onDebugStart(() => { - setIsProcessing(true) - }), - window.electronAPI.onDebugError((error: string) => { - showToast( - "Processing Failed", - "There was an error debugging your code.", - "error" - ) - setIsProcessing(false) - console.error("Processing error:", error) - }) - ] - - // Set up resize observer - const updateDimensions = () => { - if (contentRef.current) { - let contentHeight = contentRef.current.scrollHeight - const contentWidth = contentRef.current.scrollWidth - if (tooltipVisible) { - contentHeight += tooltipHeight - } - window.electronAPI.updateContentDimensions({ - width: contentWidth, - height: contentHeight - }) - } - } - - const resizeObserver = new ResizeObserver(updateDimensions) - if (contentRef.current) { - resizeObserver.observe(contentRef.current) - } - updateDimensions() - - return () => { - resizeObserver.disconnect() - cleanupFunctions.forEach((cleanup) => cleanup()) - } - }, [queryClient, setIsProcessing]) - - const handleTooltipVisibilityChange = (visible: boolean, height: number) => { - setTooltipVisible(visible) - setTooltipHeight(height) - } - - const handleDeleteExtraScreenshot = async (index: number) => { - const screenshotToDelete = screenshots[index] - - try { - const response = await window.electronAPI.deleteScreenshot( - screenshotToDelete.path - ) - - if (response.success) { - refetch() - } else { - console.error("Failed to delete extra screenshot:", response.error) - } - } catch (error) { - console.error("Error deleting extra screenshot:", error) - } - } - - return ( -
- {/* Conditionally render the screenshot queue */} -
-
-
- -
-
-
- - {/* Navbar of commands with the tooltip */} - - - {/* Main Content */} -
-
-
- {/* Thoughts Section */} - -
- {thoughtsData.map((thought, index) => ( -
-
-
{thought}
-
- ))} -
-
- ) - } - isLoading={!thoughtsData} - /> - - {/* Code Section */} - - - {/* Complexity Section */} - -
-
-
-
- ) -} - -export default Debug +// Debug.tsx +import { useQuery, useQueryClient } from "@tanstack/react-query" +import React, { useEffect, useRef, useState } from "react" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" +import ScreenshotQueue from "../components/Queue/ScreenshotQueue" +import SolutionCommands from "../components/Solutions/SolutionCommands" +import { Screenshot } from "../types/screenshots" +import { ComplexitySection, ContentSection } from "./Solutions" +import { useToast } from "../contexts/toast" +import { CopyButton } from "../components/ui/copy-button" + +const CodeSection = ({ + title, + code, + isLoading, + currentLanguage +}: { + title: string + code: React.ReactNode + isLoading: boolean + currentLanguage: string +}) => ( +
+

+ {isLoading ? ( +
+
+

+ Loading solutions... +

+
+
+ ) : ( +
+ + + {code as string} + +
+ )} +
+) + +async function fetchScreenshots(): Promise { + try { + const existing = await window.electronAPI.getScreenshots() + console.log("Raw screenshot data in Debug:", existing) + return (Array.isArray(existing) ? existing : []).map((p) => ({ + id: p.path, + path: p.path, + preview: p.preview, + timestamp: Date.now() + })) + } catch (error) { + console.error("Error loading screenshots:", error) + throw error + } +} + +interface DebugProps { + isProcessing: boolean + setIsProcessing: (isProcessing: boolean) => void + currentLanguage: string + setLanguage: (language: string) => void +} + +const Debug: React.FC = ({ + isProcessing, + setIsProcessing, + currentLanguage, + setLanguage +}) => { + const [tooltipVisible, setTooltipVisible] = useState(false) + const [tooltipHeight, setTooltipHeight] = useState(0) + const { showToast } = useToast() + + const { data: screenshots = [], refetch } = useQuery({ + queryKey: ["screenshots"], + queryFn: fetchScreenshots, + staleTime: Infinity, + gcTime: Infinity, + refetchOnWindowFocus: false + }) + + const [newCode, setNewCode] = useState(null) + const [thoughtsData, setThoughtsData] = useState(null) + const [timeComplexityData, setTimeComplexityData] = useState( + null + ) + const [spaceComplexityData, setSpaceComplexityData] = useState( + null + ) + const [timeComplexityExplanation, setTimeComplexityExplanation] = useState(null) + const [spaceComplexityExplanation, setSpaceComplexityExplanation] = useState(null) + const [debugAnalysis, setDebugAnalysis] = useState(null) + + const queryClient = useQueryClient() + const contentRef = useRef(null) + + useEffect(() => { + // Try to get the new solution data from cache first + const newSolution = queryClient.getQueryData(["new_solution"]) as { + code: string + debug_analysis: string + thoughts: string[] + time_complexity: string + space_complexity: string + time_complexity_explanation: string + space_complexity_explanation: string + } | null + + // If we have cached data, set all state variables to the cached data + if (newSolution) { + console.log("Found cached debug solution:", newSolution); + + if (newSolution.debug_analysis) { + // Store the debug analysis in its own state variable + setDebugAnalysis(newSolution.debug_analysis); + // Set code separately for the code section + setNewCode(newSolution.code || "// Debug mode - see analysis below"); + + // Process thoughts/analysis points + if (newSolution.debug_analysis.includes('\n\n')) { + const sections = newSolution.debug_analysis.split('\n\n').filter(Boolean); + // Pick first few sections as thoughts + setThoughtsData(sections.slice(0, 3)); + } else { + setThoughtsData(["Debug analysis based on your screenshots"]); + } + } else { + // Fallback to code or default + setNewCode(newSolution.code || "// No analysis available"); + setThoughtsData(newSolution.thoughts || ["Debug analysis based on your screenshots"]); + } + setTimeComplexityData(newSolution.time_complexity || "N/A - Debug mode") + setSpaceComplexityData(newSolution.space_complexity || "N/A - Debug mode") + setTimeComplexityExplanation(newSolution.time_complexity_explanation || null) + setSpaceComplexityExplanation(newSolution.space_complexity_explanation || null) + setIsProcessing(false) + } + + // Set up event listeners + const cleanupFunctions = [ + window.electronAPI.onScreenshotTaken(() => refetch()), + window.electronAPI.onResetView(() => refetch()), + window.electronAPI.onDebugSuccess((data) => { + console.log("Debug success event received with data:", data); + queryClient.setQueryData(["new_solution"], data); + + // Also update local state for immediate rendering + if (data.debug_analysis) { + // Store the debug analysis in its own state variable + setDebugAnalysis(data.debug_analysis); + // Set code separately for the code section + setNewCode(data.code || "// Debug mode - see analysis below"); + + // Process thoughts/analysis points + if (data.debug_analysis.includes('\n\n')) { + const sections = data.debug_analysis.split('\n\n').filter(Boolean); + // Pick first few sections as thoughts + setThoughtsData(sections.slice(0, 3)); + } else if (data.debug_analysis.includes('\n')) { + // Try to find bullet points or numbered lists + const lines = data.debug_analysis.split('\n'); + + const bulletPoints: string[] = lines.filter((line: string): boolean => + Boolean( + line.trim().match(/^[\d*\-•]+\s/) || + line.trim().match(/^[A-Z][\d\.\)\:]/) || + line.includes(':') && line.length < 100 + ) + ); + + if (bulletPoints.length > 0) { + setThoughtsData(bulletPoints.slice(0, 5)); + } else { + setThoughtsData(["Debug analysis based on your screenshots"]); + } + } else { + setThoughtsData(["Debug analysis based on your screenshots"]); + } + } else { + // Fallback to code or default + setNewCode(data.code || "// No analysis available"); + setThoughtsData(data.thoughts || ["Debug analysis based on your screenshots"]); + setDebugAnalysis(null); + } + setTimeComplexityData(data.time_complexity || "N/A - Debug mode"); + setSpaceComplexityData(data.space_complexity || "N/A - Debug mode"); + setTimeComplexityExplanation(data.time_complexity_explanation || null); + setSpaceComplexityExplanation(data.space_complexity_explanation || null); + + setIsProcessing(false); + }), + + window.electronAPI.onDebugStart(() => { + setIsProcessing(true) + }), + window.electronAPI.onDebugError((error: string) => { + showToast( + "Processing Failed", + "There was an error debugging your code.", + "error" + ) + setIsProcessing(false) + console.error("Processing error:", error) + }) + ] + + // Set up resize observer + const updateDimensions = () => { + if (contentRef.current) { + // Get the actual content dimensions + let contentHeight = contentRef.current.scrollHeight + const contentWidth = contentRef.current.scrollWidth + + // Add tooltip height if visible + if (tooltipVisible) { + contentHeight += tooltipHeight + } + + // Add some extra padding to ensure all content is visible + contentHeight += 20 + + console.log(`Debug: Updating content dimensions: ${contentWidth}x${contentHeight}`) + + // Send dimensions to main process + window.electronAPI.updateContentDimensions({ + width: contentWidth, + height: contentHeight + }) + } + } + + // Initialize resize observer with delayed update + const resizeObserver = new ResizeObserver(() => { + // Use setTimeout to ensure we get the final size after all DOM updates + setTimeout(updateDimensions, 0) + }) + + if (contentRef.current) { + resizeObserver.observe(contentRef.current) + } + + // Initial update + updateDimensions() + + // Set up mutation observer to detect DOM changes + const mutationObserver = new MutationObserver(() => { + setTimeout(updateDimensions, 0) + }) + + if (contentRef.current) { + mutationObserver.observe(contentRef.current, { + childList: true, + subtree: true, + attributes: true + }) + } + + return () => { + resizeObserver.disconnect() + mutationObserver.disconnect() + cleanupFunctions.forEach((cleanup) => cleanup()) + } + }, [queryClient, setIsProcessing]) + + const handleTooltipVisibilityChange = (visible: boolean, height: number) => { + setTooltipVisible(visible) + setTooltipHeight(height) + } + + const handleDeleteExtraScreenshot = async (index: number) => { + const screenshotToDelete = screenshots[index] + + try { + const response = await window.electronAPI.deleteScreenshot( + screenshotToDelete.path + ) + + if (response.success) { + refetch() + } else { + console.error("Failed to delete extra screenshot:", response.error) + } + } catch (error) { + console.error("Error deleting extra screenshot:", error) + } + } + + return ( +
+
+ {/* Conditionally render the screenshot queue */} +
+
+
+ +
+
+
+ + {/* Navbar of commands with the tooltip */} + + + {/* Main Content */} +
+
+
+ {/* Thoughts Section */} + +
+ {thoughtsData.map((thought, index) => ( +
+
+
{thought}
+
+ ))} +
+
+ ) + } + isLoading={!thoughtsData} + /> + + {/* Code Section */} + + + {/* Debug Analysis Section */} +
+

Analysis & Improvements

+ {!debugAnalysis ? ( +
+
+

+ Loading debug analysis... +

+
+
+ ) : ( +
+ {/* Process the debug analysis text by sections and lines */} + {(() => { + // First identify key sections based on common patterns in the debug output + interface Section { + title: string; + content: string[]; + } + const sections: Section[] = []; + let currentSection = { title: '', content: [] }; + + // Split by possible section headers (### or ##) + const mainSections = debugAnalysis.split(/(?=^#{1,3}\s|^\*\*\*|^\s*[A-Z][\w\s]+\s*$)/m); + + // Filter out empty sections and process each one + mainSections.filter(Boolean).forEach(sectionText => { + // First line might be a header + const lines = sectionText.split('\n'); + let title = ''; + let startLineIndex = 0; + + // Check if first line is a header + if (lines[0] && (lines[0].startsWith('#') || lines[0].startsWith('**') || + lines[0].match(/^[A-Z][\w\s]+$/) || lines[0].includes('Issues') || + lines[0].includes('Improvements') || lines[0].includes('Optimizations'))) { + title = lines[0].replace(/^#+\s*|\*\*/g, ''); + startLineIndex = 1; + } + + // Add the section + sections.push({ + title, + content: lines.slice(startLineIndex).filter(Boolean) + }); + }); + + // Render the processed sections + return sections.map((section, sectionIndex) => ( +
+ {section.title && ( +
+ {section.title} +
+ )} +
+ {section.content.map((line, lineIndex) => { + // Handle code blocks - detect full code blocks + if (line.trim().startsWith('```')) { + // If we find the start of a code block, collect all lines until the end + if (line.trim() === '```' || line.trim().startsWith('```')) { + // Find end of this code block + const codeBlockEndIndex = section.content.findIndex( + (l, i) => i > lineIndex && l.trim() === '```' + ); + + if (codeBlockEndIndex > lineIndex) { + // Extract language if specified + const langMatch = line.trim().match(/```(\w+)/); + const language = langMatch ? langMatch[1] : ''; + + // Get the code content + const codeContent = section.content + .slice(lineIndex + 1, codeBlockEndIndex) + .join('\n'); + + // Skip ahead in our loop + lineIndex = codeBlockEndIndex; + + return ( +
+ + {codeContent} +
+ ); + } + } + } + + // Handle bullet points + if (line.trim().match(/^[\-*•]\s/) || line.trim().match(/^\d+\.\s/)) { + return ( +
+
+
+ {line.replace(/^[\-*•]\s|^\d+\.\s/, '')} +
+
+ ); + } + + // Handle inline code + if (line.includes('`')) { + const parts = line.split(/(`[^`]+`)/g); + return ( +
+ {parts.map((part, partIndex) => { + if (part.startsWith('`') && part.endsWith('`')) { + return {part.slice(1, -1)}; + } + return {part}; + })} +
+ ); + } + + // Handle sub-headers + if (line.trim().match(/^#+\s/) || (line.trim().match(/^[A-Z][\w\s]+:/) && line.length < 60)) { + return ( +
+ {line.replace(/^#+\s+/, '')} +
+ ); + } + + // Regular text + return
{line}
; + })} +
+
+ )); + })()} +
+ )} +
+ + {/* Complexity Section */} + +
+
+
+
+
+ ) +} + +export default Debug \ No newline at end of file diff --git a/src/_pages/Queue.tsx b/src/_pages/Queue.tsx index 8b8ea78..b9e2235 100644 --- a/src/_pages/Queue.tsx +++ b/src/_pages/Queue.tsx @@ -1,156 +1,165 @@ -import React, { useState, useEffect, useRef } from "react" -import { useQuery } from "@tanstack/react-query" -import ScreenshotQueue from "../components/Queue/ScreenshotQueue" -import QueueCommands from "../components/Queue/QueueCommands" - -import { useToast } from "../contexts/toast" -import { Screenshot } from "../types/screenshots" - -async function fetchScreenshots(): Promise { - try { - const existing = await window.electronAPI.getScreenshots() - return existing - } catch (error) { - console.error("Error loading screenshots:", error) - throw error - } -} - -interface QueueProps { - setView: (view: "queue" | "solutions" | "debug") => void - credits: number - currentLanguage: string - setLanguage: (language: string) => void -} - -const Queue: React.FC = ({ - setView, - credits, - currentLanguage, - setLanguage -}) => { - const { showToast } = useToast() - - const [isTooltipVisible, setIsTooltipVisible] = useState(false) - const [tooltipHeight, setTooltipHeight] = useState(0) - const contentRef = useRef(null) - - const { - data: screenshots = [], - isLoading, - refetch - } = useQuery({ - queryKey: ["screenshots"], - queryFn: fetchScreenshots, - staleTime: Infinity, - gcTime: Infinity, - refetchOnWindowFocus: false - }) - - const handleDeleteScreenshot = async (index: number) => { - const screenshotToDelete = screenshots[index] - - try { - const response = await window.electronAPI.deleteScreenshot( - screenshotToDelete.path - ) - - if (response.success) { - refetch() // Refetch screenshots instead of managing state directly - } else { - console.error("Failed to delete screenshot:", response.error) - showToast("Error", "Failed to delete the screenshot file", "error") - } - } catch (error) { - console.error("Error deleting screenshot:", error) - } - } - - useEffect(() => { - // Height update logic - const updateDimensions = () => { - if (contentRef.current) { - let contentHeight = contentRef.current.scrollHeight - const contentWidth = contentRef.current.scrollWidth - if (isTooltipVisible) { - contentHeight += tooltipHeight - } - window.electronAPI.updateContentDimensions({ - width: contentWidth, - height: contentHeight - }) - } - } - - // Initialize resize observer - const resizeObserver = new ResizeObserver(updateDimensions) - if (contentRef.current) { - resizeObserver.observe(contentRef.current) - } - updateDimensions() - - // Set up event listeners - const cleanupFunctions = [ - window.electronAPI.onScreenshotTaken(() => refetch()), - window.electronAPI.onResetView(() => refetch()), - - window.electronAPI.onSolutionError((error: string) => { - showToast( - "Processing Failed", - "There was an error processing your screenshots.", - "error" - ) - setView("queue") // Revert to queue if processing fails - console.error("Processing error:", error) - }), - window.electronAPI.onProcessingNoScreenshots(() => { - showToast( - "No Screenshots", - "There are no screenshots to process.", - "neutral" - ) - }), - window.electronAPI.onOutOfCredits(() => { - showToast( - "Out of Credits", - "You are out of credits. Please refill at https://www.interviewcoder.co/settings.", - "error" - ) - }) - ] - - return () => { - resizeObserver.disconnect() - cleanupFunctions.forEach((cleanup) => cleanup()) - } - }, [isTooltipVisible, tooltipHeight]) - - const handleTooltipVisibilityChange = (visible: boolean, height: number) => { - setIsTooltipVisible(visible) - setTooltipHeight(height) - } - - return ( -
-
-
- - - -
-
-
- ) -} - -export default Queue +import React, { useState, useEffect, useRef } from "react" +import { useQuery } from "@tanstack/react-query" +import ScreenshotQueue from "../components/Queue/ScreenshotQueue" +import QueueCommands from "../components/Queue/QueueCommands" + +import { useToast } from "../contexts/toast" +import { Screenshot } from "../types/screenshots.ts" + +async function fetchScreenshots(): Promise { + try { + const result = await window.electronAPI.getScreenshots() + if (result.success && result.previews) { + return result.previews.map(preview => ({ + path: preview.path, + preview: preview.preview, + id: preview.path, + timestamp: Date.now(), + name: preview.path.split('\\').pop() || preview.path.split('/').pop() || 'Screenshot' + })) + } + return [] + } catch (error) { + console.error("Error loading screenshots:", error) + throw error + } +} + +interface QueueProps { + setView: (view: "queue" | "solutions" | "debug") => void + credits: number + currentLanguage: string + setLanguage: (language: string) => void +} + +const Queue: React.FC = ({ + setView, + credits, + currentLanguage, + setLanguage +}) => { + const { showToast } = useToast() + + const [isTooltipVisible, setIsTooltipVisible] = useState(false) + const [tooltipHeight, setTooltipHeight] = useState(0) + const contentRef = useRef(null) + + const { + data: screenshots = [], + isLoading, + refetch + } = useQuery({ + queryKey: ["screenshots"], + queryFn: fetchScreenshots, + staleTime: Infinity, + gcTime: Infinity, + refetchOnWindowFocus: false + }) + + const handleDeleteScreenshot = async (index: number) => { + const screenshotToDelete = screenshots[index] + + try { + const response = await window.electronAPI.deleteScreenshot( + screenshotToDelete.path + ) + + if (response.success) { + refetch() // Refetch screenshots instead of managing state directly + } else { + console.error("Failed to delete screenshot:", response.error) + showToast("Error", "Failed to delete the screenshot file", "error") + } + } catch (error) { + console.error("Error deleting screenshot:", error) + } + } + + useEffect(() => { + // Height update logic + const updateDimensions = () => { + if (contentRef.current) { + let contentHeight = contentRef.current.scrollHeight + const contentWidth = contentRef.current.scrollWidth + if (isTooltipVisible) { + contentHeight += tooltipHeight + } + window.electronAPI.updateContentDimensions({ + width: contentWidth, + height: contentHeight + }) + } + } + + // Initialize resize observer + const resizeObserver = new ResizeObserver(updateDimensions) + if (contentRef.current) { + resizeObserver.observe(contentRef.current) + } + updateDimensions() + + // Set up event listeners + const cleanupFunctions = [ + window.electronAPI.onScreenshotTaken(() => refetch()), + window.electronAPI.onResetView(() => refetch()), + + window.electronAPI.onSolutionError((error: string) => { + showToast( + "Processing Failed", + "There was an error processing your screenshots.", + "error" + ) + setView("queue") // Revert to queue if processing fails + console.error("Processing error:", error) + }), + window.electronAPI.onProcessingNoScreenshots(() => { + showToast( + "No Screenshots", + "There are no screenshots to process.", + "neutral" + ) + }), + window.electronAPI.onOutOfCredits(() => { + showToast( + "Out of Credits", + "You are out of credits. Please refill at https://www.interviewcoder.co/settings.", + "error" + ) + }) + ] + + return () => { + resizeObserver.disconnect() + cleanupFunctions.forEach((cleanup) => cleanup()) + } + }, [isTooltipVisible, tooltipHeight]) + + const handleTooltipVisibilityChange = (visible: boolean, height: number) => { + setIsTooltipVisible(visible) + setTooltipHeight(height) + } + + return ( +
+
+
+ + + +
+
+
+ ) +} + +export default Queue \ No newline at end of file diff --git a/src/_pages/Solutions.tsx b/src/_pages/Solutions.tsx index 496aa56..0c7962d 100644 --- a/src/_pages/Solutions.tsx +++ b/src/_pages/Solutions.tsx @@ -1,531 +1,857 @@ -// Solutions.tsx -import React, { useState, useEffect, useRef } from "react" -import { useQuery, useQueryClient } from "@tanstack/react-query" -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" -import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" - -import ScreenshotQueue from "../components/Queue/ScreenshotQueue" - -import { ProblemStatementData } from "../types/solutions" -import SolutionCommands from "../components/Solutions/SolutionCommands" -import Debug from "./Debug" -import { useToast } from "../contexts/toast" -import { COMMAND_KEY } from "../utils/platform" - -export const ContentSection = ({ - title, - content, - isLoading -}: { - title: string - content: React.ReactNode - isLoading: boolean -}) => ( -
-

- {title} -

- {isLoading ? ( -
-

- Extracting problem statement... -

-
- ) : ( -
- {content} -
- )} -
-) -const SolutionSection = ({ - title, - content, - isLoading, - currentLanguage -}: { - title: string - content: React.ReactNode - isLoading: boolean - currentLanguage: string -}) => ( -
-

- {title} -

- {isLoading ? ( -
-
-

- Loading solutions... -

-
-
- ) : ( -
- - {content as string} - -
- )} -
-) - -export const ComplexitySection = ({ - timeComplexity, - spaceComplexity, - isLoading -}: { - timeComplexity: string | null - spaceComplexity: string | null - isLoading: boolean -}) => ( -
-

- Complexity -

- {isLoading ? ( -

- Calculating complexity... -

- ) : ( -
-
-
-
- Time: {timeComplexity} -
-
-
-
-
- Space: {spaceComplexity} -
-
-
- )} -
-) - -export interface SolutionsProps { - setView: (view: "queue" | "solutions" | "debug") => void - credits: number - currentLanguage: string - setLanguage: (language: string) => void -} -const Solutions: React.FC = ({ - setView, - credits, - currentLanguage, - setLanguage -}) => { - const queryClient = useQueryClient() - const contentRef = useRef(null) - - const [debugProcessing, setDebugProcessing] = useState(false) - const [problemStatementData, setProblemStatementData] = - useState(null) - const [solutionData, setSolutionData] = useState(null) - const [thoughtsData, setThoughtsData] = useState(null) - const [timeComplexityData, setTimeComplexityData] = useState( - null - ) - const [spaceComplexityData, setSpaceComplexityData] = useState( - null - ) - - const [isTooltipVisible, setIsTooltipVisible] = useState(false) - const [tooltipHeight, setTooltipHeight] = useState(0) - - const [isResetting, setIsResetting] = useState(false) - - interface Screenshot { - id: string - path: string - preview: string - timestamp: number - } - - const [extraScreenshots, setExtraScreenshots] = useState([]) - - useEffect(() => { - const fetchScreenshots = async () => { - try { - const existing = await window.electronAPI.getScreenshots() - console.log("Raw screenshot data:", existing) - const screenshots = (Array.isArray(existing) ? existing : []).map( - (p) => ({ - id: p.path, - path: p.path, - preview: p.preview, - timestamp: Date.now() - }) - ) - console.log("Processed screenshots:", screenshots) - setExtraScreenshots(screenshots) - } catch (error) { - console.error("Error loading extra screenshots:", error) - setExtraScreenshots([]) - } - } - - fetchScreenshots() - }, [solutionData]) - - const { showToast } = useToast() - - useEffect(() => { - // Height update logic - const updateDimensions = () => { - if (contentRef.current) { - let contentHeight = contentRef.current.scrollHeight - const contentWidth = contentRef.current.scrollWidth - if (isTooltipVisible) { - contentHeight += tooltipHeight - } - window.electronAPI.updateContentDimensions({ - width: contentWidth, - height: contentHeight - }) - } - } - - // Initialize resize observer - const resizeObserver = new ResizeObserver(updateDimensions) - if (contentRef.current) { - resizeObserver.observe(contentRef.current) - } - updateDimensions() - - // Set up event listeners - const cleanupFunctions = [ - window.electronAPI.onScreenshotTaken(async () => { - try { - const existing = await window.electronAPI.getScreenshots() - const screenshots = (Array.isArray(existing) ? existing : []).map( - (p) => ({ - id: p.path, - path: p.path, - preview: p.preview, - timestamp: Date.now() - }) - ) - setExtraScreenshots(screenshots) - } catch (error) { - console.error("Error loading extra screenshots:", error) - } - }), - window.electronAPI.onResetView(() => { - // Set resetting state first - setIsResetting(true) - - // Remove queries - queryClient.removeQueries({ - queryKey: ["solution"] - }) - queryClient.removeQueries({ - queryKey: ["new_solution"] - }) - - // Reset screenshots - setExtraScreenshots([]) - - // After a small delay, clear the resetting state - setTimeout(() => { - setIsResetting(false) - }, 0) - }), - window.electronAPI.onSolutionStart(() => { - // Every time processing starts, reset relevant states - setSolutionData(null) - setThoughtsData(null) - setTimeComplexityData(null) - setSpaceComplexityData(null) - }), - window.electronAPI.onProblemExtracted((data) => { - queryClient.setQueryData(["problem_statement"], data) - }), - //if there was an error processing the initial solution - window.electronAPI.onSolutionError((error: string) => { - showToast("Processing Failed", error, "error") - // Reset solutions in the cache (even though this shouldn't ever happen) and complexities to previous states - const solution = queryClient.getQueryData(["solution"]) as { - code: string - thoughts: string[] - time_complexity: string - space_complexity: string - } | null - if (!solution) { - setView("queue") - } - setSolutionData(solution?.code || null) - setThoughtsData(solution?.thoughts || null) - setTimeComplexityData(solution?.time_complexity || null) - setSpaceComplexityData(solution?.space_complexity || null) - console.error("Processing error:", error) - }), - //when the initial solution is generated, we'll set the solution data to that - window.electronAPI.onSolutionSuccess((data) => { - if (!data) { - console.warn("Received empty or invalid solution data") - return - } - console.log({ data }) - const solutionData = { - code: data.code, - thoughts: data.thoughts, - time_complexity: data.time_complexity, - space_complexity: data.space_complexity - } - - queryClient.setQueryData(["solution"], solutionData) - setSolutionData(solutionData.code || null) - setThoughtsData(solutionData.thoughts || null) - setTimeComplexityData(solutionData.time_complexity || null) - setSpaceComplexityData(solutionData.space_complexity || null) - - // Fetch latest screenshots when solution is successful - const fetchScreenshots = async () => { - try { - const existing = await window.electronAPI.getScreenshots() - const screenshots = - existing.previews?.map((p) => ({ - id: p.path, - path: p.path, - preview: p.preview, - timestamp: Date.now() - })) || [] - setExtraScreenshots(screenshots) - } catch (error) { - console.error("Error loading extra screenshots:", error) - setExtraScreenshots([]) - } - } - fetchScreenshots() - }), - - //######################################################## - //DEBUG EVENTS - //######################################################## - window.electronAPI.onDebugStart(() => { - //we'll set the debug processing state to true and use that to render a little loader - setDebugProcessing(true) - }), - //the first time debugging works, we'll set the view to debug and populate the cache with the data - window.electronAPI.onDebugSuccess((data) => { - queryClient.setQueryData(["new_solution"], data) - setDebugProcessing(false) - }), - //when there was an error in the initial debugging, we'll show a toast and stop the little generating pulsing thing. - window.electronAPI.onDebugError(() => { - showToast( - "Processing Failed", - "There was an error debugging your code.", - "error" - ) - setDebugProcessing(false) - }), - window.electronAPI.onProcessingNoScreenshots(() => { - showToast( - "No Screenshots", - "There are no extra screenshots to process.", - "neutral" - ) - }), - window.electronAPI.onOutOfCredits(() => { - showToast( - "Out of Credits", - "You are out of credits. Please refill at https://www.interviewcoder.co/settings.", - "error" - ) - }) - ] - - return () => { - resizeObserver.disconnect() - cleanupFunctions.forEach((cleanup) => cleanup()) - } - }, [isTooltipVisible, tooltipHeight]) - - useEffect(() => { - setProblemStatementData( - queryClient.getQueryData(["problem_statement"]) || null - ) - setSolutionData(queryClient.getQueryData(["solution"]) || null) - - const unsubscribe = queryClient.getQueryCache().subscribe((event) => { - if (event?.query.queryKey[0] === "problem_statement") { - setProblemStatementData( - queryClient.getQueryData(["problem_statement"]) || null - ) - } - if (event?.query.queryKey[0] === "solution") { - const solution = queryClient.getQueryData(["solution"]) as { - code: string - thoughts: string[] - time_complexity: string - space_complexity: string - } | null - - setSolutionData(solution?.code ?? null) - setThoughtsData(solution?.thoughts ?? null) - setTimeComplexityData(solution?.time_complexity ?? null) - setSpaceComplexityData(solution?.space_complexity ?? null) - } - }) - return () => unsubscribe() - }, [queryClient]) - - const handleTooltipVisibilityChange = (visible: boolean, height: number) => { - setIsTooltipVisible(visible) - setTooltipHeight(height) - } - - const handleDeleteExtraScreenshot = async (index: number) => { - const screenshotToDelete = extraScreenshots[index] - - try { - const response = await window.electronAPI.deleteScreenshot( - screenshotToDelete.path - ) - - if (response.success) { - // Fetch and update screenshots after successful deletion - const existing = await window.electronAPI.getScreenshots() - const screenshots = (Array.isArray(existing) ? existing : []).map( - (p) => ({ - id: p.path, - path: p.path, - preview: p.preview, - timestamp: Date.now() - }) - ) - setExtraScreenshots(screenshots) - } else { - console.error("Failed to delete extra screenshot:", response.error) - showToast("Error", "Failed to delete the screenshot", "error") - } - } catch (error) { - console.error("Error deleting extra screenshot:", error) - showToast("Error", "Failed to delete the screenshot", "error") - } - } - - return ( - <> - {!isResetting && queryClient.getQueryData(["new_solution"]) ? ( - - ) : ( -
- {/* Conditionally render the screenshot queue if solutionData is available */} - {solutionData && ( -
-
-
- -
-
-
- )} - - {/* Navbar of commands with the SolutionsHelper */} - - - {/* Main Content - Modified width constraints */} -
-
-
- {!solutionData && ( - <> - - {problemStatementData && ( -
-

- Generating solutions... -

-
- )} - - )} - - {solutionData && ( - <> - -
- {thoughtsData.map((thought, index) => ( -
-
-
{thought}
-
- ))} -
-
- ) - } - isLoading={!thoughtsData} - /> - - - - - - )} -
-
-
-
- )} - - ) -} - -export default Solutions +// Solutions.tsx +import React, { useState, useEffect, useRef } from "react" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" + +import ScreenshotQueue from "../components/Queue/ScreenshotQueue" + +import { ProblemStatementData } from "../types/solutions" +import { Screenshot } from "../types/screenshots.ts" +import SolutionCommands from "../components/Solutions/SolutionCommands" +import Debug from "./Debug" +import { useToast } from "../contexts/toast" +import { COMMAND_KEY } from "../utils/platform" +import { CopyButton } from "../components/ui/copy-button" + +export const ContentSection = ({ + title, + content, + isLoading +}: { + title: string + content: React.ReactNode + isLoading: boolean +}) => ( +
+

+ {title} +

+ {isLoading ? ( +
+

+ Extracting problem statement... +

+
+ ) : ( +
+ {content} +
+ )} +
+) +const SolutionSection = ({ + title, + content, + isLoading, + currentLanguage +}: { + title: string + content: React.ReactNode + isLoading: boolean + currentLanguage: string +}) => ( +
+

+ {title} +

+ {isLoading ? ( +
+
+

+ Loading solutions... +

+
+
+ ) : ( +
+ + + {content as string} + +
+ )} +
+) + +export const ComplexitySection = ({ + timeComplexity, + spaceComplexity, + timeComplexityExplanation, + spaceComplexityExplanation, + isLoading, +}: { + timeComplexity: string | null; + spaceComplexity: string | null; + timeComplexityExplanation?: string | null; + spaceComplexityExplanation?: string | null; + isLoading: boolean; +}) => ( +
+

+ Complexity +

+ {isLoading ? ( +
+
+

+ Loading complexity analysis... +

+
+
+ ) : ( +
+
+
+ Time: {timeComplexity} +
+ {timeComplexityExplanation && ( +
+ {timeComplexityExplanation} +
+ )} +
+
+
+ Space: {spaceComplexity} +
+ {spaceComplexityExplanation && ( +
+ {spaceComplexityExplanation} +
+ )} +
+
+ )} +
+); + +export interface SolutionsProps { + setView: (view: "queue" | "solutions" | "debug") => void + credits: number + currentLanguage: string + setLanguage: (language: string) => void +} +const Solutions: React.FC = ({ + setView, + credits, + currentLanguage, + setLanguage +}) => { + const queryClient = useQueryClient() + const contentRef = useRef(null) + + const [debugProcessing, setDebugProcessing] = useState(false) + const [problemStatementData, setProblemStatementData] = + useState(null) + const [solutionData, setSolutionData] = useState(null) + const [thoughtsData, setThoughtsData] = useState(null) + const [timeComplexityData, setTimeComplexityData] = useState( + null + ) + const [spaceComplexityData, setSpaceComplexityData] = useState( + null + ) + const [timeComplexityExplanation, setTimeComplexityExplanation] = useState(null) + const [spaceComplexityExplanation, setSpaceComplexityExplanation] = useState(null) + const [isTooltipVisible, setIsTooltipVisible] = useState(false) + const [tooltipHeight, setTooltipHeight] = useState(0) + + const [isResetting, setIsResetting] = useState(false) + + const [extraScreenshots, setExtraScreenshots] = useState([]) + + useEffect(() => { + const fetchScreenshots = async () => { + try { + const existing = await window.electronAPI.getScreenshots() + console.log("Raw screenshot data:", existing) + const screenshots = (Array.isArray(existing) ? existing : []).map( + (p) => ({ + id: p.path, + path: p.path, + preview: p.preview, + timestamp: Date.now() + }) + ) + console.log("Processed screenshots:", screenshots) + setExtraScreenshots(screenshots) + } catch (error) { + console.error("Error loading extra screenshots:", error) + setExtraScreenshots([]) + } + } + + fetchScreenshots() + }, [solutionData]) + + const { showToast } = useToast() + + useEffect(() => { + // Height update logic + const updateDimensions = () => { + if (contentRef.current) { + // Get the actual content dimensions + let contentHeight = contentRef.current.scrollHeight + const contentWidth = contentRef.current.scrollWidth + + // Add tooltip height if visible + if (isTooltipVisible) { + contentHeight += tooltipHeight + } + + // Add some extra padding to ensure all content is visible + contentHeight += 20 + + console.log(`Updating content dimensions: ${contentWidth}x${contentHeight}`) + + // Send dimensions to main process + window.electronAPI.updateContentDimensions({ + width: contentWidth, + height: contentHeight + }) + } + } + + // Initialize resize observer + const resizeObserver = new ResizeObserver(() => { + // Use setTimeout to ensure we get the final size after all DOM updates + setTimeout(updateDimensions, 0) + }) + + if (contentRef.current) { + resizeObserver.observe(contentRef.current) + } + + // Initial update + updateDimensions() + + // Set up event listeners + const cleanupFunctions = [ + window.electronAPI.onScreenshotTaken(async () => { + try { + const existing = await window.electronAPI.getScreenshots() + const screenshots = (Array.isArray(existing) ? existing : []).map( + (p) => ({ + id: p.path, + path: p.path, + preview: p.preview, + timestamp: Date.now() + }) + ) + setExtraScreenshots(screenshots) + } catch (error) { + console.error("Error loading extra screenshots:", error) + } + }), + window.electronAPI.onResetView(() => { + // Set resetting state first + setIsResetting(true) + + // Remove queries + queryClient.removeQueries({ + queryKey: ["solution"] + }) + queryClient.removeQueries({ + queryKey: ["new_solution"] + }) + + // Reset screenshots + setExtraScreenshots([]) + + // After a small delay, clear the resetting state + setTimeout(() => { + setIsResetting(false) + }, 0) + }), // onSolutionStart listener removed - handled by main App.tsx to prevent duplicate conversation messages + // window.electronAPI.onSolutionStart(() => { + // // Every time processing starts, reset relevant states + // setSolutionData(null) + // setThoughtsData(null) + // setTimeComplexityData(null) + // setSpaceComplexityData(null) + // }), + window.electronAPI.onProblemExtracted((data) => { + queryClient.setQueryData(["problem_statement"], data) + }), + //if there was an error processing the initial solution + window.electronAPI.onSolutionError((error: string) => { + showToast("Processing Failed", error, "error") + // Reset solutions in the cache (even though this shouldn't ever happen) and complexities to previous states + const solution = queryClient.getQueryData(["solution"]) as { + code: string + thoughts: string + time_complexity: string + space_complexity: string + } | null + if (!solution) { + setView("queue") + } + setSolutionData(solution?.code || null) + setThoughtsData(solution?.thoughts || null) + setTimeComplexityData(solution?.time_complexity || null) + setSpaceComplexityData(solution?.space_complexity || null) + console.error("Processing error:", error) + }), //when the initial solution is generated, we'll set the solution data to that + // onSolutionSuccess listener removed - handled by main App.tsx to prevent duplicate conversation messages + /* + window.electronAPI.onSolutionSuccess((data) => { + if (!data) { + console.warn("Received empty or invalid solution data") + return + } + console.log("Raw solution data:", data) + + // 1. Parse the JSON string from data.code + let rawSolutionData; + try { + rawSolutionData = JSON.parse(data.code); // Parse the JSON string + } catch (e) { + console.error("Failed to parse JSON solution data:", e); + console.warn("Using raw data.code as is because JSON parsing failed."); + rawSolutionData = data.code; // If parsing fails, use the raw string (less ideal, but prevents crashing) + } + + + // Helper function to format JSON strings or extract content from markdown code blocks + interface ParsedField { + Code?: string + Explanation?: string + "Time Complexity"?: string + "Space Complexity"?: string + complexity_explanation?: string + [key: string]: any + } + + const formatField = (field: unknown): string | null | unknown => { + if (!field) return null + + // If it's a string that looks like JSON, try to parse it + if ( + typeof field === "string" && + (field.trim().startsWith("{") || field.trim().startsWith("[")) + ) { + try { + const parsed = JSON.parse(field) as ParsedField + + // Handle JSON object with code/explanation properties + if (parsed.Code) { + return parsed.Code.replace(/```python\n/g, "") + .replace(/```\n?/g, "") + .trim() + } else if (parsed.Explanation) { + return parsed.Explanation + } else if (parsed["Time Complexity"]) { + return parsed["Time Complexity"] + } else if (parsed["Space Complexity"]) { + return parsed["Space Complexity"] + } else if (parsed.complexity_explanation) { + return parsed.complexity_explanation + } else if (typeof parsed === "object") { + // For other objects, stringify but with formatting + return JSON.stringify(parsed, null, 2) + } + + // If it's a primitive value, return as is + return parsed + } catch (e) { + console.log("Not a valid JSON string, using as is") + } + } + + // If it's a string with markdown code blocks, clean them up + if (typeof field === "string" && field.includes("```")) { + return field.replace(/```python\n/g, "") + .replace(/```\n?/g, "") + .trim() + } + + // Return the original field if no formatting was applied + return field + } + + + // Initialize variables to hold formatted data, using rawSolutionData now + let formattedCode; + let formattedThoughts; + let formattedTimeComplexity; + let formattedSpaceComplexity; + let formattedTimeComplexityExplanation = null; + let formattedSpaceComplexityExplanation = null; + + + if (typeof rawSolutionData === 'object' && rawSolutionData !== null) { + // 2. & 3. Extract and format fields from the parsed JSON object + formattedCode = formatField(rawSolutionData.Code); + formattedThoughts = formatField(rawSolutionData.Explanation); + formattedTimeComplexity = formatField(rawSolutionData["Time Complexity"]); + formattedSpaceComplexity = formatField(rawSolutionData["Space Complexity"]); + + // Process complexity explanation if exists + if (rawSolutionData.complexity_explanation) { + const complexityExplanation = formatField(rawSolutionData.complexity_explanation) as string; + + // Try to split the explanation if it contains both time and space complexity info + if (complexityExplanation && typeof complexityExplanation === 'string') { + if (complexityExplanation.toLowerCase().includes('time') && complexityExplanation.toLowerCase().includes('space')) { + // Try to split by sentences or key phrases + const sentences = complexityExplanation.split('. '); + const timeExplanation = sentences.find(s => s.toLowerCase().includes('time')); + const spaceExplanation = sentences.find(s => s.toLowerCase().includes('space')); + + formattedTimeComplexityExplanation = timeExplanation || complexityExplanation; + formattedSpaceComplexityExplanation = spaceExplanation || complexityExplanation; + } else { + // If we can't clearly separate them, use the full explanation for both + formattedTimeComplexityExplanation = complexityExplanation; + formattedSpaceComplexityExplanation = complexityExplanation; + } + } + } + } else { + // If rawSolutionData is not an object (e.g., JSON parsing failed or wasn't JSON in the first place), + // format the original data.code directly (as a fallback) + formattedCode = formatField(data.code); + formattedThoughts = null; // Or formatField(null) if you want formatField to handle nulls explicitly + formattedTimeComplexity = null; + formattedSpaceComplexity = null; + } + + + console.log("Formatted solution data:", { + code: formattedCode, + thoughts: formattedThoughts, + time_complexity: formattedTimeComplexity, + space_complexity: formattedSpaceComplexity, + time_complexity_explanation: formattedTimeComplexityExplanation, + space_complexity_explanation: formattedSpaceComplexityExplanation + }); + + // 4. Create the solution data object with formatted fields + const solutionData = { + code: formattedCode, + thoughts: formattedThoughts, + time_complexity: formattedTimeComplexity, + space_complexity: formattedSpaceComplexity, + time_complexity_explanation: formattedTimeComplexityExplanation, + space_complexity_explanation: formattedSpaceComplexityExplanation + }; + + console.log("State values being set:", { + solutionDataCode: typeof solutionData.code === "string" ? solutionData.code : null, + thoughtsData: typeof solutionData.thoughts === "string" ? solutionData.thoughts : null, + timeComplexityData: typeof solutionData.time_complexity === "string" ? solutionData.time_complexity : null, + spaceComplexityData: typeof solutionData.space_complexity === "string" ? solutionData.space_complexity : null + }); + + // 5. Update the query cache and state (rest of your original code is fine) + queryClient.setQueryData(["solution"], solutionData); + setSolutionData(typeof solutionData.code === "string" ? solutionData.code : null); + setThoughtsData(typeof solutionData.thoughts === "string" ? solutionData.thoughts : null); + setTimeComplexityData(typeof solutionData.time_complexity === "string" ? solutionData.time_complexity : null); + setSpaceComplexityData(typeof solutionData.space_complexity === "string" ? solutionData.space_complexity : null); + setTimeComplexityExplanation(solutionData.time_complexity_explanation); + setSpaceComplexityExplanation(solutionData.space_complexity_explanation); + + + // Fetch latest screenshots - keep this part as is + const fetchScreenshots = async () => { + try { + const existing = await window.electronAPI.getScreenshots(); + const screenshots = + existing.previews?.map((p) => ({ + id: p.path, + path: p.path, + preview: p.preview, + timestamp: Date.now() + })) || []; + setExtraScreenshots(screenshots); + } catch (error) { + console.error("Error loading extra screenshots:", error); + setExtraScreenshots([]); + } + }; fetchScreenshots(); }), + */ + + // Placeholder to maintain array structure - onSolutionSuccess handling moved to App.tsx + () => () => {}, // No-op function + + //######################################################## + //DEBUG EVENTS + //######################################################## + window.electronAPI.onDebugStart(() => { + //we'll set the debug processing state to true and use that to render a little loader + setDebugProcessing(true) + }), + //the first time debugging works, we'll set the view to debug and populate the cache with the data + window.electronAPI.onDebugSuccess((data) => { + queryClient.setQueryData(["new_solution"], data) + setDebugProcessing(false) + }), + //when there was an error in the initial debugging, we'll show a toast and stop the little generating pulsing thing. + window.electronAPI.onDebugError(() => { + showToast( + "Processing Failed", + "There was an error debugging your code.", + "error" + ) + setDebugProcessing(false) + }), + window.electronAPI.onProcessingNoScreenshots(() => { + showToast( + "No Screenshots", + "There are no extra screenshots to process.", + "neutral" + ) + }), + window.electronAPI.onOutOfCredits(() => { + showToast( + "Out of Credits", + "You are out of credits. Please refill at https://www.interviewcoder.co/settings.", + "error" + ) + }) + ] + + // Set up mutation observer to detect DOM changes + const mutationObserver = new MutationObserver(() => { + setTimeout(updateDimensions, 0) + }) + + if (contentRef.current) { + mutationObserver.observe(contentRef.current, { + childList: true, + subtree: true, + attributes: true + }) + } + + return () => { + resizeObserver.disconnect() + mutationObserver.disconnect() + cleanupFunctions.forEach((cleanup) => cleanup()) + } + }, [isTooltipVisible, tooltipHeight]) + + useEffect(() => { + setProblemStatementData( + queryClient.getQueryData(["problem_statement"]) || null + ) + setSolutionData(queryClient.getQueryData(["solution"]) || null) + + const unsubscribe = queryClient.getQueryCache().subscribe((event) => { + if (event?.query.queryKey[0] === "problem_statement") { + setProblemStatementData( + queryClient.getQueryData(["problem_statement"]) || null + ) + } + if (event?.query.queryKey[0] === "solution") { + const solution = queryClient.getQueryData(["solution"]) as { + code: string + thoughts: string + time_complexity: string + space_complexity: string + } | null + + setSolutionData(solution?.code ?? null) + setThoughtsData(solution?.thoughts ?? null) + setTimeComplexityData(solution?.time_complexity ?? null) + setSpaceComplexityData(solution?.space_complexity ?? null) + } + }) + return () => unsubscribe() + }, [queryClient]) + + const handleTooltipVisibilityChange = (visible: boolean, height: number) => { + setIsTooltipVisible(visible) + setTooltipHeight(height) + } + + const handleDeleteExtraScreenshot = async (index: number) => { + const screenshotToDelete = extraScreenshots[index] + + try { + const response = await window.electronAPI.deleteScreenshot( + screenshotToDelete.path + ) + + if (response.success) { + // Fetch and update screenshots after successful deletion + const existing = await window.electronAPI.getScreenshots() + const screenshots = (Array.isArray(existing) ? existing : []).map( + (p) => ({ + id: p.path, + path: p.path, + preview: p.preview, + timestamp: Date.now() + }) + ) + setExtraScreenshots(screenshots) + } else { + console.error("Failed to delete extra screenshot:", response.error) + showToast("Error", "Failed to delete the screenshot", "error") + } + } catch (error) { + console.error("Error deleting extra screenshot:", error) + showToast("Error", "Failed to delete the screenshot", "error") + } + } + + return ( + <> + {!isResetting && queryClient.getQueryData(["new_solution"]) ? ( + + ) : ( +
+ {/* Conditionally render the screenshot queue if solutionData is available */} + {solutionData && ( +
+
+
+ +
+
+
+ )} + + {/* Navbar of commands with the SolutionsHelper */} + {/* Main Content - Chat Interface */} +
+
+ {/* Chat Messages Container */} +
+ {/* Chat Header */} +
+
+ AI Assistant • Online +
{!solutionData && ( + <> + {/* AI Chat Response for Problem Statement */} +
+
+ AI +
+
+
🔍 Understanding the Problem
+ {!problemStatementData ? ( +
+
+
+
+
+
+
+ Reading the problem... +
+
+ ) : ( + <> +
Here's what I understand:
+
+ {problemStatementData?.problem_statement} +
+ + )} +
+
+
+ {problemStatementData && ( +
+
+ AI +
+
+
+
+
+
+
+
+
+
+ Working through this problem... +
+
+
+
+
+ )} + + )}{solutionData && ( + <> + {/* AI Chat Response Container */} +
+ {/* AI Avatar and Response Bubble for Thoughts */} +
+
+ AI +
+
+
💭 My Analysis
+ {!thoughtsData ? ( +
+
+
+
+
+
+
+ Analyzing the problem... +
+
+ ) : ( +
+ {thoughtsData} +
+ )} +
+
+
+ + {/* AI Avatar and Response Bubble for Solution */} +
+
+ AI +
+
+
✨ Here's my solution
+ {!solutionData ? ( +
+
+
+
+
+
+
+ Crafting the perfect solution... +
+
+ ) : ( +
+ + + {solutionData as string} + +
+ )} +
+
+
+ + {/* AI Avatar and Response Bubble for Complexity */} +
+
+ AI +
+
+
📊 Performance Analysis
+ {!timeComplexityData || !spaceComplexityData ? ( +
+
+
+
+
+
+
+ Calculating performance metrics... +
+
+ ) : ( +
+
+
+ ⏱️ Time Complexity: {timeComplexityData} +
+ {timeComplexityExplanation && ( +
+ {timeComplexityExplanation} +
+ )} +
+
+
+ 💾 Space Complexity: {spaceComplexityData} +
+ {spaceComplexityExplanation && ( +
+ {spaceComplexityExplanation} +
+ )} +
+
+ )} +
+
+
+
+ + )} + {/* Final completion message when all data is available */} + {thoughtsData && solutionData && timeComplexityData && spaceComplexityData && ( +
+
+ +
+
+
+
+ ✨ Analysis complete! I've provided my thoughts, solution, and performance analysis. Feel free to ask if you need clarification on any part! +
+
+
+
+ )} + +
+
+
+
+ )} + + ) +} + +export default Solutions \ No newline at end of file diff --git a/src/_pages/SubscribedApp.tsx b/src/_pages/SubscribedApp.tsx index 5609d3a..18adc2f 100644 --- a/src/_pages/SubscribedApp.tsx +++ b/src/_pages/SubscribedApp.tsx @@ -1,148 +1,147 @@ -// file: src/components/SubscribedApp.tsx -import { useQueryClient } from "@tanstack/react-query" -import { useEffect, useRef, useState } from "react" -import Queue from "../_pages/Queue" -import Solutions from "../_pages/Solutions" -import { useToast } from "../contexts/toast" - -interface SubscribedAppProps { - credits: number - currentLanguage: string - setLanguage: (language: string) => void -} - -const SubscribedApp: React.FC = ({ - credits, - currentLanguage, - setLanguage -}) => { - const queryClient = useQueryClient() - const [view, setView] = useState<"queue" | "solutions" | "debug">("queue") - const containerRef = useRef(null) - const { showToast } = useToast() - - // Let's ensure we reset queries etc. if some electron signals happen - useEffect(() => { - const cleanup = window.electronAPI.onResetView(() => { - queryClient.invalidateQueries({ - queryKey: ["screenshots"] - }) - queryClient.invalidateQueries({ - queryKey: ["problem_statement"] - }) - queryClient.invalidateQueries({ - queryKey: ["solution"] - }) - queryClient.invalidateQueries({ - queryKey: ["new_solution"] - }) - setView("queue") - }) - - return () => { - cleanup() - } - }, []) - - // Dynamically update the window size - useEffect(() => { - if (!containerRef.current) return - - const updateDimensions = () => { - if (!containerRef.current) return - const height = containerRef.current.scrollHeight - const width = containerRef.current.scrollWidth - window.electronAPI?.updateContentDimensions({ width, height }) - } - - const resizeObserver = new ResizeObserver(updateDimensions) - resizeObserver.observe(containerRef.current) - - // Also watch DOM changes - const mutationObserver = new MutationObserver(updateDimensions) - mutationObserver.observe(containerRef.current, { - childList: true, - subtree: true, - attributes: true, - characterData: true - }) - - // Initial dimension update - updateDimensions() - - return () => { - resizeObserver.disconnect() - mutationObserver.disconnect() - } - }, [view]) - - // Listen for events that might switch views or show errors - useEffect(() => { - const cleanupFunctions = [ - window.electronAPI.onSolutionStart(() => { - setView("solutions") - }), - window.electronAPI.onUnauthorized(() => { - queryClient.removeQueries({ - queryKey: ["screenshots"] - }) - queryClient.removeQueries({ - queryKey: ["solution"] - }) - queryClient.removeQueries({ - queryKey: ["problem_statement"] - }) - setView("queue") - }), - window.electronAPI.onResetView(() => { - queryClient.removeQueries({ - queryKey: ["screenshots"] - }) - queryClient.removeQueries({ - queryKey: ["solution"] - }) - queryClient.removeQueries({ - queryKey: ["problem_statement"] - }) - setView("queue") - }), - window.electronAPI.onResetView(() => { - queryClient.setQueryData(["problem_statement"], null) - }), - window.electronAPI.onProblemExtracted((data: any) => { - if (view === "queue") { - queryClient.invalidateQueries({ - queryKey: ["problem_statement"] - }) - queryClient.setQueryData(["problem_statement"], data) - } - }), - window.electronAPI.onSolutionError((error: string) => { - showToast("Error", error, "error") - }) - ] - return () => cleanupFunctions.forEach((fn) => fn()) - }, [view]) - - return ( -
- {view === "queue" ? ( - - ) : view === "solutions" ? ( - - ) : null} -
- ) -} - -export default SubscribedApp +// file: src/components/SubscribedApp.tsx +import { useQueryClient } from "@tanstack/react-query" +import { useEffect, useRef, useState } from "react" +import Queue from "../_pages/Queue" +import Solutions from "../_pages/Solutions" +import { useToast } from "../contexts/toast" + +interface SubscribedAppProps { + credits: number + currentLanguage: string + setLanguage: (language: string) => void +} + +const SubscribedApp: React.FC = ({ + credits, + currentLanguage, + setLanguage +}) => { + const queryClient = useQueryClient() + const [view, setView] = useState<"queue" | "solutions" | "debug">("queue") + const containerRef = useRef(null) + const { showToast } = useToast() + + // Let's ensure we reset queries etc. if some electron signals happen + useEffect(() => { + const cleanup = window.electronAPI.onResetView(() => { + queryClient.invalidateQueries({ + queryKey: ["screenshots"] + }) + queryClient.invalidateQueries({ + queryKey: ["problem_statement"] + }) + queryClient.invalidateQueries({ + queryKey: ["solution"] + }) + queryClient.invalidateQueries({ + queryKey: ["new_solution"] + }) + setView("queue") + }) + + return () => { + cleanup() + } + }, []) + + // Dynamically update the window size + useEffect(() => { + if (!containerRef.current) return + + const updateDimensions = () => { + if (!containerRef.current) return + const height = containerRef.current.scrollHeight + const width = containerRef.current.scrollWidth + window.electronAPI?.updateContentDimensions({ width, height }) + } + + const resizeObserver = new ResizeObserver(updateDimensions) + resizeObserver.observe(containerRef.current) + + // Also watch DOM changes + const mutationObserver = new MutationObserver(updateDimensions) + mutationObserver.observe(containerRef.current, { + childList: true, + subtree: true, + attributes: true, + characterData: true + }) + + // Initial dimension update + updateDimensions() + + return () => { + resizeObserver.disconnect() + mutationObserver.disconnect() + } + }, [view]) + + // Listen for events that might switch views or show errors + useEffect(() => { const cleanupFunctions = [ + // onSolutionStart listener removed - handled by main App.tsx to prevent duplicate conversation messages + // Just keep view switching functionality here + () => () => {}, // Placeholder + window.electronAPI.onUnauthorized(() => { + queryClient.removeQueries({ + queryKey: ["screenshots"] + }) + queryClient.removeQueries({ + queryKey: ["solution"] + }) + queryClient.removeQueries({ + queryKey: ["problem_statement"] + }) + setView("queue") + }), + window.electronAPI.onResetView(() => { + queryClient.removeQueries({ + queryKey: ["screenshots"] + }) + queryClient.removeQueries({ + queryKey: ["solution"] + }) + queryClient.removeQueries({ + queryKey: ["problem_statement"] + }) + setView("queue") + }), + window.electronAPI.onResetView(() => { + queryClient.setQueryData(["problem_statement"], null) + }), + window.electronAPI.onProblemExtracted((data: any) => { + if (view === "queue") { + queryClient.invalidateQueries({ + queryKey: ["problem_statement"] + }) + queryClient.setQueryData(["problem_statement"], data) + } + }), + window.electronAPI.onSolutionError((error: string) => { + showToast("Error", error, "error") + }) + ] + return () => cleanupFunctions.forEach((fn) => fn()) + }, [view]) + + return ( +
+ {view === "queue" ? ( + + ) : view === "solutions" ? ( + + ) : null} +
+ ) +} + +export default SubscribedApp \ No newline at end of file diff --git a/src/components/Navigation/NavigationBar.tsx b/src/components/Navigation/NavigationBar.tsx new file mode 100644 index 0000000..8c16fcf --- /dev/null +++ b/src/components/Navigation/NavigationBar.tsx @@ -0,0 +1,224 @@ +import { useState } from "react" +import { FloatingDock } from "@/components/ui/floating-dock" +import { motion, AnimatePresence } from "motion/react" +import { + IconHome, + IconMicrophone, + IconMicrophoneOff, + IconSettings, + IconUser, + IconMessage, + IconBrain, + IconCode, + IconBulb, + IconSearch, + IconStar, + IconHeart, + IconMenu2, + IconChevronLeft, + IconChevronRight +} from "@tabler/icons-react" + +interface NavigationBarProps { + isMobileView: boolean + isMuted: boolean + setIsMuted: (muted: boolean) => void + onNavigate: (page: string) => void + currentPage: string +} + +export const NavigationBar = ({ + isMobileView, + isMuted, + setIsMuted, + onNavigate, + currentPage +}: NavigationBarProps) => { + const [isNavExpanded, setIsNavExpanded] = useState(false) + const [navScrollIndex, setNavScrollIndex] = useState(0) + + const navItems = [ + { + title: "Home", + icon: , + href: "#", + page: "home", + onClick: () => onNavigate('home') + }, + { + title: "Microphone", + icon: isMuted ? : , + href: "#", + page: "microphone", + onClick: () => setIsMuted(!isMuted) + }, + { + title: "Messages", + icon: , + href: "#", + page: "messages", + onClick: () => onNavigate('messages') + }, + { + title: "Brain", + icon: , + href: "#", + page: "brain", + onClick: () => onNavigate('brain') + }, + { + title: "Code", + icon: , + href: "#", + page: "code", + onClick: () => onNavigate('code') + }, + { + title: "Ideas", + icon: , + href: "#", + page: "ideas", + onClick: () => onNavigate('ideas') + }, + { + title: "Search", + icon: , + href: "#", + page: "search", + onClick: () => onNavigate('search') + }, + { + title: "Favorites", + icon: , + href: "#", + page: "favorites", + onClick: () => onNavigate('favorites') + }, + { + title: "Profile", + icon: , + href: "#", + page: "profile", + onClick: () => onNavigate('profile') + }, + { + title: "Settings", + icon: , + href: "#", + page: "settings", + onClick: () => onNavigate('settings') + }, + ] + + const visibleItems = navItems.slice(navScrollIndex, navScrollIndex + 4) + const canScrollLeft = navScrollIndex > 0 + const canScrollRight = navScrollIndex + 4 < navItems.length + + const scrollNavLeft = () => { + if (canScrollLeft) { + setNavScrollIndex(prev => Math.max(0, prev - 1)) + } + } + + const scrollNavRight = () => { + if (canScrollRight) { + setNavScrollIndex(prev => Math.min(navItems.length - 4, prev + 1)) + } + } + if (!isMobileView) { + // Desktop FloatingDock + return ( +
+ ({ + title: item.title, + icon: item.icon, + href: item.href, + onClick: item.onClick + }))} + desktopClassName="bg-black/50 backdrop-blur-md border border-gray-700/30 shadow-lg hover:shadow-xl transition-all duration-300 shadow-black/30 rounded-full" + /> +
+ ) + } + + // Mobile Collapsible Nav + return ( +
+
+ {/* Collapsed Menu Button */} setIsNavExpanded(!isNavExpanded)} + className="flex items-center justify-center w-12 h-12 bg-black/50 backdrop-blur-md border border-gray-700/30 rounded-full shadow-lg hover:shadow-xl transition-all duration-300 hover:bg-black/60" + animate={{ x: isNavExpanded ? 60 : 0 }} + transition={{ type: "spring", stiffness: 300, damping: 30 }} + > + + + + {/* Expanded Navigation */} + + {isNavExpanded && ( + + {/* Left Scroll Arrow */} + + + {/* Visible Nav Items */} +
+ {visibleItems.map((item, index) => ( + { + e.preventDefault() + if (item.onClick) { + item.onClick() + } + }} + className="w-10 h-10 flex items-center justify-center rounded-full bg-gray-800/50 hover:bg-gray-700/70 transition-all duration-200 group" + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.95 }} + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ delay: index * 0.05 }} + > +
+ {item.icon} +
+
+ ))} +
+ + {/* Right Scroll Arrow */} + +
+ )} +
+
+
+ ) +} diff --git a/src/components/Queue/ScreenshotItem.tsx b/src/components/Queue/ScreenshotItem.tsx index bf4859b..37d8adb 100644 --- a/src/components/Queue/ScreenshotItem.tsx +++ b/src/components/Queue/ScreenshotItem.tsx @@ -1,11 +1,7 @@ // src/components/ScreenshotItem.tsx import React from "react" import { X } from "lucide-react" - -interface Screenshot { - path: string - preview: string -} +import { Screenshot } from "../../types/screenshots.ts" interface ScreenshotItemProps { screenshot: Screenshot @@ -38,7 +34,7 @@ const ScreenshotItem: React.FC = ({
)} Screenshot { + if (isMobileView) return null + + const recommendations = [ + { + icon: IconBulb, + text: "Suggest optimization approaches", + color: "amber" + }, + { + icon: IconCode, + text: "Show code examples", + color: "blue" + }, + { + icon: IconSearch, + text: "Analyze time complexity", + color: "green" + }, + { + icon: IconBrain, + text: "Explain concepts", + color: "purple" + } + ] + + const getColorClasses = (color: string) => { + const colors = { + amber: "text-amber-400 group-hover:text-amber-300", + blue: "text-blue-400 group-hover:text-blue-300", + green: "text-green-400 group-hover:text-green-300", + purple: "text-purple-400 group-hover:text-purple-300" + } + return colors[color as keyof typeof colors] || colors.blue + } + + return ( +
+ {recommendations.map((item, index) => { + const Icon = item.icon + return ( +
+ + + {item.text} + +
+ ) + })} +
+ ) +} diff --git a/src/components/Solutions/SolutionDisplay.tsx b/src/components/Solutions/SolutionDisplay.tsx new file mode 100644 index 0000000..db6010d --- /dev/null +++ b/src/components/Solutions/SolutionDisplay.tsx @@ -0,0 +1,134 @@ +import React from "react" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" +import { CopyButton } from "../ui/copy-button" + +export const ContentSection = ({ + title, + content, + isLoading +}: { + title: string + content: React.ReactNode + isLoading: boolean +}) => ( +
+

+ {title} +

+ {isLoading ? ( +
+

+ Extracting problem statement... +

+
+ ) : ( +
+ {content} +
+ )} +
+) + +export const SolutionSection = ({ + title, + content, + isLoading, + currentLanguage +}: { + title: string + content: React.ReactNode + isLoading: boolean + currentLanguage: string +}) => ( +
+

+ {title} +

+ {isLoading ? ( +
+
+

+ Loading solutions... +

+
+
+ ) : ( +
+ +
+ + {content as string} + +
+
+ )} +
+) + +export const ComplexitySection = ({ + timeComplexity, + spaceComplexity, + timeComplexityExplanation, + spaceComplexityExplanation, + isLoading, +}: { + timeComplexity: string | null; + spaceComplexity: string | null; + timeComplexityExplanation?: string | null; + spaceComplexityExplanation?: string | null; + isLoading: boolean; +}) => ( +
+

+ Complexity +

+ {isLoading ? ( +
+
+

+ Loading complexity analysis... +

+
+
+ ) : ( +
+
+
+ Time: {timeComplexity} +
+ {timeComplexityExplanation && ( +
+ {timeComplexityExplanation} +
+ )} +
+
+
+ Space: {spaceComplexity} +
+ {spaceComplexityExplanation && ( +
+ {spaceComplexityExplanation} +
+ )} +
+
+ )} +
+) diff --git a/src/components/Speech/SpeechVisualization.tsx b/src/components/Speech/SpeechVisualization.tsx new file mode 100644 index 0000000..bf8129e --- /dev/null +++ b/src/components/Speech/SpeechVisualization.tsx @@ -0,0 +1,61 @@ +import { IconMicrophone, IconMessage } from "@tabler/icons-react" + +interface SpeechVisualizationProps { + isMobileView: boolean +} + +export const SpeechVisualization = ({ isMobileView }: SpeechVisualizationProps) => { + if (isMobileView) return null + + return ( +
+ {/* My Speech with Waveform */} +
+
+ + My Speech +
+
+
+
+ {[...Array(32)].map((_, i) => ( +
+ ))} +
+
+
+
+ + {/* Other Speech with Waveform */} +
+
+ + Other Speech +
+
+
+
+ {[...Array(32)].map((_, i) => ( +
+ ))} +
+
+
+
+
+ ) +} diff --git a/src/components/UpdateNotification.tsx b/src/components/UpdateNotification.tsx index a22f737..764b5cc 100644 --- a/src/components/UpdateNotification.tsx +++ b/src/components/UpdateNotification.tsx @@ -97,7 +97,7 @@ export const UpdateNotification: React.FC = () => { {updateDownloaded ? "The update has been downloaded and will be installed when you restart the app." - : "A new version of Interview Coder is available. Please update to continue using the app."} + : "A new version of Jiminy – The Second Conscience is available. Please update to continue using the app."}
{updateDownloaded ? ( diff --git a/src/components/main-layout.tsx b/src/components/main-layout.tsx new file mode 100644 index 0000000..af63a39 --- /dev/null +++ b/src/components/main-layout.tsx @@ -0,0 +1,145 @@ +import { useEffect, useState } from 'react'; + +export const MainLayout = () => { + return ( + <> + {/* Main Content Area */} +
+ {/* Left Side - Main Content Screen */} +
+
+

Current Discussion

+
+
+ Live Transcription +
+
+ +
+
+
+ A +
+
+
Assistant
+
I understand you're trying to implement a new authentication flow. Let's break down the requirements and discuss the best approach for your needs.
+
+
+ +
+
+ U +
+
+
You
+
Yes, we need to implement OAuth2 with support for multiple providers. The main challenge is handling the token refresh...
+
+
+
+
+ + {/* Right Side - Speech Areas */} +
+ {/* My Speech */} +
+
+
+
+ My Speech +
+
+
+ {/* Audio Waveform Visualization */} +
+ {[...Array(32)].map((_, i) => ( +
+ ))} +
+
+
+ Speaking for 2:45 +
+
+
+
+ + {/* Other Speech */} +
+
+
+
+ Assistant Speech +
+
+
+ {/* Audio Waveform Visualization */} +
+ {[...Array(32)].map((_, i) => ( +
+ ))} +
+
+
+ Idle - Waiting for response +
+
+
+
+
+
+ + {/* Bottom Recommendations Grid */} +
+
+
+
+ +
+ Generate Code Documentation +
+ ⌘+1 +
+
+
+
+ +
+ Refactor Implementation +
+ ⌘+2 +
+
+
+
+ +
+ Generate Test Cases +
+ ⌘+3 +
+
+
+
+ +
+ Optimize Performance +
+ ⌘+4 +
+
+ + ); +}; diff --git a/src/components/shared/LanguageSelector.tsx b/src/components/shared/LanguageSelector.tsx index 3943dcf..ef67bf2 100644 --- a/src/components/shared/LanguageSelector.tsx +++ b/src/components/shared/LanguageSelector.tsx @@ -1,26 +1,31 @@ -import React from "react" +import React from "react"; + +declare global { + interface Window { + __LANGUAGE__?: string; + } +} interface LanguageSelectorProps { - currentLanguage: string - setLanguage: (language: string) => void + currentLanguage: string; + setLanguage: (language: string) => void; } export const LanguageSelector: React.FC = ({ currentLanguage, - setLanguage + setLanguage, }) => { const handleLanguageChange = async ( e: React.ChangeEvent ) => { - const newLanguage = e.target.value + const newLanguage = e.target.value; try { - // Just update the language locally - setLanguage(newLanguage) - window.__LANGUAGE__ = newLanguage + setLanguage(newLanguage); + window.__LANGUAGE__ = newLanguage; } catch (error) { - console.error("Error updating language preference:", error) + console.error("Error updating language preference:", error); } - } + }; return (
@@ -29,7 +34,7 @@ export const LanguageSelector: React.FC = ({
- ) -} + ); +}; diff --git a/src/components/ui/copy-button.tsx b/src/components/ui/copy-button.tsx new file mode 100644 index 0000000..06caa5c --- /dev/null +++ b/src/components/ui/copy-button.tsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; +import { Copy, Check } from 'lucide-react'; +import { cn } from '../../lib/utils'; + +interface CopyButtonProps { + text: string; + className?: string; +} + +const CopyButton: React.FC = ({ text, className }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy text: ', err); + } + }; + + return ( + + ); +}; + +export { CopyButton }; diff --git a/src/components/ui/floating-dock.tsx b/src/components/ui/floating-dock.tsx new file mode 100644 index 0000000..554cd0c --- /dev/null +++ b/src/components/ui/floating-dock.tsx @@ -0,0 +1,202 @@ +import { cn } from "@/lib/utils"; +import { IconLayoutNavbarCollapse } from "@tabler/icons-react"; +import { + AnimatePresence, + MotionValue, + motion, + useMotionValue, + useSpring, + useTransform, +} from "motion/react"; + +import { useRef, useState } from "react"; + +export const FloatingDock = ({ + items, + desktopClassName, + mobileClassName, +}: { + items: { title: string; icon: React.ReactNode; href: string; onClick?: () => void }[]; + desktopClassName?: string; + mobileClassName?: string; +}) => { + return ( + <> + + + + ); +}; + +const FloatingDockMobile = ({ + items, + className, +}: { + items: { title: string; icon: React.ReactNode; href: string; onClick?: () => void }[]; + className?: string; +}) => { + const [open, setOpen] = useState(false); + return ( +
+ + {open && ( + {items.map((item, idx) => ( + + + ))} + + )} + + +
+ ); +}; + +const FloatingDockDesktop = ({ + items, + className, +}: { + items: { title: string; icon: React.ReactNode; href: string; onClick?: () => void }[]; + className?: string; +}) => { + let mouseX = useMotionValue(Infinity); + return ( + mouseX.set(e.pageX)} + onMouseLeave={() => mouseX.set(Infinity)} + className={cn( + "mx-auto hidden h-16 items-start gap-4 rounded-2xl bg-gray-50 px-4 pt-3 md:flex dark:bg-neutral-900", + className, + )} + > + {items.map((item) => ( + + ))} + + ); +}; + +function IconContainer({ + mouseX, + title, + icon, + href, + onClick, +}: { + mouseX: MotionValue; + title: string; + icon: React.ReactNode; + href: string; + onClick?: () => void; +}) { + let ref = useRef(null); + + let distance = useTransform(mouseX, (val) => { + let bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }; + + return val - bounds.x - bounds.width / 2; + }); + + let widthTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]); + let heightTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]); + + let widthTransformIcon = useTransform(distance, [-150, 0, 150], [20, 40, 20]); + let heightTransformIcon = useTransform( + distance, + [-150, 0, 150], + [20, 40, 20], + ); + + let width = useSpring(widthTransform, { + mass: 0.1, + stiffness: 150, + damping: 12, + }); + let height = useSpring(heightTransform, { + mass: 0.1, + stiffness: 150, + damping: 12, + }); + + let widthIcon = useSpring(widthTransformIcon, { + mass: 0.1, + stiffness: 150, + damping: 12, + }); + let heightIcon = useSpring(heightTransformIcon, { + mass: 0.1, + stiffness: 150, + damping: 12, + }); + + const [hovered, setHovered] = useState(false); + + return ( + + ); +} diff --git a/src/contexts/toast.tsx b/src/contexts/toast.tsx index 8b0bd4b..16c4942 100644 --- a/src/contexts/toast.tsx +++ b/src/contexts/toast.tsx @@ -1,19 +1,19 @@ -import { createContext, useContext } from "react" - -type ToastVariant = "neutral" | "success" | "error" - -interface ToastContextType { - showToast: (title: string, description: string, variant: ToastVariant) => void -} - -export const ToastContext = createContext( - undefined -) - -export function useToast() { - const context = useContext(ToastContext) - if (!context) { - throw new Error("useToast must be used within a ToastProvider") - } - return context -} +import { createContext, useContext } from "react" + +type ToastVariant = "neutral" | "success" | "error" + +interface ToastContextType { + showToast: (title: string, description: string, variant: ToastVariant) => void +} + +export const ToastContext = createContext( + undefined +) + +export function useToast() { + const context = useContext(ToastContext) + if (!context) { + throw new Error("useToast must be used within a ToastProvider") + } + return context +} \ No newline at end of file diff --git a/src/hooks/useRealtimeSTT.ts b/src/hooks/useRealtimeSTT.ts new file mode 100644 index 0000000..001de7f --- /dev/null +++ b/src/hooks/useRealtimeSTT.ts @@ -0,0 +1,200 @@ +import { useState, useRef, useCallback, useEffect } from 'react'; + +const WEBSOCKET_URL_BASE = 'ws://localhost:3000/ws/jiminy/'; + +export const useRealtimeSTT = () => { + const [transcript, setTranscript] = useState(''); + const [interimTranscript, setInterimTranscript] = useState(''); + const [isStreaming, setIsStreaming] = useState(false); + const [isConnected, setIsConnected] = useState(false); + const [sessionId, setSessionId] = useState(''); + const [isSpeaking, setIsSpeaking] = useState(false); + + // Use a ref to track streaming status to avoid dependency cycles in callbacks + const isStreamingRef = useRef(false); + useEffect(() => { + isStreamingRef.current = isStreaming; + }, [isStreaming]); + + const ws = useRef(null); + const audioContext = useRef(null); + const audioProcessor = useRef(null); + const micStream = useRef(null); + + // Effect to generate a session ID once + useEffect(() => { + setSessionId(crypto.randomUUID()); + }, []); + + const stopStreaming = useCallback(() => { + if (!isStreamingRef.current && !ws.current) return; // Use ref to check status + + console.log('[STT] Stopping streaming...'); + setIsStreaming(false); + + if (micStream.current) { + micStream.current.getTracks().forEach(track => track.stop()); + micStream.current = null; + console.log('[STT] Microphone stream stopped.'); + } + + if (audioProcessor.current) { + audioProcessor.current.disconnect(); + audioProcessor.current.onaudioprocess = null; // Make sure to remove the listener + audioProcessor.current = null; + console.log('[STT] Audio processor disconnected.'); + } + if (audioContext.current && audioContext.current.state !== 'closed') { + audioContext.current.close().then(() => console.log('[STT] Audio context closed.')); + } + + if (ws.current && ws.current.readyState === WebSocket.OPEN) { + // Don't send stop command, just close + ws.current.close(); + console.log('[STT] WebSocket closing.'); + } + ws.current = null; + setIsConnected(false); + }, []); // No dependencies needed now, safe from re-creation + + const startStreaming = useCallback(async () => { + if (isStreamingRef.current) { // Use ref to check status + console.log('[STT] Already streaming.'); + return; + } + + console.log('[STT] Starting streaming process...'); + setIsStreaming(true); // Set state to trigger re-render + setTranscript(''); + setInterimTranscript(''); + + // Ensure we have a session ID before starting + if (!sessionId) { + console.error('[STT] Cannot start streaming without a session ID.'); + return; + } + + try { + console.log('[STT] Getting user media...'); + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + micStream.current = stream; + console.log('[STT] User media obtained.'); + + + console.log('[STT] Creating AudioContext...'); + try { + audioContext.current = new (window.AudioContext || (window as any).webkitAudioContext)({ sampleRate: 16000 }); + } catch (e) { + console.warn('[STT] Failed to set sampleRate, using default. Error:', e); + audioContext.current = new (window.AudioContext || (window as any).webkitAudioContext)(); + } + console.log(`[STT] AudioContext created. Sample rate: ${audioContext.current.sampleRate}Hz`); + + const source = audioContext.current.createMediaStreamSource(stream); + audioProcessor.current = audioContext.current.createScriptProcessor(4096, 1, 1); + console.log('[STT] Audio processor created.'); + + + const websocketUrl = `${WEBSOCKET_URL_BASE}${sessionId}`; + console.log(`[STT] Connecting to WebSocket at ${websocketUrl}...`); + ws.current = new WebSocket(websocketUrl); + setIsStreaming(true); // Set streaming true before ws events + + ws.current.onopen = () => { + console.log('[STT] WebSocket connection OPENED.'); + setIsConnected(true); + + console.log('[STT] Sending start_stt command...'); + ws.current?.send(JSON.stringify({ type: 'start_stt' })); + + console.log('[STT] Attaching audioprocess listener.'); + audioProcessor.current?.addEventListener('audioprocess', (event) => { + if (ws.current?.readyState === WebSocket.OPEN) { + const inputData = event.inputBuffer.getChannelData(0); + // Resample if necessary (simple downsampling) + const sampleRate = audioContext.current?.sampleRate || 44100; + if (sampleRate > 16000) { + const compression = sampleRate / 16000; + const length = Math.floor(inputData.length / compression); + const result = new Float32Array(length); + let index = 0, j = 0; + while (index < length) { + result[index] = inputData[Math.floor(j)]; + j += compression; + index++; + } + const buffer = new Int16Array(result.length); + for (let i = 0; i < result.length; i++) { + buffer[i] = Math.max(-1, Math.min(1, result[i])) * 32767; + } + ws.current.send(buffer.buffer); + } else { + const buffer = new Int16Array(inputData.length); + for (let i = 0; i < inputData.length; i++) { + buffer[i] = Math.max(-1, Math.min(1, inputData[i])) * 32767; + } + ws.current.send(buffer.buffer); + } + } + }); + + console.log('[STT] Connecting audio graph (source -> processor -> destination).'); + if (audioProcessor.current && audioContext.current) { + source.connect(audioProcessor.current); + audioProcessor.current.connect(audioContext.current.destination); + } + }; + + ws.current.onmessage = (event) => { + console.log('[STT] Raw message received from backend:', event.data); + try { + const message = JSON.parse(event.data); + console.log('[STT] Parsed message:', message); + + if (message.event_type === 'transcription') { + setInterimTranscript(message.text || ''); + } else if (message.event_type === 'vad') { + setIsSpeaking(message.is_speech); + if (!message.is_speech && interimTranscript) { + // When speech ends, move interim to final transcript + setTranscript(prev => `${prev} ${interimTranscript}`.trim()); + setInterimTranscript(''); + } + } else if (message.event_type === 'error') { + console.error('[STT] Received error from backend:', message.data); + } else { + console.warn('[STT] Received unhandled message type:', message.event_type, message); + } + } catch (error) { + console.error('[STT] Could not parse message from backend:', event.data, error); + } + }; + + ws.current.onclose = (event) => { + console.log(`[STT] WebSocket connection CLOSED. Code: ${event.code}, Reason: ${event.reason}`); + setIsConnected(false); + stopStreaming(); // Ensure cleanup happens + }; + + ws.current.onerror = (error) => { + console.error('[STT] WebSocket ERROR:', error); + setIsConnected(false); + stopStreaming(); // Ensure cleanup happens + }; + + } catch (error) { + console.error('[STT] Error starting streaming:', error); + setIsStreaming(false); + } + }, [sessionId, stopStreaming]); // Dependencies are now stable + + return { + transcript, + interimTranscript, + isStreaming, + isConnected, + isSpeaking, + startStreaming, + stopStreaming, + }; +}; diff --git a/src/index.css b/src/index.css index 2fb6af3..dc978f4 100644 --- a/src/index.css +++ b/src/index.css @@ -2,9 +2,34 @@ @tailwind components; @tailwind utilities; +/* Hide scrollbars but keep scrolling functionality */ +/* For Webkit browsers (Chrome, Safari) */ +::-webkit-scrollbar { + width: 0; + height: 0; + background: transparent; +} + +/* For Firefox */ +* { + scrollbar-width: none; +} + +/* For IE and Edge */ +* { + -ms-overflow-style: none; +} + +/* Apply overflow auto to elements that need scrolling */ +.scrollable { + overflow: auto; +} + .frosted-glass { background: rgba(26, 26, 26, 0.8); backdrop-filter: blur(8px); + border-radius: 1rem; + overflow: hidden; } .auth-button { @@ -42,3 +67,48 @@ inset: -16px; opacity: 1; } + +/* Conversation scrollbar styles */ +.conversation-scroll::-webkit-scrollbar { + width: 6px; +} + +.conversation-scroll::-webkit-scrollbar-track { + background: #1F2937; + border-radius: 3px; +} + +.conversation-scroll::-webkit-scrollbar-thumb { + background: #4B5563; + border-radius: 3px; +} + +.conversation-scroll::-webkit-scrollbar-thumb:hover { + background: #6B7280; +} + +/* AI response scrollbar styles */ +.ai-response-scroll::-webkit-scrollbar { + width: 4px; +} + +.ai-response-scroll::-webkit-scrollbar-track { + background: #1F2937; + border-radius: 2px; +} + +.ai-response-scroll::-webkit-scrollbar-thumb { + background: #4B5563; + border-radius: 2px; +} + +.ai-response-scroll::-webkit-scrollbar-thumb:hover { + background: #6B7280; +} + +/* Make sure the scroll area uses the full available height */ +.conversation-container { + height: 100%; + max-height: 100%; + overflow: hidden; +} diff --git a/src/pages/BrainPage.tsx b/src/pages/BrainPage.tsx new file mode 100644 index 0000000..d600a1b --- /dev/null +++ b/src/pages/BrainPage.tsx @@ -0,0 +1,169 @@ +import { useEffect } from "react"; +import { useRealtimeSTT } from "../hooks/useRealtimeSTT"; +import { IconBrain, IconBulb, IconCode, IconSearch, IconMicrophone, IconUsers, IconWaveSquare } from "@tabler/icons-react" + +interface BrainPageProps { + isMobileView: boolean; + isMuted: boolean; +} + +export const BrainPage = ({ isMobileView, isMuted }: BrainPageProps) => { + console.log(`[BrainPage] Rendering with isMuted: ${isMuted}`); + + const { + transcript, + interimTranscript, + isStreaming, + isConnected, + startStreaming, + stopStreaming + } = useRealtimeSTT(); + + // Control streaming based on the global mute state + useEffect(() => { + console.log(`[BrainPage] useEffect triggered. isMuted: ${isMuted}`); + if (!isMuted) { + console.log('[BrainPage] Calling startStreaming...'); + startStreaming(); + } else { + console.log('[BrainPage] Calling stopStreaming...'); + stopStreaming(); + } + + // Cleanup on component unmount + return () => { + console.log('[BrainPage] Unmounting, calling stopStreaming for cleanup.'); + stopStreaming(); + } + }, [isMuted, startStreaming, stopStreaming]); + + const recommendations = [ + { + id: 1, + title: "Optimize Algorithm", + description: "Consider using binary search instead of linear search", + icon: IconCode, + color: "blue" + }, + { + id: 2, + title: "Improve Explanation", + description: "Explain your approach step by step", + icon: IconBulb, + color: "yellow" + }, + { + id: 3, + title: "Consider Edge Cases", + description: "Handle null inputs and empty arrays", + icon: IconSearch, + color: "green" + }, + { + id: 4, + title: "Code Structure", + description: "Add comments to clarify complex sections", + icon: IconBrain, + color: "purple" + } + ] + + const getColorClasses = (color: string) => { + const colors = { + blue: "bg-black/40 backdrop-blur-sm border-blue-500/30 text-blue-400 shadow-lg shadow-blue-900/20", + yellow: "bg-black/40 backdrop-blur-sm border-yellow-500/30 text-yellow-400 shadow-lg shadow-yellow-900/20", + green: "bg-black/40 backdrop-blur-sm border-green-500/30 text-green-400 shadow-lg shadow-green-900/20", + purple: "bg-black/40 backdrop-blur-sm border-purple-500/30 text-purple-400 shadow-lg shadow-purple-900/20" + } + return colors[color as keyof typeof colors] || colors.blue + } + + return ( +
+ {/* Main Content Area */} +
+ {/* Main Content Screen (Left Side) */} +
+
+ +

Main Content Screen

+
+ +
+
+
+ +
+

AI Brain Active

+

+ Your conversational AI assistant is ready to help with coding interviews, + algorithm discussions, and technical problem-solving. +

+
+
+
+ + {/* Right Side Panels */} +
+ {/* My Speech Panel */} +
+
+ +

My Speech

+
+
+ +
+

{transcript}

+

{interimTranscript}

+
+
+ + {/* Other Speech Panel */} +
+
+ +

Other Speech

+
+
+ +
+

Recording text and visualization...

+
+ +
+
+
+
+
+
+
+
+ + {/* Bottom Recommendations Row */} +
+ {recommendations.map((recommendation) => { + const Icon = recommendation.icon + return ( +
+
+
+ + + {recommendation.title} + +
+

+ {recommendation.description} +

+
+
+ ) + })} +
+
+ ) +} diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx new file mode 100644 index 0000000..d359869 --- /dev/null +++ b/src/pages/ChatPage.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { IconBrain, IconUser } from "@tabler/icons-react" + +interface ChatPageProps { + isMobileView: boolean +} + +export const ChatPage: React.FC = ({ isMobileView }) => { + return ( +
+ {/* AI Message */} +
+
+ +
+
+

+ I can help you with general questions, creative tasks, or anything you'd like to discuss. What's on your mind? +

+
+
+ + {/* User Message */} +
+
+

+ How do I improve my productivity when working from home? +

+
+
+ +
+
+
+ ) +} diff --git a/src/pages/CodePage.tsx b/src/pages/CodePage.tsx new file mode 100644 index 0000000..c832944 --- /dev/null +++ b/src/pages/CodePage.tsx @@ -0,0 +1,365 @@ +import { useState, useEffect } from "react" +import { IconCode, IconMicrophone, IconMicrophoneOff, IconUser, IconBrain } from "@tabler/icons-react" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" +import { CopyButton } from "../components/ui/copy-button" + +interface CodePageProps { + isMobileView: boolean + screenshotQueue: Array<{path: string, preview: string}> + setScreenshotQueue: (queue: Array<{path: string, preview: string}>) => void + chatInput: string + setChatInput: (input: string) => void + selectedLanguage: string + setSelectedLanguage: (language: string) => void + isMuted: boolean + setIsMuted: (muted: boolean) => void + conversationHistory: Array<{ + type: 'user' | 'ai' | 'processing' + content: string + screenshots?: Array<{path: string, preview: string}> + timestamp: number + solutionData?: any + }> + onSendMessage: () => void +} + +export const CodePage = ({ + isMobileView, + screenshotQueue, + setScreenshotQueue, + chatInput, + setChatInput, + selectedLanguage, + setSelectedLanguage, + isMuted, + setIsMuted, + conversationHistory, + onSendMessage +}: CodePageProps) => { + + const handleSendMessage = async () => { + if (!chatInput.trim() && screenshotQueue.length === 0) return + onSendMessage() + } + + return ( +
+ {/* Code Mode Header */} +
+
+ + Code Analysis Mode +
+
Ctrl+H to capture code
+
+ + {/* Scrollable Conversation Messages */} +
+ + + {/* Initial AI Message */} + {conversationHistory.length === 0 && ( + <> +
+
+ +
+
+

+ Welcome to Code Analysis Mode! 🚀 I can help you analyze code screenshots, debug issues, optimize performance, and explain complex algorithms. Simply capture some code using Ctrl+H and describe what you'd like me to help with. +

+
+
+
+
+

+ Hey! I need help optimizing this algorithm. Let me capture the code first... +

+
+
+ +
+
+ + )} + + {/* Dynamic Conversation History */} + {conversationHistory.map((message, index) => ( +
+ {message.type !== 'user' && ( +
+ +
+ )} + +
+ {message.type === 'user' ? ( + /* User Message with Screenshots */ +
+

+ {message.content} +

+ {message.screenshots && ( +
+ {message.screenshots.map((screenshot, idx) => ( + {`Code + ))} +
+ )} +
+ ) : message.type === 'processing' ? ( + /* Processing Message */ +
+
+
+
+
+ {message.content} +
+
+ ) : ( + /* AI Solution Response */ +
{message.solutionData ? ( +
{/* Debug: Show all available data (remove this after debugging) */} + {process.env.NODE_ENV === 'development' && ( +
+ 🔍 Debug: Available Data +
+                            {JSON.stringify(message.solutionData, null, 2)}
+                          
+
+ )} + + {/* Transparency fix hint */} +
+ 💡 Tip: If window becomes transparent, press Ctrl+] to restore full opacity +
+ + {/* Conversational intro */} +

+ I've analyzed your code screenshots! Here's what I found and how to solve it: +

+ + {/* Problem Information */} + {message.solutionData["Problem Information"] && ( +
+

🎯 Problem Identified:

+

+ {message.solutionData["Problem Information"]} +

+
+ )} {/* Solution code with conversational intro */} + {message.solutionData.Code && ( +
+

+ Here's the optimized solution in {selectedLanguage}: +

+
+ + + {message.solutionData.Code.replace(/```python\n/g, '').replace(/```javascript\n/g, '').replace(/```java\n/g, '').replace(/```cpp\n/g, '').replace(/```\n?/g, '').trim()} + +
+
+ )} + + {/* Explanation/Thoughts */} + {message.solutionData.Explanation && ( +
+

💡 How it works:

+

+ {message.solutionData.Explanation} +

+
+ )} + + {/* Complexity Analysis */} + {(message.solutionData["Time Complexity"] || message.solutionData["Space Complexity"]) && ( +
+

⚡ Performance Analysis:

+
+ {message.solutionData["Time Complexity"] && ( +

+ Time: {message.solutionData["Time Complexity"]} +

+ )} + {message.solutionData["Space Complexity"] && ( +

+ Space: {message.solutionData["Space Complexity"]} +

+ )} +
+ + {/* Detailed complexity explanation */} + {message.solutionData.complexity_explanation && ( +
+

Detailed Analysis:

+

+ {message.solutionData.complexity_explanation} +

+
+ )} +
+ )} +
+ ) : ( +

+ I've processed your request, but there seems to be an issue with the response format. Let me know if you'd like me to try again! +

+ )} +
+ )} +
+ + {message.type === 'user' && ( +
+ +
+ )} +
+ ))} +
+ + {/* Fixed Input Section at Bottom */} +
+ {/* Screenshot Queue - Only show in code mode */} + {screenshotQueue.length > 0 && ( +
+
+ Screenshots ({screenshotQueue.length}) + +
+
+ {screenshotQueue.map((screenshot, index) => ( +
+ {`Screenshot +
+ ))} +
+
+ )} + + {/* Language Selector */} +
+ + +
+ + {/* Text Input */} +
+ setChatInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + handleSendMessage() + } else if (e.key === 'Enter' && !e.shiftKey) { + handleSendMessage() + } + }} + placeholder="Describe what you'd like me to help with regarding your code..." + className={`w-full bg-gray-800/40 hover:bg-gray-800/60 focus:bg-gray-800/80 transition-all duration-300 rounded-lg border border-gray-700/50 ${isMobileView ? 'py-2 px-3 text-xs' : 'py-3 px-4 text-sm'} text-gray-300 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500/50 pr-12`} + /> + +
+ + {/* Mode-specific Help Text */} +
+ Press Ctrl+H to capture code screenshots • Ctrl+R to clear queue • Enter to analyze +
+
+
+ ) +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 0000000..8ecb445 --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1,120 @@ +import { IconHome, IconBrain } from "@tabler/icons-react" + +interface HomePageProps { + isMobileView: boolean +} + +export const HomePage = ({ isMobileView }: HomePageProps) => { + return ( +
+ {/* Header */} +
+
+ + Welcome Home +
+
+
+ Live Session +
+
+ + {/* Main Content */} +
+ {/* Welcome Section */} +
+
+ +

AI Interview Assistant

+
+

+ Welcome to your intelligent coding interview companion! I'm here to help you analyze code, + debug issues, optimize algorithms, and prepare for technical interviews. +

+
+
+

Code Analysis

+

+ Capture code screenshots and get instant analysis, optimization suggestions, and explanations. +

+
+
+

Real-time Help

+

+ Get immediate assistance during coding sessions with intelligent suggestions and debugging. +

+
+
+
+ + {/* Quick Actions */} +
+
+
+ +
+

Start Session

+

Begin a new coding session

+
+ +
+
+ +
+

AI Help

+

Get intelligent assistance

+
+ +
+
+ + + +
+

Analytics

+

View performance stats

+
+ +
+
+ + + + +
+

Settings

+

Configure preferences

+
+
+ + {/* Recent Activity */} +
+

Recent Activity

+
+
+
+
+

Code analysis completed

+

2 minutes ago

+
+
+
+
+
+

Algorithm optimization suggested

+

5 minutes ago

+
+
+
+
+
+

Debug session started

+

10 minutes ago

+
+
+
+
+
+
+ ) +} diff --git a/src/pages/MessagesPage.tsx b/src/pages/MessagesPage.tsx new file mode 100644 index 0000000..0ea11ad --- /dev/null +++ b/src/pages/MessagesPage.tsx @@ -0,0 +1,102 @@ +import { IconMessage, IconBrain, IconUser } from "@tabler/icons-react" + +interface MessagesPageProps { + isMobileView: boolean +} + +export const MessagesPage = ({ isMobileView }: MessagesPageProps) => { + const conversations = [ + { + id: 1, + name: "AI Assistant", + lastMessage: "I've analyzed your algorithm and found some optimizations...", + timestamp: "2 min ago", + unread: true, + avatar: + }, + { + id: 2, + name: "Code Review Bot", + lastMessage: "Your latest solution looks great! Time complexity is optimal.", + timestamp: "1 hour ago", + unread: false, + avatar: + }, + { + id: 3, + name: "Interview Prep", + lastMessage: "Ready for the next practice session?", + timestamp: "3 hours ago", + unread: false, + avatar: + } + ] + + return ( +
+ {/* Header */} +
+
+ + Messages +
+
+
+ Online +
+
+ + {/* Conversations List */} +
+ {conversations.map((conversation) => ( +
+
+
+ {conversation.avatar} +
+
+
+

+ {conversation.name} +

+ {conversation.timestamp} +
+

+ {conversation.lastMessage} +

+ {conversation.unread && ( +
+ +
+ )} +
+
+
+ ))} +
+ + {/* Quick Actions */} +
+ + + +
+
+ ) +} diff --git a/src/pages/QueuePage.tsx b/src/pages/QueuePage.tsx new file mode 100644 index 0000000..562f45a --- /dev/null +++ b/src/pages/QueuePage.tsx @@ -0,0 +1,136 @@ +import React, { useState, useEffect, useRef } from "react" +import { useQuery } from "@tanstack/react-query" +import { IconFiles, IconBrain } from "@tabler/icons-react" +import ScreenshotQueue from "../components/Queue/ScreenshotQueue" +import QueueCommands from "../components/Queue/QueueCommands" +import { useToast } from "../contexts/toast" +import { Screenshot } from "../types/screenshots" + +async function fetchScreenshots(): Promise { + try { + const result = await window.electronAPI.getScreenshots() + if (result.success && result.previews) { + return result.previews.map(preview => ({ + path: preview.path, + preview: preview.preview, + id: preview.path, + timestamp: Date.now(), + name: preview.path.split('\\').pop() || preview.path.split('/').pop() || 'Screenshot' + })) + } + return [] + } catch (error) { + console.error("Error loading screenshots:", error) + throw error + } +} + +interface QueuePageProps { + isMobileView: boolean + setView?: (view: "queue" | "solutions" | "debug") => void + credits?: number + currentLanguage: string + setLanguage: (language: string) => void +} + +export const QueuePage: React.FC = ({ + isMobileView, + setView = () => {}, + credits = 999, + currentLanguage, + setLanguage +}) => { + const { showToast } = useToast() + const [isTooltipVisible, setIsTooltipVisible] = useState(false) + const [tooltipHeight, setTooltipHeight] = useState(0) + const contentRef = useRef(null) + + const { + data: screenshots = [], + isLoading, + refetch + } = useQuery({ + queryKey: ["screenshots"], + queryFn: fetchScreenshots, + staleTime: Infinity, + gcTime: Infinity, + refetchOnWindowFocus: false + }) + const handleDeleteScreenshot = async (index: number) => { + try { + if (screenshots && screenshots[index]) { + const imagePath = screenshots[index].path; + const result = await window.electronAPI.deleteScreenshot(imagePath) + if (result.success) { + showToast("Success", "Screenshot deleted successfully", "success") + refetch() + } else { + showToast("Error", result.error || "Failed to delete screenshot", "error") + } + } + } catch (error) { + console.error("Error deleting screenshot:", error) + showToast("Error", "Failed to delete screenshot", "error") + } + } + + const handleTooltipVisibilityChange = (visible: boolean, height = 0) => { + setIsTooltipVisible(visible) + setTooltipHeight(height) + } + + return ( +
+ {/* Header */}
+
+ + Screenshot Queue +
+
+
+ Credits: {credits} +
+
+
+ Ready +
+
+
+ + {/* Main Content */} +
+ {/* Screenshot Queue */} +
+ +
{/* Queue Commands */} + +
+ + {/* Instructions */} +
+
+ +
+

How to use the Queue

+
+

• Press Ctrl+H to capture code screenshots

+

• Screenshots will appear in the queue above

+

• Select your programming language and click "Generate Solution" to analyze

+

• Use the delete button to remove unwanted screenshots

+
+
+
+
+
+ ) +} diff --git a/src/pages/SolutionsPage.tsx b/src/pages/SolutionsPage.tsx new file mode 100644 index 0000000..3ed4c34 --- /dev/null +++ b/src/pages/SolutionsPage.tsx @@ -0,0 +1,239 @@ +import React, { useState, useEffect, useRef } from "react" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { IconBulb, IconBrain, IconCode } from "@tabler/icons-react" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" +import ScreenshotQueue from "../components/Queue/ScreenshotQueue" +import { ProblemStatementData } from "../types/solutions" +import { Screenshot } from "../types/screenshots" +import SolutionCommands from "../components/Solutions/SolutionCommands" +import { useToast } from "../contexts/toast" +import { CopyButton } from "../components/ui/copy-button" + +async function fetchScreenshots(): Promise { + try { + const result = await window.electronAPI.getScreenshots() + if (result.success && result.previews) { + return result.previews.map(preview => ({ + path: preview.path, + preview: preview.preview, + id: preview.path, + timestamp: Date.now(), + name: preview.path.split('\\').pop() || preview.path.split('/').pop() || 'Screenshot' + })) + } + return [] + } catch (error) { + console.error("Error loading screenshots:", error) + throw error + } +} + +interface SolutionsPageProps { + isMobileView: boolean + setView?: (view: "queue" | "solutions" | "debug") => void + credits?: number + currentLanguage: string + setLanguage: (language: string) => void +} + +export const SolutionsPage: React.FC = ({ + isMobileView, + setView = () => {}, + credits = 999, + currentLanguage, + setLanguage +}) => { + const { showToast } = useToast() + const contentRef = useRef(null) + + const [debugProcessing, setDebugProcessing] = useState(false) + const [problemStatementData, setProblemStatementData] = useState(null) + const [solutionData, setSolutionData] = useState(null) + const [thoughtsData, setThoughtsData] = useState(null) + const [timeComplexityData, setTimeComplexityData] = useState(null) + const [spaceComplexityData, setSpaceComplexityData] = useState(null) + const [timeComplexityExplanation, setTimeComplexityExplanation] = useState(null) + const [spaceComplexityExplanation, setSpaceComplexityExplanation] = useState(null) + const [isTooltipVisible, setIsTooltipVisible] = useState(false) + const [tooltipHeight, setTooltipHeight] = useState(0) + + const { + data: screenshots = [], + refetch + } = useQuery({ + queryKey: ["screenshots"], + queryFn: fetchScreenshots, + staleTime: Infinity, + gcTime: Infinity, + refetchOnWindowFocus: false + }) + const handleDeleteScreenshot = async (index: number) => { + try { + const imagePath = screenshots[index].path + const result = await window.electronAPI.deleteScreenshot(imagePath) + if (result.success) { + showToast("Success", "Screenshot deleted successfully", "success") + refetch() + } else { + showToast("Error", result.error || "Failed to delete screenshot", "error") + } + } catch (error) { + console.error("Error deleting screenshot:", error) + showToast("Error", "Failed to delete screenshot", "error") + } + } + + const handleTooltipVisibilityChange = (visible: boolean, height = 0) => { + setIsTooltipVisible(visible) + setTooltipHeight(height) + } + + return ( +
+ {/* Header */} +
+
+ + AI Solutions +
+
+
+ Credits: {credits} +
+
+
+ Processing +
+
+
+ + {/* Main Content */} +
+ {/* Screenshot Queue */} +
+ +
+ + {/* Solution Commands */} + + + {/* Solutions Content */} +
+
+ {/* Problem Statement Section */} + {problemStatementData && ( +
+

+ + Problem Statement +

+ {problemStatementData.problem_statement} +
+
+ )} + + {/* Solution Section */} + {solutionData && ( +
+

+ + Optimized Solution +

+
+ + + {solutionData} + +
+
+ )} + + {/* Thoughts Section */} + {thoughtsData && ( +
+

+ Algorithm Approach +

+
+ {thoughtsData} +
+
+ )} + + {/* Complexity Analysis */} + {(timeComplexityData || spaceComplexityData) && ( +
+

+ Complexity Analysis +

+
+ {timeComplexityData && ( +
+ Time Complexity: + {timeComplexityData} + {timeComplexityExplanation && ( +

+ {timeComplexityExplanation} +

+ )} +
+ )} + {spaceComplexityData && ( +
+ Space Complexity: + {spaceComplexityData} + {spaceComplexityExplanation && ( +

+ {spaceComplexityExplanation} +

+ )} +
+ )} +
+
+ )} + + {/* Empty State */} + {!problemStatementData && !solutionData && !thoughtsData && ( +
+ +

No Solutions Yet

+

+ Capture some code screenshots and generate a solution to see AI-powered analysis and optimization suggestions here. +

+
+ )} +
+
+
+
+ ) +} diff --git a/src/pages/index.ts b/src/pages/index.ts new file mode 100644 index 0000000..4b99374 --- /dev/null +++ b/src/pages/index.ts @@ -0,0 +1,7 @@ +export { ChatPage } from './ChatPage' +export { HomePage } from './HomePage' +export { CodePage } from './CodePage' +export { QueuePage } from './QueuePage' +export { SolutionsPage } from './SolutionsPage' +export { MessagesPage } from './MessagesPage' +export { BrainPage } from './BrainPage' diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 9cad47b..1e34d80 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -7,6 +7,7 @@ export interface ElectronAPI { width: number height: number }) => Promise + ensureWindowVisible: () => Promise clearStore: () => Promise<{ success: boolean; error?: string }> getScreenshots: () => Promise<{ success: boolean @@ -19,6 +20,8 @@ export interface ElectronAPI { onScreenshotTaken: ( callback: (data: { path: string; preview: string }) => void ) => () => void + onClearQueue: (callback: () => void) => () => void + processScreenshots: () => Promise<{ success: boolean; error?: string }> onResetView: (callback: () => void) => () => void onSolutionStart: (callback: () => void) => () => void onDebugStart: (callback: () => void) => () => void @@ -38,6 +41,10 @@ export interface ElectronAPI { triggerMoveRight: () => Promise<{ success: boolean; error?: string }> triggerMoveUp: () => Promise<{ success: boolean; error?: string }> triggerMoveDown: () => Promise<{ success: boolean; error?: string }> + triggerScaleUp: () => Promise<{ success: boolean; error?: string }> + triggerScaleDown: () => Promise<{ success: boolean; error?: string }> + triggerScaleReset: () => Promise<{ success: boolean; error?: string }> + onScaleWindow: (callback: (data: { direction: "up" | "down" | "reset" }) => void) => () => void onSubscriptionUpdated: (callback: () => void) => () => void onSubscriptionPortalClosed: (callback: () => void) => () => void startUpdate: () => Promise<{ success: boolean; error?: string }> @@ -48,6 +55,8 @@ export interface ElectronAPI { decrementCredits: () => Promise setInitialCredits: (credits: number) => Promise onCreditsUpdated: (callback: (credits: number) => void) => () => void + onWindowAspectChanged: (callback: (data: { isMobile: boolean, width: number, height: number }) => void) => () => void + removeAllListeners: (eventName: string) => void onOutOfCredits: (callback: () => void) => () => void openSettingsPortal: () => Promise getPlatform: () => string diff --git a/src/types/index.tsx b/src/types/index.tsx index 2147bab..bd47509 100644 --- a/src/types/index.tsx +++ b/src/types/index.tsx @@ -1,13 +1,6 @@ -export interface Screenshot { - id: string - path: string - timestamp: number - thumbnail: string // Base64 thumbnail -} - -export interface Solution { - initial_thoughts: string[] - thought_steps: string[] - description: string - code: string -} +export interface Solution { + initial_thoughts: string[] + thought_steps: string[] + description: string + code: string +} \ No newline at end of file diff --git a/src/types/screenshots.ts b/src/types/screenshots.ts index 86228da..35baa13 100644 --- a/src/types/screenshots.ts +++ b/src/types/screenshots.ts @@ -1,4 +1,7 @@ -export interface Screenshot { - path: string - preview: string -} +export interface Screenshot { + path: string; + preview: string; + id?: string; + timestamp?: number; + name?: string; +} \ No newline at end of file diff --git a/src/types/solutions.ts b/src/types/solutions.ts index c0f74f8..8f39b5e 100644 --- a/src/types/solutions.ts +++ b/src/types/solutions.ts @@ -1,30 +1,30 @@ -export interface Solution { - initial_thoughts: string[] - thought_steps: string[] - description: string - code: string -} - -export interface SolutionsResponse { - [key: string]: Solution -} - -export interface ProblemStatementData { - problem_statement: string - input_format: { - description: string - parameters: any[] - } - output_format: { - description: string - type: string - subtype: string - } - complexity: { - time: string - space: string - } - test_cases: any[] - validation_type: string - difficulty: string -} +export interface Solution { + initial_thoughts: string[] + thought_steps: string[] + description: string + code: string +} + +export interface SolutionsResponse { + [key: string]: Solution +} + +export interface ProblemStatementData { + problem_statement: string + input_format: { + description: string + parameters: any[] + } + output_format: { + description: string + type: string + subtype: string + } + complexity: { + time: string + space: string + } + test_cases: any[] + validation_type: string + difficulty: string +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a831c76..a2c65c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,8 +17,12 @@ "allowJs": true, "esModuleInterop": true, "allowImportingTsExtensions": true, - "types": ["vite/client"] + "types": ["vite/client"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } }, "include": ["electron/**/*", "src/**/*"], "references": [{ "path": "./tsconfig.node.json" }] -} +} \ No newline at end of file