From 7a66e073d8d87235ddc9f43cc4d3e82fde6ffbd8 Mon Sep 17 00:00:00 2001 From: William Hill Date: Mon, 23 Feb 2026 15:59:26 -0500 Subject: [PATCH 1/8] docs: NQL interface redesign design doc (#88) --- docs/plans/2026-02-23-nql-redesign-design.md | 114 +++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/plans/2026-02-23-nql-redesign-design.md diff --git a/docs/plans/2026-02-23-nql-redesign-design.md b/docs/plans/2026-02-23-nql-redesign-design.md new file mode 100644 index 0000000..5de1882 --- /dev/null +++ b/docs/plans/2026-02-23-nql-redesign-design.md @@ -0,0 +1,114 @@ +# NQL Interface Redesign — Design Doc +**Issue:** #88 +**Date:** 2026-02-23 + +## Overview + +Three improvements to the Natural Query Language interface at `/query`: + +1. Add **Query** nav link to the global header +2. Move query history into a **fixed left sidebar** +3. Add an opt-in **Summarize** button that generates a plain-English LLM narrative of results + +--- + +## Architecture + +### Files changed + +| File | Change | +|------|--------| +| `codebenders-dashboard/components/nav-header.tsx` | Add `{ href: "/query", label: "Query" }` to `NAV_LINKS` | +| `codebenders-dashboard/app/query/page.tsx` | Redesign as sidebar + main two-column layout; add summarize state | +| `codebenders-dashboard/components/query-history-panel.tsx` | Strip Card wrapper; adapt for sidebar use (full height, scrollable) | +| `codebenders-dashboard/app/api/query-summary/route.ts` | New POST endpoint for LLM result summarization | + +--- + +## Section 1: Nav link + +Add `{ href: "/query", label: "Query" }` to the `NAV_LINKS` array in `nav-header.tsx`. + +- Consistent with Dashboard / Courses / Students pattern +- Middleware already guards `/query` for `admin, advisor, ir, faculty` roles +- No role-filtering on the link itself (unauthorized users get redirected by middleware) + +--- + +## Section 2: Sidebar layout + +**Desktop (≥ 768px):** +``` +┌──────────┬──────────────────────────────────────┐ +│ History │ Query Controls │ +│ sidebar │ [Institution] [Prompt ▸▸▸] [Run] │ +│ ~260px │ │ +│ • q1 ├──────────────────────────────────────┤ +│ • q2 │ Analysis Results [✦ Summarize] │ +│ • q3 │ Chart / Table │ +│ … │ │ +│ │ ── LLM summary paragraph ── │ +│ [Export] │ │ +│ [Clear] │ Query Plan (collapsible) │ +└──────────┴──────────────────────────────────────┘ +``` + +**Mobile (< 768px):** Sidebar hidden by default. A **History** toggle button in the page header opens it as an overlay drawer from the left. + +**QueryHistoryPanel changes:** +- Remove `` wrapper — sidebar provides its own container +- Make list `flex-1 overflow-y-auto` so it fills available height +- Pin Export + Clear buttons to the bottom of the sidebar +- Header ("Recent Queries") stays at the top + +**Page header:** Remove the "Back to Dashboard" `` — global nav covers navigation now. Keep the page title and description as a slim heading. + +--- + +## Section 3: LLM result summary + +**UI:** After a successful query, a `Summarize` button (Sparkles icon, ghost variant) appears next to the "Analysis Results" heading. States: +- Idle: `✦ Summarize` +- Loading: `Generating…` (spinner) +- Done: button hidden; summary paragraph renders below chart with a Sparkles icon prefix +- Error: inline error message + +**Summary resets** when a new query is run. + +**API: `POST /api/query-summary`** + +Request body: +```json +{ + "prompt": "retention by cohort for last two terms", + "data": [ ...up to 50 rows... ], + "rowCount": 12, + "vizType": "bar" +} +``` + +Response: +```json +{ "summary": "The Fall 2022 cohort had the highest retention rate at 71%, ..." } +``` + +Implementation: +- Uses `generateText` from `ai` SDK with `openai("gpt-4o-mini")` +- Caps data rows at 50 before sending to LLM +- `maxOutputTokens: 200` (2–3 sentences) +- System prompt: advisor-friendly, data-driven, no speculation beyond the numbers +- RBAC: `/api/query-summary` accessible to same roles as `/query` + +--- + +## Frontend design + +The `frontend-design` skill must be invoked when implementing the query page layout and sidebar to ensure visual quality. The redesign should feel like a professional analytics workbench — not a generic form page. + +--- + +## Out of scope + +- Streaming the summary (full text response is fine at 2–3 sentences) +- Persisting summaries to localStorage or DB +- Changing the SQL generation or execution logic From 3bb0dfcd2981131810e2bd887b7e1f80a9e0fffd Mon Sep 17 00:00:00 2001 From: William Hill Date: Mon, 23 Feb 2026 16:05:18 -0500 Subject: [PATCH 2/8] docs: NQL redesign implementation plan (#88) --- docs/plans/2026-02-23-nql-redesign.md | 419 ++++++++++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 docs/plans/2026-02-23-nql-redesign.md diff --git a/docs/plans/2026-02-23-nql-redesign.md b/docs/plans/2026-02-23-nql-redesign.md new file mode 100644 index 0000000..f5c9e45 --- /dev/null +++ b/docs/plans/2026-02-23-nql-redesign.md @@ -0,0 +1,419 @@ +# NQL Interface Redesign Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add a "Query" nav link, move query history into a fixed left sidebar, and add an opt-in LLM "Summarize" button that generates a plain-English narrative from query results. + +**Architecture:** Four file changes — nav link is a one-liner, the new `POST /api/query-summary` route reuses the existing `@ai-sdk/openai` + `generateText` pattern from other API routes, `QueryHistoryPanel` is stripped of its Card wrapper and made sidebar-native, and the query page is restructured as a two-column layout (260px sidebar + flex-1 main). Use the `frontend-design` skill for the page layout task. + +**Tech Stack:** Next.js 16 App Router, TypeScript, Tailwind CSS, shadcn/ui, `ai` v5 + `@ai-sdk/openai` v2, Lucide icons. + +**Design doc:** `docs/plans/2026-02-23-nql-redesign-design.md` + +--- + +### Task 1: Add "Query" to global nav + +**Files:** +- Modify: `codebenders-dashboard/components/nav-header.tsx` + +**Context:** +`NAV_LINKS` is an array at the top of the file. The `/query` route is already RBAC-guarded in `lib/roles.ts` for `admin, advisor, ir, faculty`. The middleware injects `x-user-role` and redirects unauthorized users — no change to `roles.ts` needed for the nav link itself. + +**Step 1: Add the link** + +In `nav-header.tsx`, update `NAV_LINKS`: + +```ts +const NAV_LINKS = [ + { href: "/", label: "Dashboard" }, + { href: "/courses", label: "Courses" }, + { href: "/students", label: "Students" }, + { href: "/query", label: "Query" }, +] +``` + +**Step 2: Verify type check passes** + +```bash +cd codebenders-dashboard && npx tsc --noEmit +``` +Expected: no errors. + +**Step 3: Commit** + +```bash +git add codebenders-dashboard/components/nav-header.tsx +git commit -m "feat: add Query link to global nav header (#88)" +``` + +--- + +### Task 2: Create POST /api/query-summary route + +**Files:** +- Create: `codebenders-dashboard/app/api/query-summary/route.ts` +- Modify: `codebenders-dashboard/lib/roles.ts` (add RBAC entry) + +**Context:** +Follow the exact pattern from `app/api/courses/explain-pairing/route.ts`: +- Import `generateText` from `"ai"`, `createOpenAI` from `"@ai-sdk/openai"` +- Instantiate `const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY || "" })` +- Use `generateText({ model: openai("gpt-4o-mini"), prompt, maxOutputTokens: 200 })` +- Return `NextResponse.json({ summary: result.text })` +- Guard with `canAccess("/api/query-summary", role)` + +**Step 1: Add RBAC entry in `lib/roles.ts`** + +```ts +{ prefix: "/api/query-summary", roles: ["admin", "advisor", "ir", "faculty"] }, +``` + +Add it after the existing `/api/courses` entry. + +**Step 2: Create the route** + +```ts +import { type NextRequest, NextResponse } from "next/server" +import { canAccess, type Role } from "@/lib/roles" +import { generateText } from "ai" +import { createOpenAI } from "@ai-sdk/openai" + +const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY || "" }) + +export async function POST(request: NextRequest) { + const role = request.headers.get("x-user-role") as Role | null + if (!role || !canAccess("/api/query-summary", role)) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }) + } + + if (!process.env.OPENAI_API_KEY) { + return NextResponse.json({ error: "OpenAI API key not configured" }, { status: 500 }) + } + + const { prompt, data, rowCount, vizType } = await request.json() + + if (!prompt || !Array.isArray(data)) { + return NextResponse.json({ error: "prompt and data are required" }, { status: 400 }) + } + + // Cap rows sent to LLM to avoid token overflow + const sampleRows = data.slice(0, 50) + + const llmPrompt = `You are a student success analyst at a community college. An advisor ran the following query and got these results. + +QUERY: "${prompt}" +RESULT: ${rowCount} rows, visualization type: ${vizType} +DATA SAMPLE: +${JSON.stringify(sampleRows, null, 2)} + +Write a 2-3 sentence plain-English summary of what these results show. Be specific about the numbers. Do not speculate beyond the data. Address the advisor directly.` + + try { + const result = await generateText({ + model: openai("gpt-4o-mini"), + prompt: llmPrompt, + maxOutputTokens: 200, + }) + return NextResponse.json({ summary: result.text }) + } catch (error) { + console.error("[query-summary] Error:", error) + return NextResponse.json( + { error: "Failed to generate summary", details: error instanceof Error ? error.message : String(error) }, + { status: 500 }, + ) + } +} +``` + +**Step 3: Verify type check** + +```bash +cd codebenders-dashboard && npx tsc --noEmit +``` +Expected: no errors. + +**Step 4: Commit** + +```bash +git add codebenders-dashboard/app/api/query-summary/route.ts \ + codebenders-dashboard/lib/roles.ts +git commit -m "feat: add POST /api/query-summary LLM result narration (#88)" +``` + +--- + +### Task 3: Adapt QueryHistoryPanel for sidebar + +**Files:** +- Modify: `codebenders-dashboard/components/query-history-panel.tsx` + +**Context:** +Currently renders a `` with `` and ``. In the sidebar it will be rendered inside an `