diff --git a/backend/config/index.js b/backend/config/index.js
index bbd6904..629eea8 100644
--- a/backend/config/index.js
+++ b/backend/config/index.js
@@ -1,10 +1,5 @@
-import dotenv from 'dotenv';
-
-dotenv.config({ path: '.secrets' });
-
const config = {
port: process.env.PORT || 3001,
- openaiApiKey: process.env.OPENAI_API_KEY,
};
export default config;
diff --git a/backend/routes/notes.routes.js b/backend/routes/notes.routes.js
index 659b989..13e8247 100644
--- a/backend/routes/notes.routes.js
+++ b/backend/routes/notes.routes.js
@@ -2,6 +2,7 @@ import { Router } from 'express';
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
+import { clusterNotes } from '../services/clustering.service.js';
const router = Router();
@@ -9,14 +10,28 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const DATA_PATH = join(__dirname, '..', 'data', 'notes.json');
+const loadNotes = async () => {
+ const raw = await readFile(DATA_PATH, 'utf-8');
+ return JSON.parse(raw);
+};
+
router.get('/', async (req, res) => {
try {
- const raw = await readFile(DATA_PATH, 'utf-8');
- const notes = JSON.parse(raw);
+ const notes = await loadNotes();
res.json(notes);
} catch (err) {
res.status(500).json({ error: 'Failed to load notes' });
}
});
+router.post('/cluster', async (req, res) => {
+ try {
+ const notes = await loadNotes();
+ const clusters = await clusterNotes(notes);
+ res.json(clusters);
+ } catch (err) {
+ res.status(500).json({ error: 'Clustering failed' });
+ }
+});
+
export default router;
diff --git a/backend/services/clustering.service.js b/backend/services/clustering.service.js
index 30db22f..c73ab2d 100644
--- a/backend/services/clustering.service.js
+++ b/backend/services/clustering.service.js
@@ -1,19 +1,46 @@
import Anthropic from "@anthropic-ai/sdk";
-import anthropicApiKey from "../.secrets";
+import { anthropicApiKey } from "../.secrets.js";
const client = new Anthropic({
- apiKey: anthropicApiKey
+ apiKey: anthropicApiKey,
});
-const prompt = `You are a helpful assistant that analyzes notes for semantic similarity. Each note is a json object with the
-various attributes. For clustering purposes, the relvant attribute is "text". Analze the text attributes of the notes and return a json object with the following structure: {{$notes}}`;
+const buildPrompt = (notes) => {
+ const notesJson = JSON.stringify(notes, null, 2);
+
+ return `You are an expert at analyzing text for semantic similarity and thematic patterns.
+
+Below is a JSON array of sticky notes. Each note has an "id" and a "text" field. Analyze the "text" field of every note and group them into meaningful thematic clusters.
+
+For each cluster, return ONLY a valid JSON array with the below exact structure — no markdown, no explanation, no extra text - where the value for the "label" key is a name you create to describe the cluster's theme and the value for the "noteIds" key is an array containing the Ids of the notes that fit into that cluster theme.
+
+[
+ {
+ "label": "Short descriptive theme name for cluster",
+ "noteIds": ["note_001", "note_002"]
+ }
+]
+
+Rules:
+- Every note must appear in exactly one cluster
+- Each cluster must have a concise, descriptive label
+- Group by semantic meaning, not by keywords
+- Aim for the most natural number of groups given the data
+
+Here are the notes:
+
+${notesJson}`;
+};
export const clusterNotes = async (notes) => {
const response = await client.messages.create({
- model: "claude-opus-4-6",
+ model: "claude-sonnet-4-20250514",
+ max_tokens: 4096,
messages: [
- { role: "user", content: `${prompt}`}
- ]
+ { role: "user", content: buildPrompt(notes) },
+ ],
});
- return response.content[0].text;
-};
\ No newline at end of file
+
+ const raw = response.content[0].text;
+ return JSON.parse(raw);
+};
diff --git a/frontend/index.html b/frontend/index.html
index fc3a2ba..de74e7a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
-