jashdoshi77 commited on
Commit
00f0078
Β·
1 Parent(s): 4aa0ed6

feat: show query result table in history sidebar modal

Browse files
Files changed (4) hide show
  1. app.py +8 -2
  2. db/memory.py +45 -19
  3. frontend/script.js +28 -0
  4. frontend/style.css +59 -0
app.py CHANGED
@@ -99,8 +99,14 @@ def chat_endpoint(req: QuestionRequest):
99
  (result.get("sql") or "").replace("\n", " ")[:200],
100
  )
101
 
102
- # Persist this turn for future context
103
- add_turn(conversation_id, req.question, result["answer"], result["sql"])
 
 
 
 
 
 
104
 
105
  return ChatResponse(**result)
106
 
 
99
  (result.get("sql") or "").replace("\n", " ")[:200],
100
  )
101
 
102
+ # Persist this turn for future context (store up to 200 rows so modal can show them)
103
+ add_turn(
104
+ conversation_id,
105
+ req.question,
106
+ result["answer"],
107
+ result["sql"],
108
+ query_result=(result["data"][:200] if result.get("data") else None),
109
+ )
110
 
111
  return ChatResponse(**result)
112
 
db/memory.py CHANGED
@@ -6,6 +6,7 @@ use recent context for follow‑up questions.
6
 
7
  from __future__ import annotations
8
 
 
9
  from typing import Any, List, Dict
10
 
11
  from sqlalchemy import text
@@ -17,38 +18,51 @@ _TABLE_CREATED = False
17
 
18
 
19
  def _ensure_table() -> None:
20
- """Create the chat_history table if it doesn't exist."""
21
  global _TABLE_CREATED
22
  if _TABLE_CREATED:
23
  return
24
 
25
- ddl = text(
26
- """
27
- CREATE TABLE IF NOT EXISTS chat_history (
28
- id BIGSERIAL PRIMARY KEY,
29
- conversation_id TEXT NOT NULL,
30
- question TEXT NOT NULL,
31
- answer TEXT NOT NULL,
32
- sql_query TEXT,
33
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
34
- );
35
- """
36
- )
37
  engine = get_engine()
38
  with engine.begin() as conn:
39
- conn.execute(ddl)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  _TABLE_CREATED = True
42
 
43
 
44
- def add_turn(conversation_id: str, question: str, answer: str, sql_query: str | None) -> None:
 
 
 
 
 
 
45
  """Append a single Q/A turn to the history."""
46
  _ensure_table()
47
  engine = get_engine()
 
48
  insert_stmt = text(
49
  """
50
- INSERT INTO chat_history (conversation_id, question, answer, sql_query)
51
- VALUES (:conversation_id, :question, :answer, :sql_query)
52
  """
53
  )
54
  with engine.begin() as conn:
@@ -59,6 +73,7 @@ def add_turn(conversation_id: str, question: str, answer: str, sql_query: str |
59
  "question": question,
60
  "answer": answer,
61
  "sql_query": sql_query,
 
62
  },
63
  )
64
 
@@ -80,7 +95,7 @@ def get_full_history(conversation_id: str) -> List[Dict[str, Any]]:
80
  engine = get_engine()
