11"use client"
22
33import { useState } from "react"
4- import { Card , CardContent , CardDescription , CardHeader , CardTitle } from "@/components/ui/card"
54import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@/components/ui/select"
6- import { Input } from "@/components/ui/input"
75import { Button } from "@/components/ui/button"
86import { Switch } from "@/components/ui/switch"
97import { Label } from "@/components/ui/label"
@@ -13,8 +11,7 @@ import { QueryHistoryPanel } from "@/components/query-history-panel"
1311import { analyzePrompt } from "@/lib/prompt-analyzer"
1412import { executeQuery } from "@/lib/query-executor"
1513import type { QueryPlan , QueryResult , HistoryEntry } from "@/lib/types"
16- import Link from "next/link"
17- import { ArrowLeft } from "lucide-react"
14+ import { Loader2 , Sparkles , PanelLeft } from "lucide-react"
1815
1916const INSTITUTIONS = [
2017 { name : "Bishop State" , code : "bscc" } ,
@@ -30,6 +27,10 @@ export default function QueryPage() {
3027 const [ queryPlan , setQueryPlan ] = useState < QueryPlan | null > ( null )
3128 const [ queryResult , setQueryResult ] = useState < QueryResult | null > ( null )
3229 const [ useDirectDB , setUseDirectDB ] = useState ( true )
30+ const [ sidebarOpen , setSidebarOpen ] = useState ( false )
31+ const [ summary , setSummary ] = useState < string | null > ( null )
32+ const [ summaryLoading , setSummaryLoading ] = useState ( false )
33+ const [ summaryError , setSummaryError ] = useState < string | null > ( null )
3334 const [ history , setHistory ] = useState < HistoryEntry [ ] > ( ( ) => {
3435 // Read from localStorage on mount (client-only)
3536 if ( typeof window === "undefined" ) return [ ]
@@ -44,6 +45,9 @@ export default function QueryPage() {
4445 overridePrompt ?: string ,
4546 overrideInstitution ?: string ,
4647 ) => {
48+ setSummary ( null )
49+ setSummaryError ( null )
50+
4751 const activePrompt = overridePrompt ?? prompt
4852 const activeInstitution = overrideInstitution ?? institution
4953
@@ -125,100 +129,205 @@ export default function QueryPage() {
125129 localStorage . removeItem ( "bishop_query_history" )
126130 }
127131
132+ const handleSummarize = async ( ) => {
133+ if ( ! queryResult || ! queryPlan ) return
134+ setSummaryLoading ( true )
135+ setSummaryError ( null )
136+ try {
137+ const res = await fetch ( "/api/query-summary" , {
138+ method : "POST" ,
139+ headers : { "Content-Type" : "application/json" } ,
140+ body : JSON . stringify ( {
141+ prompt,
142+ data : queryResult . data ,
143+ rowCount : queryResult . rowCount ,
144+ vizType : queryPlan . vizType ,
145+ } ) ,
146+ } )
147+ const json = await res . json ( )
148+ if ( ! res . ok ) throw new Error ( json . error || "Failed" )
149+ setSummary ( json . summary )
150+ } catch ( e ) {
151+ setSummaryError ( e instanceof Error ? e . message : String ( e ) )
152+ } finally {
153+ setSummaryLoading ( false )
154+ }
155+ }
156+
128157 return (
129- < div className = "min-h-screen bg-background" >
130- < div className = "container mx-auto p-6 space-y-6" >
131- < div className = "border-b border-border pb-6" >
132- < Link href = "/" className = "inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-4" >
133- < ArrowLeft className = "h-4 w-4 mr-2" />
134- Back to Dashboard
135- </ Link >
136- < h1 className = "text-3xl font-bold tracking-tight text-foreground" > SQL Query Interface</ h1 >
137- < p className = "text-muted-foreground mt-2" > Analyze student performance data with natural language queries</ p >
138- </ div >
139-
140- < Card >
141- < CardHeader >
142- < CardTitle > Query Controls</ CardTitle >
143- < CardDescription > Select an institution and enter your analysis prompt</ CardDescription >
144- </ CardHeader >
145- < CardContent className = "space-y-4" >
146- < div className = "flex items-center space-x-2 pb-4 border-b border-border" >
158+ < div className = "min-h-screen bg-background flex flex-col" >
159+ { /* Slim page-level header bar */ }
160+ < header className = "h-12 flex items-center gap-3 px-4 border-b border-border/60 shrink-0" >
161+ { /* Mobile sidebar toggle */ }
162+ < button
163+ onClick = { ( ) => setSidebarOpen ( true ) }
164+ className = "md:hidden p-1.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
165+ aria-label = "Open query history"
166+ >
167+ < PanelLeft className = "h-4 w-4" />
168+ </ button >
169+
170+ { /* Title group */ }
171+ < span className = "text-sm font-semibold tracking-widest uppercase text-foreground" >
172+ Query Interface
173+ </ span >
174+ < span className = "hidden sm:block h-4 w-px bg-border/60" aria-hidden = "true" />
175+ < span className = "hidden sm:block text-xs text-muted-foreground font-mono" >
176+ Natural Language Analytics
177+ </ span >
178+ </ header >
179+
180+ { /* Body: sidebar + main */ }
181+ < div className = "flex flex-1 overflow-hidden" >
182+ { /* Desktop sidebar */ }
183+ < aside className = "hidden md:flex w-[260px] border-r border-border/60 flex-col shrink-0 bg-muted/20" >
184+ < QueryHistoryPanel entries = { history } onRerun = { handleRerun } onClear = { handleClear } />
185+ </ aside >
186+
187+ { /* Mobile sidebar overlay */ }
188+ { sidebarOpen && (
189+ < div className = "fixed inset-0 z-50 md:hidden" >
190+ < div
191+ className = "absolute inset-0 bg-background/80 backdrop-blur-sm"
192+ onClick = { ( ) => setSidebarOpen ( false ) }
193+ />
194+ < aside className = "absolute left-0 top-0 bottom-0 w-[260px] bg-background border-r border-border/60 flex flex-col" >
195+ < QueryHistoryPanel entries = { history } onRerun = { handleRerun } onClear = { handleClear } />
196+ </ aside >
197+ </ div >
198+ ) }
199+
200+ { /* Main content */ }
201+ < main className = "flex-1 overflow-auto p-6 space-y-6" >
202+ { /* Query controls */ }
203+ < div className = "border border-border/60 rounded-lg p-5 space-y-4" >
204+ { /* DB mode toggle row */ }
205+ < div className = "flex items-center gap-3 pb-4 border-b border-border/40" >
147206 < Switch id = "db-mode" checked = { useDirectDB } onCheckedChange = { setUseDirectDB } />
148- < Label htmlFor = "db-mode" className = "text-sm font-medium" >
207+ < Label htmlFor = "db-mode" className = "text-sm font-medium cursor-pointer " >
149208 { useDirectDB ? "Direct Database" : "API Mode" }
150209 </ Label >
151- < span className = "text-xs text-muted-foreground" >
152- { useDirectDB ? "(Execute SQL directly)" : "(Fetch from API endpoints)" }
210+ < span className = "text-xs text-muted-foreground font-mono " >
211+ { useDirectDB ? "(execute SQL directly)" : "(fetch from API endpoints)" }
153212 </ span >
154213 </ div >
155214
156- < div className = "grid gap-4 md:grid-cols-[200px_1fr_auto]" >
157- < div className = "space-y-2" >
158- < label className = "text-sm font-medium text-foreground" > Institution</ label >
159- < Select value = { institution } onValueChange = { setInstitution } >
160- < SelectTrigger >
161- < SelectValue />
162- </ SelectTrigger >
163- < SelectContent >
164- { INSTITUTIONS . map ( ( inst ) => (
165- < SelectItem key = { inst . code } value = { inst . code } >
166- { inst . name }
167- </ SelectItem >
168- ) ) }
169- </ SelectContent >
170- </ Select >
171- </ div >
215+ { /* Institution selector */ }
216+ < div className = "space-y-1.5" >
217+ < label className = "text-[10px] font-semibold tracking-widest uppercase text-muted-foreground/70" >
218+ Institution
219+ </ label >
220+ < Select value = { institution } onValueChange = { setInstitution } >
221+ < SelectTrigger className = "border-border/60 bg-background text-sm w-full md:w-[220px]" >
222+ < SelectValue />
223+ </ SelectTrigger >
224+ < SelectContent >
225+ { INSTITUTIONS . map ( ( inst ) => (
226+ < SelectItem key = { inst . code } value = { inst . code } >
227+ { inst . name }
228+ </ SelectItem >
229+ ) ) }
230+ </ SelectContent >
231+ </ Select >
232+ </ div >
172233
173- < div className = "space-y-2" >
174- < label className = "text-sm font-medium text-foreground" > Analysis Prompt</ label >
175- < Input
176- placeholder = "e.g., retention by cohort for last two terms"
177- value = { prompt }
178- onChange = { ( e ) => setPrompt ( e . target . value ) }
179- onKeyDown = { ( e ) => {
180- if ( e . key === "Enter" && ! e . shiftKey ) {
181- e . preventDefault ( )
182- handleAnalyze ( )
183- }
184- } }
185- />
186- </ div >
234+ { /* Query textarea */ }
235+ < div className = "space-y-1.5" >
236+ < label className = "text-[10px] font-semibold tracking-widest uppercase text-muted-foreground/70" >
237+ Natural Language Query
238+ </ label >
239+ < textarea
240+ placeholder = "e.g., retention by cohort for last two terms"
241+ value = { prompt }
242+ onChange = { ( e ) => setPrompt ( e . target . value ) }
243+ onKeyDown = { ( e ) => {
244+ if ( e . key === "Enter" && ! e . shiftKey ) {
245+ e . preventDefault ( )
246+ handleAnalyze ( )
247+ }
248+ } }
249+ className = "flex w-full rounded-md border border-border/60 bg-muted/20 px-3 py-2 font-mono text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 min-h-[80px] resize-none"
250+ />
251+ </ div >
252+
253+ { /* Run button row */ }
254+ < div className = "flex justify-end" >
255+ < Button
256+ onClick = { ( ) => handleAnalyze ( ) }
257+ disabled = { isAnalyzing || ! prompt . trim ( ) }
258+ className = "bg-foreground text-background hover:bg-foreground/90 text-xs font-semibold tracking-wide uppercase px-5"
259+ >
260+ { isAnalyzing ? (
261+ < >
262+ < Loader2 className = "h-3 w-3 animate-spin mr-1.5" />
263+ Analyzing…
264+ </ >
265+ ) : (
266+ "Run Analysis"
267+ ) }
268+ </ Button >
269+ </ div >
270+ </ div >
187271
188- < div className = "flex items-end" >
189- < Button onClick = { ( ) => handleAnalyze ( ) } disabled = { isAnalyzing || ! prompt . trim ( ) } className = "w-full md:w-auto" >
190- { isAnalyzing ? "Analyzing..." : "Analyze" }
191- </ Button >
272+ { /* Empty state */ }
273+ { ! queryResult && (
274+ < div className = "border border-dashed border-border/50 rounded-lg py-12 flex items-center justify-center" >
275+ < div className = "text-center space-y-1.5" >
276+ < p className = "text-sm text-muted-foreground" > Enter a query above and run analysis to see results</ p >
277+ < p className = "text-xs text-muted-foreground font-mono" >
278+ Try: “Show me all cohorts” or “Count students by term”
279+ </ p >
192280 </ div >
193281 </ div >
194- </ CardContent >
195- </ Card >
196-
197- { history . length > 0 && (
198- < QueryHistoryPanel
199- entries = { history }
200- onRerun = { handleRerun }
201- onClear = { handleClear }
202- />
203- ) }
282+ ) }
204283
205- { queryResult && queryPlan && (
206- < div className = "grid gap-6 lg:grid-cols-[1fr_400px]" >
207- < AnalysisResult result = { queryResult } plan = { queryPlan } />
208- < QueryPlanPanel plan = { queryPlan } />
209- </ div >
210- ) }
284+ { /* Results section */ }
285+ { queryResult && queryPlan && (
286+ < div className = "space-y-3" >
287+ { /* Results header row */ }
288+ < div className = "flex items-center justify-between" >
289+ < h2 className = "text-[10px] font-semibold text-muted-foreground uppercase tracking-widest" >
290+ Results
291+ </ h2 >
292+ { ! summary && (
293+ < button
294+ onClick = { handleSummarize }
295+ disabled = { summaryLoading }
296+ className = "inline-flex items-center gap-1.5 text-xs font-medium text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 disabled:opacity-50 transition-colors"
297+ >
298+ { summaryLoading ? (
299+ < >
300+ < Loader2 className = "h-3 w-3 animate-spin" />
301+ Generating…
302+ </ >
303+ ) : (
304+ < >
305+ < Sparkles className = "h-3 w-3" />
306+ Summarize
307+ </ >
308+ ) }
309+ </ button >
310+ ) }
311+ </ div >
211312
212- { ! queryResult && (
213- < Card className = "border-dashed" >
214- < CardContent className = "flex items-center justify-center py-12" >
215- < div className = "text-center space-y-2" >
216- < p className = "text-muted-foreground" > Enter a prompt and click Analyze to see results</ p >
217- < p className = "text-sm text-muted-foreground" > Try: "Show me all cohorts" or "Count students by term"</ p >
313+ { /* AI summary block */ }
314+ { summary && (
315+ < div className = "flex gap-2 px-4 py-3 rounded-lg bg-amber-50/50 dark:bg-amber-950/20 border border-amber-200/60 dark:border-amber-800/40" >
316+ < Sparkles className = "h-4 w-4 text-amber-500/70 mt-0.5 shrink-0" />
317+ < p className = "text-sm text-foreground/90 leading-relaxed" > { summary } </ p >
318+ </ div >
319+ ) }
320+ { summaryError && (
321+ < p className = "text-xs text-destructive" > { summaryError } </ p >
322+ ) }
323+
324+ < div className = "grid gap-6 lg:grid-cols-[1fr_400px]" >
325+ < AnalysisResult result = { queryResult } plan = { queryPlan } />
326+ < QueryPlanPanel plan = { queryPlan } />
218327 </ div >
219- </ CardContent >
220- </ Card >
221- ) }
328+ </ div >
329+ ) }
330+ </ main >
222331 </ div >
223332 </ div >
224333 )
0 commit comments