# Project export: Noteva

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: The ultimate productivity browser for daily tasks catering to academia.
- Devpost: https://devpost.com/software/notevi
- GitHub: https://github.com/zeuzmakessoftware/broser/
- Video: https://www.youtube.com/embed/IXVC-_zeWF4?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Sponsor - Opennote] Best Productivity (3rd place))
- Team: 2 GitHub contributor(s) — zeuzmakessoftware (28 commits), evajain (10 commits)

## Devpost submission (written by the team)

### Inspiration

We found that there wasn't a browser for students out there that specifically catered to a student's wants and needs. The closest thing to a true educational browser is just Google Chrome with a bunch of janky extensions. We noticed that even in the AI era, modern browsers haven't really changed in around decade. We saw how bulky and bland the usual browser experience was so we wanted to have a more autonomous and simple experience for productive browsing.

### What it does

Noteva works like a normal browser but with added AI features to be able to handle and automate different tabs, research modes for bulk sourcing of different subjects, and educative features to teach and quiz you on web pages you visit. This browser was designed to be used for educational purposes and to streamline the workflow of researchers and students by integrating powerful AI tools directly into the browsing experience.

### How we built it

We built it mostly using Chromium and Electron for the browser shell, React and Tailwind for the frontend design, MongoDB for the personal cloud notes feature, and Gemini and ElevenLabs for the AI infrastructure. For building the app, me and my team utilized Antigravity to handle most of the coding tasks as an incredibly efficient unit.

### Challenges we ran into

We ran into several challenges due to all of us being quite new to building entire browser but with enough determination (and Google Gemini), we were able to overcome pretty much all of them. A problem we ran into very early on is that Gemini on Antigravity didn't generate very good looking raw Electron UI. But after one of us suggested to migrate all the frontend code to React, it kind of became magically not ugly which really helped us sculpt out our design visions way easier. One problem we ran into repeatedly is since we all pushed into the main git branch throughout the hackathon, it was quite tricky to resolve all the merge conflicts. But through that struggle, we made the very cursed discovery that we can resolve any single merge conflicts by vibe coding the git commands. Another minor issue was dealing with some minor Electron related UI bugs since it handles frontend elements a bit differently compared to a normal web app but they were mostly resolvable.

### Accomplishments we're proud of

Just the fact we all made an entire browser is the accomplishment itself. I think the single biggest accomplishment is that every person on the team did their fair share in the browser project in terms of features and additions in building Noteva.

### What we learned

We learned a lot about how to leverage Antigravity to split and finish tasks really quickly so through that system we created an incredibly powerful team of software engineers on this browser project. As we faced the adversity of implementation our ambitious ideas, we overall learned that perseverance was the key to truly execute on our ideals and vision for this project.

### What's next

