From 9ba13206c5d00670fef682ea19d103127ee4a794 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Tue, 24 Feb 2026 13:32:06 -0500 Subject: [PATCH] Add UI/logical temporal ordering feature --- README.md | 48 +++++++++++------- frontend/src/components/stickies.tsx | 76 +++++++++++++++++++++++++--- frontend/src/styles/stickies.css | 52 ++++++++++++++++++- frontend/src/tests/stickies.test.tsx | 38 +++++++++++++- frontend/src/types/types.d.ts | 4 ++ 5 files changed, 193 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 25a7e8c..a13f036 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,28 @@ -# kongruity app +# kongruity -kongruity clusters large volumes of unstructured action items in development settings by their thematic or topical similarity. +kongruity pulls in the unstructured artifacts of the creative-engineering process -- to-dos, action items, agile tickets, Jira comment threads, retrospective notes -- and synthesizes them into semantically coherent, prioritized clusters ready for implementation planning. -In kongruity, "to dos", action items, agile tickets, Jira comments (appropriately tagged) ... all the myriad artifacts of the creative-engineering process, become thematic "sticky notes." +In kongruity world, these artifacts are "sticky notes." A board full of them looks chaotic. With one click, an LLM analyzes their meaning and groups them into thematic clusters, each with a descriptive header. -kongruity's React/Vite UI displays a board of your team's seemingly chaotic "sticky notes" - transformed into manageable, actionable groups, with headers that explain their semantic relation, and introduces project management workflow: ranking temporal priority of clusters for future release. +An independent embedding-based evaluation scores clustering quality, so the output is data-backed. From there, teams can cimply drag-and-rank clusters by implementation priority, turning a wall of noise into an actionable workflow. -The backend features an Express server/API that serves "sticky note" data and proxies semantic grouping requests to Large Language Models. +## How it works -Developers may freely swap in other LLM SDKs and/or APIs... and alter prompt syntax at backend/services/clustering.service.js to complement R&D with any LLM model/platform they prefer. +1. **Ingest** — Sticky notes are loaded and displayed on a board. +2. **Cluster** — An LLM reads every note and groups them by semantic similarity (not keywords). +3. **Evaluate** — In parallel, a separate embedding model (Voyage AI) generates vector representations of each note. A silhouette-based cohesion score measures how well-separated and internally consistent clusters are. The score is displayed alongside the results. +4. **Validate** — Structural checks confirm every note is assigned to exactly one cluster, no clusters are empty, and labels are present. +5. **Prioritize** — Clusters appear ranked and are drag-reorderable. Teams set implementation priority by dragging clusters into position. + +## Dev implementation note + +Developers may swap in other LLM SDKs/APIs and alter prompt syntax in `backend/services/clustering.service.js` to experiment with any model or platform. ## Prerequisites - Node.js (v18 or later recommended) -- An LLM Platform API key +- An [Anthropic API key](https://console.anthropic.com/) (or other LLM platform, for clustering) +- A [Voyage AI API key](https://dash.voyageai.com/) (for embedding-based evaluation) ## Setup @@ -26,13 +35,16 @@ cd kongruity ### 2. Create an environment file -The backend expects a `.env` file containing an Anthropic API key in the root `backend/` directory. This file is git-ignored and must be created manually: +The backend expects a `.env` file in the `backend/` directory. This file is git-ignored and must be created manually: ```bash -echo 'LLM_API_KEY=' > backend/.env +cat > backend/.env << 'EOF' +ANTHROPIC_API_KEY= +VOYAGEAI_API_KEY= +EOF ``` -Replace `` with your actual key. +Replace placeholder values with your actual keys. ### 3. Install dependencies @@ -48,7 +60,7 @@ npm install ## Run the app -### Start the backend - Production Mode +### Start the backend — Production mode From the `backend/` directory: @@ -58,15 +70,15 @@ npm run start The API server starts on **http://localhost:3001** (configurable via the `PORT` environment variable). -### Start the backend - Development mode +### Start the backend — Development mode -To start using Nodemon for "hot reloads," if developing your own features: +To start using Nodemon for hot reloads while developing: ```bash npm run dev ``` -### Build the frontend - Production mode +### Build the frontend — Production mode From the `frontend/` directory: @@ -74,13 +86,14 @@ From the `frontend/` directory: npm run build ``` -### Start the frontend - Development mode +### Start the frontend — Development mode From the `frontend/` directory: ```bash npm run dev ``` + The Vite dev server starts on **http://localhost:5173** by default. Open that URL in a browser. ## Running tests @@ -92,7 +105,8 @@ From the `backend/` directory: ```bash npm test ``` -Backend tests use Supertest for HTTP assertions. + +Backend tests use Vitest with Supertest for HTTP assertions. ### Frontend tests @@ -102,7 +116,7 @@ From the `frontend/` directory: npm test ``` -This runs `vitest run` with jsdom. For watch mode, during development: +This runs Vitest with jsdom. For watch mode during development: ```bash npm run test:watch diff --git a/frontend/src/components/stickies.tsx b/frontend/src/components/stickies.tsx index fe199c0..e2e4e0e 100644 --- a/frontend/src/components/stickies.tsx +++ b/frontend/src/components/stickies.tsx @@ -1,5 +1,7 @@ +import { useState, useEffect, useRef } from 'react'; +import type { DragEvent } from 'react'; import { useGetStickies, useClusterStickies } from '../api/api'; -import type { Sticky as StickyType } from '../types/types'; +import type { Sticky as StickyType, RankedCluster } from '../types/types'; import Sticky from './sticky'; import Button from './button'; import '../styles/stickies.css'; @@ -14,10 +16,22 @@ const scoreLabel = (score: number): string => { const Stickies = () => { const { data: stickies, isLoading, error } = useGetStickies(); const { mutate: cluster, data: clusterResponse, isPending } = useClusterStickies(); + + const [rankedClusters, setRankedClusters] = useState([]); + const dragIndex = useRef(null); + const [dragOverIndex, setDragOverIndex] = useState(null); + + useEffect(() => { + if (clusterResponse?.clusters) { + setRankedClusters( + clusterResponse.clusters.map((c, i) => ({ ...c, rank: i + 1 })) + ); + } + }, [clusterResponse]); + if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; - const clusters = clusterResponse?.clusters; const score = clusterResponse?.score; const handleCluster = () => { @@ -35,19 +49,69 @@ const Stickies = () => { const renderStickies = (items: StickyType[]) => items?.map((sticky) => ); + const handleDragStart = (index: number) => { + dragIndex.current = index; + }; + + const handleDragOver = (e: DragEvent, index: number) => { + e.preventDefault(); + setDragOverIndex(index); + }; + + const handleDragLeave = () => { + setDragOverIndex(null); + }; + + const handleDrop = (targetIndex: number) => { + const sourceIndex = dragIndex.current; + if (sourceIndex === null || sourceIndex === targetIndex) { + dragIndex.current = null; + setDragOverIndex(null); + return; + } + + const reordered = [...rankedClusters]; + const [moved] = reordered.splice(sourceIndex, 1); + reordered.splice(targetIndex, 0, moved); + + setRankedClusters(reordered.map((c, i) => ({ ...c, rank: i + 1 }))); + dragIndex.current = null; + setDragOverIndex(null); + }; + + const handleDragEnd = () => { + dragIndex.current = null; + setDragOverIndex(null); + }; + return (