AI-Integrated Summary Dashboard — Phase 01 build challenge for the Uanvi Vibe Coding / AI Developer position.
Repository: https://github.com/alextotalk/testUanvi Test assessment platform: https://code-test.uanvi.com/
Paste raw user feedback into the textarea, hit Summarize, and the
service returns a one-line summary plus a sentiment tag (positive,
neutral, negative). Submissions accumulate in an in-memory,
server-side history that persists for the lifetime of the Node.js
process and is hydrated into the UI on page load.
- Next.js 16 (App Router) — both the React frontend and the Node.js API endpoint live in the same project.
- React 19 + Tailwind CSS 3 for the UI.
- Vitest 2 + Testing Library for unit, component, and API integration tests (24 tests across 4 files).
- TypeScript strict mode.
- Node.js 22+ runtime.
app/
page.tsx UI: textarea, button, loading state, history list
page.test.tsx Component tests for the page
api/summarize/
route.ts POST + GET API endpoints with error handling
route.test.ts API integration tests
lib/
summarizer.ts Mock AI service: first-sentence + sentiment + 1.5s delay
summarizer.test.ts Unit tests for the summary logic
history.ts In-memory server-side history store
history.test.ts Unit tests for the store
types.ts Shared types
Prerequisites: Node.js 22+ and npm 10+.
# 1. Clone the repository
git clone https://github.com/alextotalk/testUanvi.git
cd testUanvi
# 2. Install dependencies
npm install
# 3. Start the dev server
npm run dev
# → http://localhost:3000npm test # vitest run — runs all 24 tests
npm run test:watch # vitest in watch mode
npm run typecheck # tsc --noEmit
npm run lint # eslint .
npm run build # production build
npm run start # serve the production build# POST a feedback string
curl -X POST http://localhost:3000/api/summarize \
-H "Content-Type: application/json" \
-d '{"text":"The new dashboard is amazing! It loads instantly."}'
# Read accumulated history
curl http://localhost:3000/api/summarizerunMockSummarizer(text) simulates a 1.5 s upstream call (per the spec)
and returns:
summary— first sentence of the input, capitalised. Falls back to the trimmed input when there is no sentence terminator.sentiment— keyword-based heuristic over positive/negative lexicons.
The pure, synchronous summarizeText is exported separately so unit
tests can exercise the logic without waiting on timers.
POST /api/summarize — body { "text": string }
201→{ entry, history }400→ malformed JSON, missingtext, or emptytext413→ input longer than 10 000 characters500→ summarizer threw
GET /api/summarize — 200 → { history }
A line-by-line verification against the Phase 01 brief. Every requirement and acceptance criterion is met.
Verification plan: locate a textarea and a button labelled "Summarize" in the page component.
Files: app/page.tsx, app/layout.tsx, app/globals.css
app/page.tsx:88-95—<textarea id="feedback">bound to controlledtextstate.app/page.tsx:103-112—<button type="submit">showing "Summarize" / "Summarizing…".- Tailwind wired through
globals.css→app/layout.tsx. - Page
/renders with HTTP 200 (confirmed in dev-server logs).
Status: done.
Verification plan: find a Node.js API route that accepts text and
returns JSON with summary and sentiment.
File: app/api/summarize/route.ts
export const runtime = "nodejs"— explicit Node.js runtime.POSTaccepts{ text }and returns{ entry: { id, originalText, summary, sentiment, createdAt }, history }.- Live curl call returns
{"summary":"The new dashboard is amazing","sentiment":"positive",...}.
Status: done.
Verification plan: confirm the service layer is decoupled from the API, waits 1500 ms, slices the first sentence, and capitalises it.
File: lib/summarizer.ts
MOCK_AI_DELAY_MS = 1500.runMockSummarizerawaitssetTimeout(1500)before calling the synchronoussummarizeText.extractFirstSentencesplits on/[.!?]+/and trims.capitalize→value.charAt(0).toUpperCase() + value.slice(1).- Real timing measured via curl:
real 0m1,533s— exactly 1.5 s.
Status: done.
Verification plan: history is held server-side in process memory and survives between requests.
File: lib/history.ts
const store: { entries: HistoryEntry[] } = { entries: [] }— module-level state, lives for the lifetime of the Node.js process.addHistoryEntryinserts at the head and caps the list at 50.getHistoryreturns a defensive copy (covered bylib/history.test.ts).- Confirmed at runtime: three sequential POSTs accumulated; the follow-up GET returned all three entries in newest-first order.
Status: done.
Verification plan: at least one unit test on the summary logic, plus at least one component test that exercises the button click.
Files: lib/summarizer.test.ts, app/page.test.tsx
- Unit:
summarizeText > returns the capitalized first sentence and a sentiment tag(plus 10 more unit cases coveringextractFirstSentence,capitalize,detectSentiment). - Component:
<HomePage /> > submits the textarea contents to /api/summarize on button click— drivesuserEvent.clickand asserts the outgoingfetchcall and request body.
Status: done (well beyond the minimum).
| Required | Implemented |
|---|---|
| React / Next.js | Next.js ^16.2.6 + React ^19.2.6 |
| Node.js | v22.22.3 |
| Tailwind CSS | ^3.4.16 |
| Vitest / Jest | Vitest ^2.1.8 |
npm run build→ succeeded.- Dev-server logs show only HTTP
200 / 201 / 400lines — no warnings, no errors. - 24/24 tests green (including DOM-render tests through jsdom — any React warning would surface there).
- Manual eight-step smoke run via curl: all expected statuses.
Status: met.
app/api/summarize/route.test.ts — 5 integration tests:
POST returns 201, a structured entry, and updated history for valid input— summarisation happy path.POST returns 400 when the text field is missing or empty.POST returns 400 for malformed JSON bodies.POST returns 500 when the summarizer throws.GET returns the in-memory history accumulated across POSTs.
Plus 5 UI-level integration tests in app/page.test.tsx (render +
mocked fetch + interaction).
Status: met (5 ≥ 3).
Every error branch verified with a live request:
400malformed JSON →"Request body must be valid JSON."400missingtextfield →"Field 'text' is required and must be a string."400emptytext→"Field 'text' must not be empty."413for input longer than 10 000 characters (payload-size guard).500when the summarizer throws → logged asSummarization failed.201 Createdon success,200 OKfor GET.
Status: met.
- Loading: button switches to
Summarizing…with a spinner; both the button and the textarea becomedisabled(app/page.tsx:103-112). - Covered by the test
shows the loading state while the request is in-flight, then renders the new history entry. - History:
<section aria-label="history">lists sentiment badges, summary, snippet of the original text, and a timestamp (app/page.tsx:121-160). - History hydrates from
GET /api/summarizeon mount (app/page.tsx:17-31), covered by the testhydrates the history list from GET /api/summarize on mount.
Status: met.
| # | Requirement | Status |
|---|---|---|
| 1 | Frontend: React/Next.js + textarea + Summarize button | ✅ |
| 2 | Backend: Node.js endpoint, text → JSON summary + sentiment | ✅ |
| 3 | Mock service: 1.5 s delay + first sentence + capitalize | ✅ |
| 4 | In-memory server-side history persisting during session | ✅ |
| 5 | ≥ 1 unit test (summary logic) + ≥ 1 component test (button click) | ✅ |
| Tech | React/Next.js + Node.js + Tailwind + Vitest | ✅ |
| A | Fully functional, no console / runtime errors | ✅ |
| B | ≥ 3 integration tests covering summarisation flow | ✅ (5) |
| C | Backend graceful errors + correct HTTP codes | ✅ (400 / 413 / 500 / 201 / 200) |
| D | UI loading state + history of previous summaries | ✅ |
No requirement was skipped.