9 Commits

Author SHA1 Message Date
KS Jannette
5208327b5d more 2026-02-13 15:12:18 -05:00
KS Jannette
ac76871d60 hottie 2026-02-13 13:47:43 -05:00
S Jannette
66c86c2625 Merge pull request #10 from kjannette/refact5
clean
2026-02-13 13:45:24 -05:00
KS Jannette
56d0c21b79 clean 2026-02-13 13:45:00 -05:00
S Jannette
37e81e773e Merge pull request #9 from kjannette/refactor4
more
2026-02-13 13:42:52 -05:00
KS Jannette
534da11217 more 2026-02-13 13:42:31 -05:00
S Jannette
2212755772 Merge pull request #8 from kjannette/refact3
cleanup
2026-02-13 13:35:29 -05:00
KS Jannette
f2e1390507 cleanup 2026-02-13 13:35:11 -05:00
S Jannette
31496876a2 Merge pull request #7 from kjannette/refact2
env
2026-02-13 13:25:59 -05:00
8 changed files with 41 additions and 29 deletions

View File

@@ -4,11 +4,13 @@ kongruity employs Large Language Model ("LLM") semantic grouping functionality t
(To learn more about this topic, see, e.g., [Kozlowski A., Boutyline A., Semantic Structure in Large Language Model Embeddings Aug. 2025, arXiv:2508.10003v1:04 Aug 2025](https://arxiv.org/html/2508.10003v1)).
In the world of kongruity, these "to dos" are called "sticky notes." kongruity's React/Vite UI views a board of seemingly chaotic "sticky notes". But with one click, they are transformed into manageable, actionable groups, each with a header that explains the group semantic interrelation.
In kongruity world, "to dos", action items, agile tickets... the myriad artifacts of the creative/engineering process, are "sticky notes."
The backend is an Express API that serves "sticky note" data and proxies semantic grouping requests to Antrhopic Claude.
kongruity's React/Vite UI displays a board of seemingly chaotic "sticky notes". But with one click, they are transformed into manageable, actionable groups, each with a header that explains that group's semantic relation.
Developers may feel free to install other LLM SDKs and alter the syntax at backend/services/clustering.service.js to experiment with any LLM model/platform they prefer.
The backend is an Express API that serves "sticky note" data and proxies semantic grouping requests to Large Language Models.
Developers may freeely swap in other LLM SDKs and/or APIs... and alter prompt syntax at backend/services/clustering.service.js to compliement their experimentation with any LLM model/platform they prefer.
## Prerequisites

View File

@@ -1,14 +1,9 @@
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();
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const DATA_PATH = join(__dirname, '..', 'data', 'notes.json');
const DATA_PATH = new URL('../data/notes.json', import.meta.url);
const loadNotes = async () => {
const raw = await readFile(DATA_PATH, 'utf-8');
@@ -20,7 +15,7 @@ router.get('/', async (req, res) => {
const notes = await loadNotes();
res.json(notes);
} catch (err) {
console.error(`Error loading notes: ${err}`)
console.error(`Error loading notes: ${err}`);
res.status(500).json({ error: 'Failed to load notes' });
}
});
@@ -31,8 +26,8 @@ router.post('/cluster', async (req, res) => {
const clusters = await clusterNotes(notes);
res.json(clusters);
} catch (err) {
console.error(`Clustering failed: ${err}` )
res.status(500).json({ error: `Clustering failed: ${err}` });
console.error(`Clustering failed: ${err}`);
res.status(500).json({ error: 'Clustering failed' });
}
});

View File

@@ -40,6 +40,15 @@ export const clusterNotes = async (notes) => {
],
});
const raw = response.content[0].text;
return JSON.parse(raw);
const textBlock = response?.content?.[0];
if (!textBlock || textBlock.type !== 'text' || typeof textBlock.text !== 'string') {
throw new Error('Unexpected response from LLM API: no text content returned');
}
try {
return JSON.parse(textBlock.text);
} catch {
throw new Error('LLM API returned non-JSON response');
}
};

View File

@@ -35,7 +35,7 @@ describe('clusterNotes service', () => {
it('should call Anthropic messages.create with the correct model', async () => {
createMock.mockResolvedValue({
content: [{ text: JSON.stringify(MOCK_CLUSTERS) }],
content: [{ type: 'text', text: JSON.stringify(MOCK_CLUSTERS) }],
});
await clusterNotes(MOCK_NOTES);
@@ -48,7 +48,7 @@ describe('clusterNotes service', () => {
it('should include all note texts in prompt sent to the LLM API', async () => {
createMock.mockResolvedValue({
content: [{ text: JSON.stringify(MOCK_CLUSTERS) }],
content: [{ type: 'text', text: JSON.stringify(MOCK_CLUSTERS) }],
});
await clusterNotes(MOCK_NOTES);
@@ -62,7 +62,7 @@ describe('clusterNotes service', () => {
it('should parse and return the clustered JSON from the API response', async () => {
createMock.mockResolvedValue({
content: [{ text: JSON.stringify(MOCK_CLUSTERS) }],
content: [{ type: 'text', text: JSON.stringify(MOCK_CLUSTERS) }],
});
const result = await clusterNotes(MOCK_NOTES);
@@ -72,10 +72,16 @@ describe('clusterNotes service', () => {
it('should throw error when the API returns non-JSON', async () => {
createMock.mockResolvedValue({
content: [{ text: 'An unknown error occured when generting structured response.' }],
content: [{ type: 'text', text: 'An unknown error occured when generting structured response.' }],
});
await expect(clusterNotes(MOCK_NOTES)).rejects.toThrow();
await expect(clusterNotes(MOCK_NOTES)).rejects.toThrow('non-JSON response');
});
it('should throw error when the API response has no text content', async () => {
createMock.mockResolvedValue({ content: [] });
await expect(clusterNotes(MOCK_NOTES)).rejects.toThrow('no text content returned');
});
it('should throw error when Anthropic API authentication fails', async () => {

View File

@@ -24,7 +24,7 @@ const Stickies = () => {
const stickyMap = buildStickyMap();
const renderStickies = (items: StickyType[]) =>
items.map((sticky) => <Sticky key={sticky.id} sticky={sticky} />);
items?.map((sticky) => <Sticky key={sticky.id} sticky={sticky} />);
return (
<div className="stickies-container">
@@ -36,8 +36,8 @@ const Stickies = () => {
<h3 className="cluster-label">{group.label}</h3>
<div className="stickies-grid">
{renderStickies(
group.noteIds
.map((id) => stickyMap.get(id))
group?.noteIds
.map((id) => stickyMap?.get(id))
.filter((s): s is StickyType => !!s)
)}
</div>

View File

@@ -5,12 +5,12 @@ import '../styles/home.css'
const Home = () => {
return (
<div>
<div>
<Navbar />
</div>
<div>
<Stickies />
</div>
</div>
);
};

View File

@@ -1,7 +1,7 @@
.main-head-box {
border-radius: 8px;
border: 1px solid #6dd6f4;
background-color: rgb(92, 0 91);
background-color: rgb(92, 0, 91);
display: flex;
}