81
  query = text(
82
  """
83
- SELECT id, question, answer, sql_query, created_at
84
  FROM chat_history
85
  WHERE conversation_id = :conversation_id
86
  ORDER BY created_at ASC
@@ -90,7 +105,18 @@ def get_full_history(conversation_id: str) -> List[Dict[str, Any]]:
90
  rows = conn.execute(
91
  query, {"conversation_id": conversation_id}
92
  ).mappings().all()
93
- return [dict(r) for r in rows]
 
 
 
 
 
 
 
 
 
 
 
94
 
95
 
96
  def get_recent_history(conversation_id: str, limit: int = 5) -> List[Dict[str, Any]]:
 
6
 
7
  from __future__ import annotations
8
 
9
+ import json
10
  from typing import Any, List, Dict
11
 
12
  from sqlalchemy import text
 
18
 
19
 
20
  def _ensure_table() -> None:
21
+ """Create the chat_history table if it doesn't exist, and add query_result column if missing."""
22
  global _TABLE_CREATED
23
  if _TABLE_CREATED:
24
  return
25
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  engine = get_engine()
27
  with engine.begin() as conn:
28
+ conn.execute(text(
29
+ """
30
+ CREATE TABLE IF NOT EXISTS chat_history (
31
+ id BIGSERIAL PRIMARY KEY,
32
+ conversation_id TEXT NOT NULL,
33
+ question TEXT NOT NULL,
34
+ answer TEXT NOT NULL,
35
+ sql_query TEXT,
36
+ query_result TEXT,
37
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
38
+ );
39
+ """
40
+ ))
41
+ # Migrate existing tables that don't have the query_result column yet
42
+ conn.execute(text(
43
+ """
44
+ ALTER TABLE chat_history ADD COLUMN IF NOT EXISTS query_result TEXT;
45
+ """
46
+ ))
47
 
48
  _TABLE_CREATED = True
49
 
50
 
51
+ def add_turn(
52
+ conversation_id: str,
53
+ question: str,
54
+ answer: str,
55
+ sql_query: str | None,
56
+ query_result: list | None = None,
57
+ ) -> None:
58
  """Append a single Q/A turn to the history."""
59
  _ensure_table()
60
  engine = get_engine()
61
+ result_json = json.dumps(query_result, default=str) if query_result else None
62
  insert_stmt = text(
63
  """
64
+ INSERT INTO chat_history (conversation_id, question, answer, sql_query, query_result)
65
+ VALUES (:conversation_id, :question, :answer, :sql_query, :query_result)
66
  """
67
  )
68
  with engine.begin() as conn:
 
73
  "question": question,
74
  "answer": answer,
75
  "sql_query": sql_query,
76
+ "query_result": result_json,
77
  },
78
  )
79
 
 
95
  engine = get_engine()
