File size: 9,058 Bytes
abd4352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c960c82
 
 
 
 
 
 
 
 
 
 
abd4352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5f9c5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
abd4352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
const BASE = import.meta.env.VITE_API_BASE_URL || ''

export interface QueryRequest {
  user_query: string
  connector_id: string
  session_id: string
  user_id?: string
}

export interface TraceEvent {
  type: 'trace'
  node: string
  status: 'started' | 'completed' | 'failed'
  latency_ms: number
  tokens_used: number
  metadata: Record<string, unknown>
}

export interface QueryResponse {
  session_id: string
  intent: string
  generated_code: string
  code_type: string
  execution_result: Record<string, unknown>[]
  insight_text: string
  chart_spec: ChartSpec | null
  from_cache: boolean
  latency_ms: number
  correction_attempts: number
  history_id: string | null
  anomalies: string[]
  trace: TraceEvent[]
  query_plan?: { tables?: string[]; approach?: string; complexity?: string }
  trace_summary?: {
    total_latency_ms: number
    total_tokens: number
    nodes_executed: number
    events: TraceEvent[]
  }
}

export interface ChartSpec {
  type: string
  plotly_json?: { data: unknown[]; layout: unknown }
  data?: Record<string, unknown>[]
  columns?: string[]
}

export interface HistoryRecord {
  id: string
  session_id: string
  user_query: string
  code_type: string
  insight_text: string | null
  latency_ms: number | null
  retry_count: number | null
  created_at: string
}

export interface Panel {
  id: string
  user_id: string
  session_id: string
  dashboard_id: string | null
  title: string
  chart_spec: ChartSpec
  query: string
  created_at: string
}

export interface MetricsData {
  uptime_seconds: number
  total_queries: number
  latency: { p50_ms: number; p95_ms: number; p99_ms: number; avg_ms: number }
  cache: { hits: number; misses: number; hit_ratio: number }
  self_correction: { avg_retries: number; queries_needing_correction: number; correction_rate: number }
  tokens: { total: number; avg_per_query: number }
  intents: Record<string, number>
  errors: Record<string, number>
}

export interface ProfileData {
  connector_id: string
  total_tables: number
  total_columns: number
  tables: {
    name: string
    row_count: number
    column_count: number
    columns: {
      name: string
      type: string
      null_count: number
      null_rate: number
      unique_count: number
      cardinality: string
      inferred_type?: string
      stats?: { count: number; mean: number; median: number; min: number; max: number; std: number }
      histogram?: { range: string; count: number }[]
      top_values?: { value: string; count: number; frequency: number }[]
    }[]
    correlations: { column_a: string; column_b: string; correlation: number; strength: string }[]
  }[]
}

// ── Query ──────────────────────────────────────────────────────────────────────

export async function runQuery(req: QueryRequest): Promise<QueryResponse> {
  const res = await fetch(`${BASE}/api/query/run`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(req),
  })
  if (!res.ok) throw new Error(`Query failed: ${await res.text()}`)
  return res.json()
}

export type StreamEvent = { token?: string; type?: string } & Partial<QueryResponse> & { done?: boolean }

export async function* streamQuery(req: QueryRequest): AsyncGenerator<StreamEvent> {
  const res = await fetch(`${BASE}/api/query/stream`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(req),
  })
  if (!res.ok) throw new Error(`Stream failed: ${await res.text()}`)
  const reader = res.body!.getReader()
  const decoder = new TextDecoder()
  let buf = ''
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    buf += decoder.decode(value, { stream: true })
    const lines = buf.split('\n')
    buf = lines.pop() ?? ''
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        try { yield JSON.parse(line.slice(6)) } catch { /* ignore */ }
      }
    }
  }
}

export async function fetchSuggestions(connectorId: string): Promise<string[]> {
  const res = await fetch(`${BASE}/api/query/suggest`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ connector_id: connectorId })
  })
  if (!res.ok) throw new Error('Failed to fetch suggestions')
  const data = await res.json()
  return data.suggestions || []
}

// ── History ────────────────────────────────────────────────────────────────────

export async function getHistory(sessionId: string): Promise<HistoryRecord[]> {
  const res = await fetch(`${BASE}/api/history/${sessionId}`)
  if (!res.ok) throw new Error('History fetch failed')
  return res.json()
}

export async function deleteHistoryRecord(id: string): Promise<void> {
  await fetch(`${BASE}/api/history/${id}`, { method: 'DELETE' })
}

// ── Upload ─────────────────────────────────────────────────────────────────────

export { setConnectorId } from '@/session'

export async function uploadFile(file: File, userId = 'anonymous'): Promise<{ connector_id: string; tables_ingested: number }> {
  const form = new FormData()
  form.append('file', file)
  form.append('user_id', userId)
  const res = await fetch(`${BASE}/api/upload/file`, { method: 'POST', body: form })
  if (!res.ok) throw new Error(`Upload failed: ${await res.text()}`)
  return res.json()
}

export async function connectDatabase(url: string, schemaName = 'public'): Promise<{ connector_id: string; tables_ingested: number }> {
  const res = await fetch(`${BASE}/api/upload/connect-db`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ url, schema_name: schemaName }),
  })
  if (!res.ok) {
    let errMsg = 'Connection failed'
    try {
      const data = await res.json()
      errMsg = data.detail || errMsg
    } catch {
      errMsg = await res.text() || errMsg
    }
    throw new Error(errMsg)
  }
  return res.json()
}

// ── Dashboard ─────────────────────────────────────────────────────────────────

export async function getPanels(userId: string): Promise<Panel[]> {
  const res = await fetch(`${BASE}/api/dashboard/panel/${userId}`)
  if (!res.ok) throw new Error('Panels fetch failed')
  return res.json()
}

export async function savePanel(panel: Omit<Panel, 'id' | 'created_at'>): Promise<string> {
  const res = await fetch(`${BASE}/api/dashboard/panel`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(panel),
  })
  if (!res.ok) throw new Error('Save panel failed')
  const d = await res.json()
  return d.panel_id
}

export async function deletePanel(panelId: string, userId: string): Promise<void> {
  await fetch(`${BASE}/api/dashboard/panel/${panelId}?user_id=${userId}`, { method: 'DELETE' })
}

// ── Report ────────────────────────────────────────────────────────────────────

export async function downloadReport(sessionId: string, userId: string): Promise<void> {
  const res = await fetch(`${BASE}/api/report/generate`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ session_id: sessionId, user_id: userId }),
  })
  if (!res.ok) throw new Error('Report generation failed')
  const blob = await res.blob()
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `report-${sessionId.slice(0, 8)}.pdf`
  a.click()
  URL.revokeObjectURL(url)
}

// ── Metrics ───────────────────────────────────────────────────────────────────

export async function getMetrics(): Promise<MetricsData> {
  const res = await fetch(`${BASE}/api/metrics/`)
  if (!res.ok) throw new Error('Metrics fetch failed')
  return res.json()
}

// ── Profile ───────────────────────────────────────────────────────────────────

export async function getProfile(connectorId: string): Promise<ProfileData> {
  const res = await fetch(`${BASE}/api/profile/`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ connector_id: connectorId }),
  })
  if (!res.ok) throw new Error('Profile failed')
  return res.json()
}