for Notevi We plan to have an account system that has different paid tiers for access to the cloud notes and more powerful AI browser agent models. We hope to partner with Timeslicer to limit screen time on certain social media sites in order to ensure our users are productive and gets things done.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 37 recognized source files, 266 KB.
- CSS (language) — detected in the code
- Google Gemini (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- MongoDB (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (44 of 44)

```
.gitignore
backend/db.ts
backend/main.ts
backend/models/Citation.ts
backend/models/History.ts
backend/models/Note.ts
backend/models/Source.ts
backend/models/Task.ts
backend/models/Workspace.ts
backend/preload.ts
backend/services/ai.ts
backend/services/voice.ts
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/postcss.config.js
frontend/README.md
frontend/src/App.css
frontend/src/App.tsx
frontend/src/components/AddressBar.tsx
frontend/src/components/ParticleSystem.tsx
frontend/src/components/ResearchPanel.tsx
frontend/src/components/Sidebar.tsx
frontend/src/components/SidePanelContent.tsx
frontend/src/components/StartPage.tsx
frontend/src/components/TabBar.tsx
frontend/src/components/VoiceOverlay.tsx
frontend/src/components/VoiceRecorder.tsx
frontend/src/hooks/useBrowserAPI.ts
frontend/src/hooks/useVoiceInput.ts
frontend/src/index.css
frontend/src/main.tsx
frontend/src/types.d.ts
frontend/tsconfig.app.json
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vite.config.ts
package.json
preload.js
scripts/patch-plist.js
scripts/probe-eleven.js
scripts/test-keys.js
tsconfig.json
```

### Dependencies

- frontend/package.json: @eslint/js@^9.39.1, @tailwindcss/postcss@^4.1.18, @types/node@^24.10.1, @types/react@^19.2.5, @types/react-dom@^19.2.3, @vitejs/plugin-react@^5.1.1, autoprefixer@^10.4.23, clsx@^2.1.1, eslint@^9.39.1, eslint-plugin-react-hooks@^7.0.1, eslint-plugin-react-refresh@^0.4.24, framer-motion@^12.26.2, globals@^16.5.0, lucide-react@^0.562.0, postcss@^8.5.6, react@^19.2.0, react-dom@^19.2.0, react-markdown@^10.1.0, remark-gfm@^4.0.1, tailwind-merge@^3.4.0, tailwindcss@^4.1.18, typescript@~5.9.3, typescript-eslint@^8.46.4, vite@^7.2.4
- package.json: @elevenlabs/elevenlabs-js@^2.31.0, @google/generative-ai@^0.24.1, @types/electron@^1.4.38, @types/node@^25.0.9, concurrently@^9.2.1, dotenv@^17.2.3, electron@^34.0.0, electron-builder@^25.1.8, mongoose@^9.1.4, ts-node@^10.9.2, typescript@^5.9.3, wait-on@^9.0.3

### Recent commits (newest first)

- thinkers to students
- added dock icon and cherry on top
- fixed pdf bug and big where it uses voice ai for everything instead of just the voice mode
- fixed reload bug with CMD+R
- changed canvas to ucsc for clarity
- persist cookies
- added tab icon support
- added right click support and fixed ai assistant text bug
- added animations
- added light mode
- added small particles
- i like this font ill sneak it in
- fixed research notes
- added loading state
- replaced settings w voice on the start screen
- Fix lint errors and hook violations after rebase
- framer motion stuff
- Replace History with Canvas integration on Start Page and refine Sidebar UI
- fix: resolve remaining git conflicts in SidePanelContent
- voice toolcall

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "broser",
  "version": "1.0.0",
  "description": "A simple Electron-based web browser",
  "main": "dist/main.js",
  "keywords": [
    "electron",
    "browser",
    "webview"
  ],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/electron": "^1.4.38",
    "@types/node": "^25.0.9",
    "concurrently": "^9.2.1",
    "electron": "^34.0.0",
    "electron-builder": "^25.1.8",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.3",
    "wait-on": "^9.0.3"
  },
  "scripts": {
    "prestart": "npm run build:backend",
    "start": "concurrently -k \"npm run dev:frontend\" \"npm run build:backend -- --watch\" \"wait-on tcp:5173 dist/main.js && electron .\"",
    "build:backend": "tsc",
    "dev:frontend": "npm run dev --prefix frontend",
    "build": "npm run build:backend && npm run build --prefix frontend && electron-builder",
    "pack": "electron-builder --dir",
    "postinstall": "node scripts/patch-plist.js && npm install --prefix frontend"
  },
  "dependencies": {
    "@elevenlabs/elevenlabs-js": "^2.31.0",
    "@google/generative-ai": "^0.24.1",
    "dotenv": "^17.2.3",
    "mongoose": "^9.1.4"
  },
  "build": {
    "appId": "com.zeuzmakessoftware.broser",
    "mac": {
      "category": "public.app-category.productivity",
      "extendInfo": {
        "NSCameraUsageDescription": "This app requires camera access for video conferencing and AI vision features.",
        "NSMicrophoneUsageDescription": "This app requires microphone access for voice commands and AI interaction.",
        "NSCameraUseContinuityCameraDeviceType": true
      }
    }
  }
}
```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "clsx": "^2.1.1",
    "framer-motion": "^12.26.2",
    "lucide-react": "^0.562.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-markdown": "^10.1.0",
    "remark-gfm": "^4.0.1",
    "tailwind-merge": "^3.4.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.1",
    "@tailwindcss/postcss": "^4.1.18",
    "@types/node": "^24.10.1",
    "@types/react": "^19.2.5",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^5.1.1",
    "autoprefixer": "^10.4.23",
    "eslint": "^9.39.1",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-react-refresh": "^0.4.24",
    "globals": "^16.5.0",
    "postcss": "^8.5.6",
    "tailwindcss": "^4.1.18",
    "typescript": "~5.9.3",
    "typescript-eslint": "^8.46.4",
    "vite": "^7.2.4"
  }
}

```

