diff --git a/codebenders-dashboard/app/api/query-summary/route.ts b/codebenders-dashboard/app/api/query-summary/route.ts new file mode 100644 index 0000000..6fa563d --- /dev/null +++ b/codebenders-dashboard/app/api/query-summary/route.ts @@ -0,0 +1,63 @@ +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 }) + } + + let prompt: string + let data: unknown[] + let rowCount: number + let vizType: string + + try { + const body = await request.json() + prompt = body.prompt + data = body.data + rowCount = body.rowCount ?? 0 + vizType = body.vizType ?? "unknown" + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }) + } + + 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.slice(0, 2000)}" +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 }, + ) + } +} diff --git a/codebenders-dashboard/app/query/page.tsx b/codebenders-dashboard/app/query/page.tsx index f770675..a0ab4fe 100644 --- a/codebenders-dashboard/app/query/page.tsx +++ b/codebenders-dashboard/app/query/page.tsx @@ -1,9 +1,7 @@ "use client" import { useState } from "react" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Switch } from "@/components/ui/switch" import { Label } from "@/components/ui/label" @@ -13,8 +11,7 @@ import { QueryHistoryPanel } from "@/components/query-history-panel" import { analyzePrompt } from "@/lib/prompt-analyzer" import { executeQuery } from "@/lib/query-executor" import type { QueryPlan, QueryResult, HistoryEntry } from "@/lib/types" -import Link from "next/link" -import { ArrowLeft } from "lucide-react" +import { Loader2, Sparkles, PanelLeft } from "lucide-react" const INSTITUTIONS = [ { name: "Bishop State", code: "bscc" }, @@ -30,6 +27,10 @@ export default function QueryPage() { const [queryPlan, setQueryPlan] = useState(null) const [queryResult, setQueryResult] = useState(null) const [useDirectDB, setUseDirectDB] = useState(true) + const [sidebarOpen, setSidebarOpen] = useState(false) + const [summary, setSummary] = useState(null) + const [summaryLoading, setSummaryLoading] = useState(false) + const [summaryError, setSummaryError] = useState(null) const [history, setHistory] = useState(() => { // Read from localStorage on mount (client-only) if (typeof window === "undefined") return [] @@ -44,6 +45,9 @@ export default function QueryPage() { overridePrompt?: string, overrideInstitution?: string, ) => { + setSummary(null) + setSummaryError(null) + const activePrompt = overridePrompt ?? prompt const activeInstitution = overrideInstitution ?? institution @@ -125,100 +129,205 @@ export default function QueryPage() { localStorage.removeItem("bishop_query_history") } + const handleSummarize = async () => { + if (!queryResult || !queryPlan) return + setSummaryLoading(true) + setSummaryError(null) + try { + const res = await fetch("/api/query-summary", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + prompt, + data: queryResult.data, + rowCount: queryResult.rowCount, + vizType: queryPlan.vizType, + }), + }) + const json = await res.json() + if (!res.ok) throw new Error(json.error || "Failed") + setSummary(json.summary) + } catch (e) { + setSummaryError(e instanceof Error ? e.message : String(e)) + } finally { + setSummaryLoading(false) + } + } + return ( -
-
-
- - - Back to Dashboard - -

SQL Query Interface

-

Analyze student performance data with natural language queries

-
- - - - Query Controls - Select an institution and enter your analysis prompt - - -
+
+ {/* Slim page-level header bar */} +
+ {/* Mobile sidebar toggle */} + + + {/* Title group */} + + Query Interface + +
+ + {/* Body: sidebar + main */} +
+ {/* Desktop sidebar */} + + + {/* Mobile sidebar overlay */} + {sidebarOpen && ( +
+
setSidebarOpen(false)} + /> + +
+ )} + + {/* Main content */} +
+ {/* Query controls */} +
+ {/* DB mode toggle row */} +
-
-
-
- - -
+ {/* Institution selector */} +
+ + +
-
- - setPrompt(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault() - handleAnalyze() - } - }} - /> -
+ {/* Query textarea */} +
+ +