ExplainVisualizer.Header.tsx147 lines · main
1import { Code, Eye, HelpCircle } from 'lucide-react'
2import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
3
4import { buildExplainPrompt } from './ExplainVisualizer.ai'
5import type { QueryPlanRow } from './ExplainVisualizer.types'
6import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
7import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
8import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
9import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
10import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
11
12export interface ExplainSummary {
13 totalTime: number
14 hasSeqScan: boolean
15 hasIndexScan: boolean
16}
17
18export interface ExplainHeaderProps {
19 mode: 'visual' | 'raw'
20 onToggleMode: () => void
21 summary?: ExplainSummary
22 id?: string
23 rows?: readonly QueryPlanRow[]
24}
25
26export function ExplainHeader({ mode, onToggleMode, summary, id, rows }: ExplainHeaderProps) {
27 const isVisual = mode === 'visual'
28
29 const snapV2 = useSqlEditorV2StateSnapshot()
30 const { openSidebar } = useSidebarManagerSnapshot()
31 const aiSnap = useAiAssistantStateSnapshot()
32
33 const getPromptData = () => {
34 if (!id) return null
35 const snippet = snapV2.snippets[id]?.snippet
36 if (!snippet?.content?.unchecked_sql) return null
37
38 return buildExplainPrompt({
39 sql: snippet.content.unchecked_sql,
40 explainPlanRows: (rows as QueryPlanRow[]) ?? [],
41 })
42 }
43
44 const handleExplainWithAI = () => {
45 const promptData = getPromptData()
46 if (!promptData) return
47
48 openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
49 aiSnap.newChat({
50 sqlSnippets: [
51 {
52 label: 'Query',
53 content: promptData.query,
54 },
55 ],
56 initialMessage: promptData.prompt,
57 })
58 }
59
60 const buildPromptForCopy = () => {
61 const promptData = getPromptData()
62 if (!promptData) return ''
63
64 // Combine SQL and prompt into a single copyable text
65 return `${promptData.prompt}\n\nSQL Query:\n\`\`\`sql\n${promptData.query}\n\`\`\``
66 }
67
68 const hasSummaryStats =
69 isVisual && summary && (summary.totalTime > 0 || (summary.hasSeqScan && !summary.hasIndexScan))
70
71 return (
72 <div className="bg-surface-100 border-b px-4 py-3 flex flex-col gap-3 text-xs">
73 {/* Title row */}
74 <div className="flex items-center justify-between text-sm">
75 <div className="flex items-center gap-2">
76 <h3 className="font-medium text-foreground">Query Execution Plan</h3>
77 <Tooltip>
78 <TooltipTrigger>
79 <HelpCircle
80 size="14"
81 strokeWidth={2}
82 className="text-foreground-lighter hover:text-foreground-light transition-colors cursor-help"
83 />
84 </TooltipTrigger>
85 <TooltipContent side="top" className="max-w-xs p-4">
86 <h3 className="text-xs font-medium mb-2">How to read</h3>
87 <p className="text-foreground-light text-xs">
88 Start at the bottom where data is read from tables, then follow upward as each step
89 processes the results.
90 </p>
91
92 {isVisual && (
93 <>
94 <h3 className="text-xs font-medium mt-4 mb-2">Key</h3>
95 <div className="flex flex-col gap-1 text-foreground-light">
96 <div className="flex items-center gap-1.5">
97 <span className="inline-block w-1.5 h-1.5 rounded-full bg-warning" />
98 <span>Seq Scan (slow)</span>
99 </div>
100 <div className="flex items-center gap-1.5">
101 <span className="inline-block w-1.5 h-1.5 rounded-full bg-brand" />
102 <span>Index Scan (fast)</span>
103 </div>
104 </div>
105 </>
106 )}
107 </TooltipContent>
108 </Tooltip>
109 {/* Summary stats - only show in visual mode when we have the data */}
110 {hasSummaryStats && (
111 <div className="flex items-center gap-4 flex-wrap">
112 {summary.totalTime > 0 && (
113 <div className="flex items-center gap-2">
114 <span className="text-foreground-muted">/</span>
115 <span className="text-foreground-light">Total time</span>
116 <span className="font-medium text-foreground">
117 {summary.totalTime.toFixed(2)}ms
118 </span>
119 </div>
120 )}
121 </div>
122 )}
123 </div>
124 <div className="flex items-center gap-2">
125 {id && rows && (
126 <AiAssistantDropdown
127 label="Explain with AI"
128 buildPrompt={buildPromptForCopy}
129 onOpenAssistant={handleExplainWithAI}
130 telemetrySource="explain_visualizer"
131 size="tiny"
132 type="default"
133 />
134 )}
135 <Button
136 type="default"
137 size="tiny"
138 icon={isVisual ? <Code size={14} /> : <Eye size={14} />}
139 onClick={onToggleMode}
140 >
141 {isVisual ? 'Raw' : 'Visual'}
142 </Button>
143 </div>
144 </div>
145 </div>
146 )
147}