### frontend/src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### backend/main.ts

```typescript
import { app, BrowserWindow, ipcMain, session, systemPreferences, Menu, MenuItem, WebContents, shell, nativeImage } from 'electron';
import path from 'path';
import connectDB from './db';

// Models
import Note from './models/Note';
import Task from './models/Task';
import Source from './models/Source';
import Citation from './models/Citation';
import Workspace from './models/Workspace';
import History from './models/History';

// Services
import * as aiService from './services/ai';
import * as voiceService from './services/voice';

// Connect to MongoDB
connectDB();

// HandleUserData persistence in Dev
if (!app.isPackaged) {
    const userDataPath = path.join(process.cwd(), '.userData');
    app.setPath('userData', userDataPath);
    console.log(`[Dev] Setting userData path to: ${userDataPath}`);
}

// Declare global mainWindow
declare global {
  // eslint-disable-next-line no-var
  var mainWindow: BrowserWindow | undefined;
}

// ... (Window creation logic remains)

ipcMain.handle('db:save-note', async (event, payload: any) => {
  try {
    const { content, workspaceId } = typeof payload === 'string' ? { content: payload } : payload;
    const note = await Note.create({ content, workspaceId });
    return { success: true, note };
  } catch (error) {
    console.error('Error saving note:', error);
    return { success: false, error: (error as any).message };
  }
});

ipcMain.handle('db:get-notes', async () => {
  try {
    const res = await Note.find().sort({ createdAt: -1 }).lean();
    return JSON.parse(JSON.stringify(res));
  } catch (error) {
    return [];
  }
});

ipcMain.handle('db:create-task', async (event, title) => {
  try {
    const task = await Task.create({ title });
    return { success: true, task };
  } catch (error) {
    console.error('Error creating task:', error);
    return { success: false, error: (error as any).message };
  }
});

ipcMain.handle('db:get-tasks', async () => {
  try {
    const res = await Task.find().sort({ createdAt: -1 }).lean();
    return JSON.parse(JSON.stringify(res));
  } catch (error) {
    return [];
  }
});

// Research Handlers
ipcMain.handle('db:get-sources', async () => {
  try {
    const res = await Source.find().sort({ createdAt: -1 }).lean();
    return JSON.parse(JSON.stringify(res));
  } catch (error) {
    return [];
  }
});

ipcMain.handle('db:get-citations', async () => {
  try {
    const res = await Citation.find().sort({ createdAt: -1 }).lean();
    return JSON.parse(JSON.stringify(res));
  } catch (error) {
    return [];
  }
});

// History Handlers
ipcMain.handle('db:save-history', async (event, { url, title }) => {
  try {
    await History.create({ url, title });
    return { success: true };
  } catch (error) {
    return { success: false };
  }
});

ipcMain.handle('db:get-history', async () => {
  try {
    const res = await History.find().sort({ timestamp: -1 }).limit(100).lean();
    return JSON.parse(JSON.stringify(res));
  } catch (error) {
    return [];
  }
});

// Workspace Handlers
ipcMain.handle('db:get-workspaces', async () => {
  try {
    const res = await Workspace.find().sort({ createdAt: -1 }).lean();
    return JSON.parse(JSON.stringify(res));
  } catch (error) {
    console.error('Error fetching workspaces:', error);
    return [];
  }
});

ipcMain.handle('db:create-workspace', async (event, title) => {
  try {
    const workspace = await Workspace.create({ title });
    // Ensure we return a plain object with string ID
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const wsObj: any = workspace.toObject();
    wsObj._id = wsObj._id.toString();
    return { success: true, workspace: wsObj };
  } catch (error) {
    console.error('Error creating workspace:', error);
    return { success: false, error: (error as any).message };
  }
});

ipcMain.handle('db:get-workspace-data', async (event, workspaceId) => {
  try {
    console.log(`[DEBUG] Fetching data for workspace: ${workspaceId}`);
    const [notes, sources, citations] = await Promise.all([
      Note.find({ workspaceId }).sort({ createdAt: -1 }).lean(),
      Source.find({ workspaceId }).sort({ createdAt: -1 }).lean(),
      Citation.find({ workspaceId }).sort({ createdAt: -1 }).lean()
    ]);

    console.log(`[DEBUG] Found ${notes.length} notes, ${sources.length} sources, ${citations.length} citations`);

    return {
      notes: JSON.parse(JSON.stringify(notes)),
      sources: JSON.parse(JSON.stringify(sources)),
      citations: JSON.parse(JSON.stringify(citations))
    };
  } catch (error) {
    console.error('Error fetching workspace data:', error);
    return { notes: [], sources: [], citations: [] };
  }
});

// AI & Voice Handler
// Helper to handle individual AI actions
const handleAIAction = async (action: { type: string, payload: any }, defaultWorkspaceId?: string) => {
  // Inject defaultWorkspaceId if payload is missing it
  if (defaultWorkspaceId && action.payload && typeof action.payload === 'object') {
     // For CREATE_NOTE, SAVE_SOURCE, SAVE_CITATION, inject workspaceId if missing
     if (['CREATE_NOTE', 'SAVE_SOURCE', 'SAVE_CITATION'].includes(action.type)) {
         if (!action.payload.workspaceId) {
             console.log(`[Auto-Fix] Injecting missing workspaceId: ${defaultWorkspaceId} into action ${action.type}`);
             action.payload.workspaceId = defaultWorkspaceId;
         }
     }
  }

  if (action.type === 'CREATE_NOTE') {
    const content = typeof action.payload === 'string' ? action.payload : action.payload.content;
    const workspaceId = typeof action.payload === 'object' ? action.payload.workspaceId : undefined;
    if (!workspaceId) console.warn('Warning: CREATE_NOTE missing workspaceId');
    await Note.create({ content, workspaceId });
  } else if (action.type === 'CREATE_TASK') { // LEGACY support
    const title = typeof action.payload === 'string' ? action.payload : action.payload.title;
    await Task.create({ title });
  } else if (action.type === 'SAVE_SOURCE') {
 
[truncated — 15935 more characters]
```