96
  query = text(
97
  """
98
+ SELECT id, question, answer, sql_query, query_result, created_at
99
  FROM chat_history
100
  WHERE conversation_id = :conversation_id
101
  ORDER BY created_at ASC
 
105
  rows = conn.execute(
106
  query, {"conversation_id": conversation_id}
107
  ).mappings().all()
108
+
109
+ result = []
110
+ for r in rows:
111
+ row = dict(r)
112
+ # Deserialize query_result JSON string back to a list
113
+ if row.get("query_result"):
114
+ try:
115
+ row["query_result"] = json.loads(row["query_result"])
116
+ except (json.JSONDecodeError, TypeError):
117
+ row["query_result"] = None
118
+ result.append(row)
119
+ return result
120
 
121
 
122
  def get_recent_history(conversation_id: str, limit: int = 5) -> List[Dict[str, Any]]:
frontend/script.js CHANGED
@@ -145,6 +145,27 @@
145
  }
146
 
147
  // ── History modal ─────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  function openHistoryModal(turn) {
149
  modalTitle.textContent = turn.question.length > 60
150
  ? turn.question.slice(0, 60) + "…"
@@ -153,6 +174,9 @@
153
  const date = new Date(turn.created_at);
154
  const timeStr = date.toLocaleString();
155
 
 
 
 
156
  modalBody.innerHTML = `
157
  <div class="modal-section">
158
  <span class="modal-section-label">Question</span>
@@ -163,6 +187,10 @@
163
  <span class="modal-section-label">Generated SQL</span>
164
  <div class="modal-section-content modal-sql">${escapeHtml(turn.sql_query)}</div>
165
  </div>` : ""}
 
 
 
 
166
  <div class="modal-section">
167
  <span class="modal-section-label">AI Explanation</span>
168
  <div class="modal-section-content">${escapeHtml(turn.answer)}</div>
 
145
  }
146
 
147
  // ── History modal ─────────────────────────────────────────────────────
148
+ function buildModalTable(rows) {
149
+ if (!rows || rows.length === 0) return '<p class="modal-no-data">No data returned.</p>';
150
+ const cols = Object.keys(rows[0]);
151
+ let html = '<div class="modal-table-wrapper"><table class="modal-table"><thead><tr>';
152
+ cols.forEach(c => { html += `<th>${escapeHtml(c)}</th>`; });
153
+ html += "</tr></thead><tbody>";
154
+ rows.forEach(row => {
155
+ html += "<tr>";
156
+ cols.forEach(c => {
157
+ const val = row[c];
158
+ html += `<td>${escapeHtml(val === null || val === undefined ? "NULL" : String(val))}</td>`;
159
+ });
160
+ html += "</tr>";
161
+ });
162
+ html += "</tbody></table></div>";
163
+ if (rows.length === 200) {
164
+ html += '<p class="modal-no-data" style="margin-top:0.5rem;">Showing first 200 rows.</p>';
165
+ }
166
+ return html;
167
+ }
168
+
169
  function openHistoryModal(turn) {
170
  modalTitle.textContent = turn.question.length > 60
171
  ? turn.question.slice(0, 60) + "…"
 
174
  const date = new Date(turn.created_at);
175
  const timeStr = date.toLocaleString();
176
 
177
+ const rowCount = turn.query_result ? turn.query_result.length : 0;
178
+ const rowLabel = rowCount === 1 ? "1 row" : `${rowCount} rows`;
179
+
180
  modalBody.innerHTML = `
181
  <div class="modal-section">
182
  <span class="modal-section-label">Question</span>
 
187
  <span class="modal-section-label">Generated SQL</span>
188
  <div class="modal-section-content modal-sql">${escapeHtml(turn.sql_query)}</div>
189
  </div>` : ""}
190
+ <div class="modal-section">
191
+ <span class="modal-section-label">Query Result${turn.query_result ? ` Β· ${rowLabel}` : ""}</span>
192
+ ${buildModalTable(turn.query_result)}
193
+ </div>
194
  <div class="modal-section">
195
  <span class="modal-section-label">AI Explanation</span>
196
  <div class="modal-section-content">${escapeHtml(turn.answer)}</div>
frontend/style.css CHANGED
@@ -423,6 +423,65 @@ body {
423
  margin-top: -0.5rem;
424
  }
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  /* ── Background Effects ─────────────────────────────────────────────────── */
427
 
428
  .bg-effects {
 
423
  margin-top: -0.5rem;
424
  }
425
 
426
+ .modal-table-wrapper {
427
+ overflow-x: auto;
428
+ border: 1px solid var(--border-subtle);
429
+ border-radius: var(--radius-md);
430
+ background: var(--bg-secondary);
431
+ }
432
+
433
+ .modal-table {
434
+ width: 100%;
435
+ border-collapse: collapse;
436
+ font-size: 0.8rem;
437
+ font-family: var(--font-mono);
438
+ }
439
+
440
+ .modal-table thead th {
441
+ background: rgba(16,185,129,0.08);
442
+ color: var(--accent-emerald);
443
+ font-size: 0.72rem;
444
+ font-weight: 700;
445
+ text-transform: uppercase;
446
+ letter-spacing: 0.05em;
447
+ padding: 0.55rem 0.75rem;
448
+ text-align: left;
449
+ border-bottom: 1px solid var(--border-subtle);
450
+ white-space: nowrap;
451
+ }
452
+
453
+ .modal-table tbody tr {
454
+ border-bottom: 1px solid var(--border-subtle);
455
+ transition: background var(--transition-fast);
456
+ }
457
+
458
+ .modal-table tbody tr:last-child {
459
+ border-bottom: none;
460
+ }
461
+
462
+ .modal-table tbody tr:hover {
463
+ background: rgba(16,185,129,0.04);
464
+ }
465
+
466
+ .modal-table tbody td {
467
+ padding: 0.5rem 0.75rem;
468
+ color: var(--text-secondary);
469
+ white-space: nowrap;
470
+ max-width: 260px;
471
+ overflow: hidden;
472
+ text-overflow: ellipsis;
473
+ }
474
+
475
+ .modal-no-data {
476
+ font-size: 0.82rem;
477
+ color: var(--text-muted);
478
+ padding: 0.75rem 1rem;
479
+ background: var(--bg-secondary);
480
+ border: 1px solid var(--border-subtle);
481
+ border-radius: var(--radius-md);
482
+ margin: 0;
483
+ }
484
+
485
  /* ── Background Effects ─────────────────────────────────────────────────── */
486
 
487
  .bg-effects {