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..40ffd17 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.
+
+Return ONLY a valid JSON array with this exact structure — no markdown, no explanation, no extra text:
+
+[
+ {
+ "label": "Short descriptive theme name",
+ "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 @@
-
my-react-app
+ Resias
diff --git a/frontend/src/App.css b/frontend/src/App.css
index f5d69d5..902778b 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -4,8 +4,3 @@
padding: 2rem;
text-align: center;
}
-
-.card {
- padding: 2em;
-}
-
diff --git a/frontend/src/api/api.tsx b/frontend/src/api/api.tsx
index d9da4cc..a123466 100644
--- a/frontend/src/api/api.tsx
+++ b/frontend/src/api/api.tsx
@@ -1,17 +1,35 @@
-import { useQuery } from '@tanstack/react-query';
-import type { Sticky } from '../types/types';
+import { useQuery, useMutation } from '@tanstack/react-query';
+import type { Sticky, Cluster } from '../types/types';
+
+const API_BASE = 'http://localhost:3001/v1/notes';
const fetchStickies = async (): Promise => {
- const response = await fetch('http://localhost:3001/v1/notes');
+ const response = await fetch(API_BASE);
if (!response.ok) {
throw new Error('Failed to fetch stickies');
}
return response.json();
};
+const fetchClusters = async (): Promise => {
+ const response = await fetch(`${API_BASE}/cluster`, {
+ method: 'POST',
+ });
+ if (!response.ok) {
+ throw new Error('Failed to cluster stickies');
+ }
+ return response.json();
+};
+
export const useGetStickies = () => {
return useQuery({
queryKey: ['stickies'],
queryFn: fetchStickies,
});
};
+
+export const useClusterStickies = () => {
+ return useMutation({
+ mutationFn: fetchClusters,
+ });
+};
diff --git a/frontend/src/components/button.tsx b/frontend/src/components/button.tsx
index 0e52e55..13691ab 100644
--- a/frontend/src/components/button.tsx
+++ b/frontend/src/components/button.tsx
@@ -1,11 +1,14 @@
-import { useState } from 'react';
-
-const Button = () => {
- return (
-
- );
+type ClusterButtonProps = {
+ onClick: () => void;
+ isLoading: boolean;
};
-export default Button;
\ No newline at end of file
+const ClusterButton = ({ onClick, isLoading }: ClusterButtonProps) => {
+ return (
+
+ );
+};
+
+export default ClusterButton;
diff --git a/frontend/src/components/stickies.css b/frontend/src/components/stickies.css
index 20f30d6..a6d0094 100644
--- a/frontend/src/components/stickies.css
+++ b/frontend/src/components/stickies.css
@@ -5,3 +5,22 @@
justify-content: center;
padding: 24px;
}
+
+.clusters-container {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ padding: 24px 0;
+}
+
+.cluster-group {
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ padding: 16px;
+}
+
+.cluster-label {
+ margin: 0 0 16px 0;
+ font-size: 1.2em;
+ font-weight: 600;
+}
diff --git a/frontend/src/components/stickies.tsx b/frontend/src/components/stickies.tsx
index 5c7bed4..383dd41 100644
--- a/frontend/src/components/stickies.tsx
+++ b/frontend/src/components/stickies.tsx
@@ -1,18 +1,57 @@
-import { useGetStickies } from '../api/api';
+import { useGetStickies, useClusterStickies } from '../api/api';
+import type { Sticky as StickyType } from '../types/types';
import Sticky from './sticky';
+import ClusterButton from './button';
import './stickies.css';
const Stickies = () => {
- const { data, isLoading, error } = useGetStickies();
+ const { data: stickies, isLoading, error } = useGetStickies();
+ const { mutate: cluster, data: clusters, isPending } = useClusterStickies();
if (isLoading) return Loading...
;
if (error) return Error: {error.message}
;
+ const handleCluster = () => {
+ cluster();
+ };
+
+ const buildStickyMap = (): Map => {
+ const map = new Map();
+ stickies?.forEach((s) => map.set(s.id, s));
+ return map;
+ };
+
+ if (clusters) {
+ const stickyMap = buildStickyMap();
+
+ return (
+
+
+
+ {clusters.map((group) => (
+
+
{group.label}
+
+ {group.noteIds.map((id) => {
+ const sticky = stickyMap.get(id);
+ return sticky ? : null;
+ })}
+
+
+ ))}
+
+
+ );
+ }
+
return (
-
- {data?.map((sticky) => (
-
- ))}
+
+
+
+ {stickies?.map((sticky) => (
+
+ ))}
+
);
};
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 08a3ac9..c68032a 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -2,30 +2,17 @@
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
-
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
-
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
body {
margin: 0;
- display: flex;
- place-items: center;
min-width: 320px;
min-height: 100vh;
}
@@ -46,22 +33,26 @@ button {
cursor: pointer;
transition: border-color 0.25s;
}
+
button:hover {
border-color: #646cff;
}
+
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
- a:hover {
- color: #747bff;
- }
button {
background-color: #f9f9f9;
}
diff --git a/frontend/src/types/types.tsx b/frontend/src/types/types.tsx
index dab29dd..b4cf319 100644
--- a/frontend/src/types/types.tsx
+++ b/frontend/src/types/types.tsx
@@ -6,3 +6,8 @@ export type Sticky = {
author: string;
color: string;
};
+
+export type Cluster = {
+ label: string;
+ noteIds: string[];
+};