### frontend/src/App.tsx

```typescript
import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import './index.css';
import { Sidebar } from './components/Sidebar';
import { TabBar } from './components/TabBar';
import { AddressBar } from './components/AddressBar';
import { SidePanelContent } from './components/SidePanelContent';

interface Tab {
  id: string;
  title: string;
  url: string; // The source of truth for the webview 'src'
  displayUrl?: string; // What is shown in the address bar
  favicon?: string;
  active: boolean;
}

import { useVoiceInput } from './hooks/useVoiceInput';
import { useBrowserAPI } from './hooks/useBrowserAPI';

// ...

import { StartPage } from './components/StartPage';
import { VoiceOverlay } from './components/VoiceOverlay';

// ...

function App() {
  const api = useBrowserAPI();
  const [tabs, setTabs] = useState<Tab[]>([
    { id: '1', title: 'Start Page', url: 'noteva://start', displayUrl: 'noteva://start', active: true }
  ]);
  const [theme, setTheme] = useState<'dark' | 'light'>('dark');
  const [sidebarMode, setSidebarMode] = useState<'notes' | 'chat' | 'settings' | 'research' | 'history' | null>(null);
  const [expansionMode, setExpansionMode] = useState<'compact' | 'half' | 'full'>('compact');
  const [aiResponseToProcess, setAiResponseToProcess] = useState<any>(null);
  const [researchContext, setResearchContext] = useState<{ topic?: string; workspaceId?: string; queries?: string[] } | null>(null);
  const [isVoiceProcessing, setIsVoiceProcessing] = useState(false);
  const [initialChatQuery, setInitialChatQuery] = useState<string | null>(null);

  const handleVoiceData = async (base64Audio: string) => {
    // Ideally show a spinner or "Processing..." state
    setIsVoiceProcessing(true);
    console.log("Audio recorded, sending to AI...");
    
    try {
      // We can optionally pass current context (url, title) here too
      const webview = document.getElementById('main-webview') as any;
      let context = {};
      if (webview) {
        try {
          const title = await webview.getTitle();
          const url = webview.getURL();
          context = { title, url };
        } catch (e) {/* ignore */ }
      }

      const res = await api.ai.chat({ audio: base64Audio, context, generateAudio: true });

      // Delegate response handling to SidePanelContent
      setAiResponseToProcess(res);

      // Open chat panel to show response
      setSidebarMode('chat');
    } catch (e) {
      console.error("Voice Error", e);
    } finally {
      setIsVoiceProcessing(false);
    }
  };

  const { isListening, toggleListening } = useVoiceInput({ onAudioData: handleVoiceData });

  const handleVoiceInteraction = () => {
    if (isVoiceProcessing) {
      setIsVoiceProcessing(false); // Cancel processing
      return;
    }
    
    if (isListening) {
      // Stop listening, prepare for processing
      toggleListening();
      setIsVoiceProcessing(true);
    } else {
      // Start listening
      toggleListening();
      setIsVoiceProcessing(false);
    }
  };

  const activeTab = tabs.find(t => t.active) || tabs[0];

  const handleNewTab = (url?: string) => {
    const newTab: Tab = {
      id: Date.now().toString(),
      title: url ? url : 'Start Page',
      url: url ? url : 'noteva://start',
      displayUrl: url ? url : 'noteva://start',
      active: true
    };
    setTabs(prev => prev.map(t => ({ ...t, active: false })).concat(newTab));
  };

  const handleSwitchTab = (id: string) => {
    setTabs(prev => prev.map(t => ({ ...t, active: t.id === id })));
  };

  const handleCloseTab = (id: string, e: React.MouseEvent) => {
    e.stopPropagation();
    if (tabs.length === 1) return; // Don't close last tab for now

    // If closing active tab, activate the one before it
    let newActiveId = activeTab.id;
    if (activeTab.id === id) {
      const index = tabs.findIndex(t => t.id === id);
      const newTab = tabs[index - 1] || tabs[index + 1];
      newActiveId = newTab.id;
    }

    setTabs(prev =>
      prev.filter(t => t.id !== id).map(t => ({ ...t, active: t.id === newActiveId }))
    );
  };

  const handleNavigate = (url: string) => {
    if (!url) return;

    // Normalize input
    let finalUrl = url.trim();

    // Quick check if it's a URL
    const isUrl = finalUrl.includes('.') && !finalUrl.includes(' ');
    const hasProtocol = finalUrl.includes('://');

    if (!hasProtocol) {
      if (isUrl) {
        finalUrl = 'https://' + finalUrl;
      } else {
        // Default to Google search
        finalUrl = `https://www.google.com/search?q=${encodeURIComponent(finalUrl)}`;
      }
    }

    console.log(`[Navigation] Navigating to: ${finalUrl}`);
    // Update BOTH url and displayUrl when user manually navigates
    setTabs(prev => prev.map(t => t.active ? { ...t, url: finalUrl, displayUrl: finalUrl, title: finalUrl } : t));

    // Save to History
    if (!finalUrl.startsWith('noteva://')) {
      api.db.saveHistory(finalUrl, url); // Simple title for now, webview can update it later
    }

    // Electron Webview update logic
    // We update via src prop in React, but for existing webviews, loadURL is faster/more reliable
    const webview = document.getElementById('main-webview') as any;
    if (webview && !finalUrl.startsWith('noteva://')) {
      try {
        webview.loadURL(finalUrl);
      } catch (e) {
        console.error("Navigation error", e);
      }
    }
  };

  const toggleSidebar = (mode: 'notes' | 'chat' | 'settings' | 'research' | 'history') => {
    setSidebarMode(curr => curr === mode ? null : mode);
    if (!sidebarMode) setExpansionMode('compact');
  };

  const toggleExpand = () => {
    setExpansionMode(curr => {
      if (curr === 'compact') return 'half';
      if (curr === 'half') return 'full';
      return 'compact';
    });
  };

  const toggleTheme = () => {
    setTheme(curr => curr === 'dark' ? 'light' : 'dark');
  };


  useEffect(() => {
    if (theme === 'light')
[truncated — 7457 more characters]
```

### preload.js

```javascript
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
    // We can expose safe APIs here if needed.
});

```

### frontend/postcss.config.js

```javascript
export default {
    plugins: {
        '@tailwindcss/postcss': {},
        autoprefixer: {},
    },
}

```

### frontend/vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  base: './', // Important for file system loading in Electron
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  server: {
    port: 5173,
    strictPort: true,
  },
  build: {
    outDir: 'dist',
    emptyOutDir: true,
  }
})

```

### frontend/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
  },
])

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&display=swap" rel="stylesheet">
    <title>frontend</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

[26 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]