An interactive whiteboard for organizing sticky notes into clusters. Notes can be grouped by color, author, or AI-suggested themes using Google Gemini. Supports grid and stacked layouts, drag-and-drop, zoom, and custom AI prompts for flexible clustering.
- React 19 with TypeScript
- Vite 8 for dev server and bundling
- Tailwind CSS 4 for styling
- json-server as a mock REST API
- Google Gemini API (free tier) for AI-powered content clustering
- Node.js (v18+)
- A Google Gemini API key (free from Google AI Studio)
npm installCopy the example env file and add your Gemini API key:
cp .env.example .envEdit .env:
VITE_GEMINI_API_KEY=your-api-key-here
npm run devThis starts both the Vite dev server (port 5173) and the json-server API (port 3001) concurrently.
You can also run them separately:
npm run client # Vite dev server only
npm run api # json-server onlynpm test # Run all tests once
npm run test:watch # Run in watch modeTests use Vitest and cover:
- Geometry utilities --
groupNotesByField,calculateClusterPositions(grid and stack),getBoardSize,getCenter - Gemini response parsing -- clean JSON, markdown fence stripping, unknown ID filtering, empty group handling, API errors, missing API key, custom prompt passthrough
- Color -- groups notes by their color
- Author -- groups notes by who wrote them
- Suggested (AI) -- uses Gemini to identify common themes in note content and group accordingly
- Grid -- spreads notes in a grid within each group
- Stack -- piles notes on top of each other with a slight offset per group
Type a custom instruction (e.g., "sort into bug reports and feature requests") and Gemini will cluster notes according to your description.
Each group gets an auto-generated title label that can be:
- Dragged to reposition
- Renamed by double-clicking
- Deleted via a trash icon on hover
Labels are persisted to the API.
- Drag notes to reposition them (persisted to the API)
- Pan by clicking and dragging the background
- Zoom with toolbar buttons or Cmd/Ctrl + scroll wheel (zooms toward cursor)
- Zoom range: 25% to 200%
Clustering parameters (field and mode) are stored in the URL query string, so layouts persist across page reloads and can be shared via link.
On mobile breakpoints, the AI prompt input moves to a bottom bar while clustering and zoom controls stay at the top.
src/
api/
gemini.ts # Gemini API integration
notes.ts # Notes CRUD + clustering logic
titles.ts # Cluster label CRUD
components/
ClusterLabelComponent.tsx
Note.tsx
ToolsPanel.tsx
Whiteboard.tsx
media/icons/
ClusterIcon.tsx
GridIcon.tsx
StackIcon.tsx
ZoomInIcon.tsx
ZoomOutIcon.tsx
utils/
geometry.ts # Layout calculations, board sizing
types.ts # Shared TypeScript types
App.tsx
main.tsx
style.css
db.json # json-server database (notes + titles)
The AI clustering features send note text content to Google's Gemini API for processing. Keep the following in mind:
- Note content is transmitted to a third-party service. Do not use AI clustering with sensitive, confidential, or personally identifiable information unless your organization's data policies permit it.
- The Gemini API key is embedded in the client bundle (via the
VITE_prefix). This is acceptable for development and free-tier usage, but for production deployments the API calls should be proxied through a backend to avoid exposing the key. - No data is stored by this application beyond json-server. However, Google's data retention policies for the Gemini API apply to any content sent for classification. Review Google's API Terms of Service for details.
- State management with Zustand -- replace prop-drilling and
useStatein App with a Zustand store. As the app grows (undo/redo, multi-select, collaborative editing), centralized state with slices would be cleaner than threading callbacks through the component tree - Google Generative AI SDK for JavaScript -- replace the raw
fetchcalls to Gemini with the official@google/generative-aiSDK, which handles retries, streaming, error parsing, and type-safe responses out of the box - URL state with a custom
useSearchParamshook -- replace the manualgetParams/setParamsfunctions with a reusable hook that syncs React state with URL query parameters bidirectionally, similar tonuqsor a lightweight custom implementation. This would eliminate the current split between reading params on mount and writing them on change, and make it easy to persist additional state (zoom level, scroll position) to the URL - Backend API with AI orchestration -- replace json-server with a proper backend (e.g., Express or Fastify) that owns the Gemini API key, handles batch note updates in a single request, and orchestrates AI calls server-side. This backend could expose a structured AI endpoint that manages prompt templates, caches results, and enforces rate limits
- Lasso selection tool -- select multiple notes by drawing a freeform shape on the canvas, enabling bulk move, delete, or re-color operations
- Z-index awareness on drop -- when a note is dropped on top of another, ensure the dropped note stays visually on top regardless of its order in the database
- Per-note color picker -- allow changing individual note colors directly from the whiteboard
- Auto-color clusters -- automatically assign a distinct color to all notes within a cluster after grouping
- Organic cluster layout -- a "messy cluster" mode that arranges notes in a natural, slightly randomized pile rather than a strict grid or uniform stack
- Create and edit notes -- add new sticky notes directly from the whiteboard and edit existing note text inline