From b60faa42d4a835c9c3b959557efc54c2ac80e844 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Wed, 11 Feb 2026 14:40:45 -0500 Subject: [PATCH 1/2] huge push of backend and frontend to dos to finish wiring up and UI display --- backend/config/index.js | 5 --- backend/routes/notes.routes.js | 19 +++++++++- backend/services/clustering.service.js | 45 ++++++++++++++++++----- frontend/index.html | 2 +- frontend/src/App.css | 5 --- frontend/src/api/api.tsx | 24 ++++++++++-- frontend/src/components/button.tsx | 21 ++++++----- frontend/src/components/stickies.css | 19 ++++++++++ frontend/src/components/stickies.tsx | 51 +++++++++++++++++++++++--- frontend/src/index.css | 23 ++++-------- frontend/src/types/types.tsx | 5 +++ 11 files changed, 163 insertions(+), 56 deletions(-) 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[]; +}; From e523bafc9bc82d450a1d2fa201f6373c3265c9b9 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Wed, 11 Feb 2026 14:46:56 -0500 Subject: [PATCH 2/2] cleanup --- backend/services/clustering.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/services/clustering.service.js b/backend/services/clustering.service.js index 40ffd17..c73ab2d 100644 --- a/backend/services/clustering.service.js +++ b/backend/services/clustering.service.js @@ -12,11 +12,11 @@ const buildPrompt = (notes) => { 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: +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", + "label": "Short descriptive theme name for cluster", "noteIds": ["note_001", "note_002"] } ]