jashdoshi77 commited on
Commit
8c6e5a2
Β·
1 Parent(s): 37a0153

added sidebar for conversation history

Browse files
Files changed (5) hide show
  1. app.py +13 -0
  2. db/memory.py +30 -0
  3. frontend/index.html +191 -147
  4. frontend/script.js +155 -0
  5. frontend/style.css +373 -0
app.py CHANGED
@@ -107,6 +107,19 @@ def chat_endpoint(req: QuestionRequest):
107
 
108
  # ── Schema info endpoint (for debugging / transparency) ─────────────────────
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  @app.get("/schema")
111
  def schema_endpoint():
112
  from db.schema import get_schema
 
107
 
108
  # ── Schema info endpoint (for debugging / transparency) ─────────────────────
109
 
110
+ @app.get("/history")
111
+ def history_endpoint(conversation_id: str = "default"):
112
+ from db.memory import get_full_history
113
+ return get_full_history(conversation_id)
114
+
115
+
116
+ @app.delete("/history/{turn_id}")
117
+ def delete_turn_endpoint(turn_id: int):
118
+ from db.memory import delete_turn
119
+ delete_turn(turn_id)
120
+ return {"ok": True}
121
+
122
+
123
  @app.get("/schema")
124
  def schema_endpoint():
125
  from db.schema import get_schema
db/memory.py CHANGED
@@ -63,6 +63,36 @@ def add_turn(conversation_id: str, question: str, answer: str, sql_query: str |
63
  )
64
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  def get_recent_history(conversation_id: str, limit: int = 5) -> List[Dict[str, Any]]:
67
  """Return the most recent `limit` turns for a conversation (oldest first)."""
68
  _ensure_table()
 
63
  )
64
 
65
 
66
+ def delete_turn(turn_id: int) -> None:
67
+ """Delete a single chat history turn by its id."""
68
+ _ensure_table()
69
+ engine = get_engine()
70
+ with engine.begin() as conn:
71
+ conn.execute(
72
+ text("DELETE FROM chat_history WHERE id = :id"),
73
+ {"id": turn_id},
74
+ )
75
+
76
+
77
+ def get_full_history(conversation_id: str) -> List[Dict[str, Any]]:
78
+ """Return ALL turns for a conversation (oldest first) for the sidebar display."""
79
+ _ensure_table()
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
87
+ """
88
+ )
89
+ with engine.connect() as conn:
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]]:
97
  """Return the most recent `limit` turns for a conversation (oldest first)."""
98
  _ensure_table()
frontend/index.html CHANGED
@@ -11,178 +11,222 @@
11
  <link rel="stylesheet" href="/static/style.css" />
12
  </head>
13
  <body>
14
- <!-- ── Background particles ─────────────────────────────────────── -->
 
15
  <div class="bg-effects">
16
  <div class="orb orb-1"></div>
17
  <div class="orb orb-2"></div>
18
  <div class="orb orb-3"></div>
19
  </div>
20
 
21
- <div class="container">
 
22
 
23
- <!-- ── Header ───────────────────────────────────────────────── -->
24
- <header class="header">
25
- <div class="logo">
26
- <div class="logo-icon">
27
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
28
- <path d="M12 2L2 7l10 5 10-5-10-5z"/>
29
- <path d="M2 17l10 5 10-5"/>
30
- <path d="M2 12l10 5 10-5"/>
31
  </svg>
32
- </div>
33
- <div>
34
- <h1>AI SQL Analyst</h1>
35
- <p class="tagline">Intelligent Data Explorer</p>
36
- </div>
37
  </div>
38
-
39
- <!-- ── Model Switcher ──────────────────────────────────── -->
40
- <div class="model-switcher">
41
- <span class="switcher-label">Model</span>
42
- <div class="switcher-track" id="modelSwitcher">
43
- <button class="switcher-btn active" data-provider="groq">
44
- <span class="btn-dot"></span>
45
- Groq
46
- </button>
47
- <button class="switcher-btn" data-provider="openai">
48
- <span class="btn-dot"></span>
49
- OpenAI
50
- </button>
51
- </div>
52
- </div>
53
- </header>
54
-
55
- <!-- ── Input Section ────────────────────────────────────────── -->
56
- <section class="input-section">
57
- <div class="input-card glass">
58
- <label for="questionInput" class="input-label">
59
- Ask a question about your data
60
- </label>
61
- <div class="input-wrapper">
62
- <textarea
63
- id="questionInput"
64
- rows="3"
65
- placeholder="e.g. What are the top 10 customers by total revenue?"
66
- spellcheck="false"
67
- ></textarea>
68
- <button id="submitBtn" class="submit-btn" title="Send question">
69
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
70
- <line x1="22" y1="2" x2="11" y2="13"/>
71
- <polygon points="22 2 15 22 11 13 2 9 22 2"/>
72
- </svg>
73
- </button>
74
- </div>
75
- </div>
76
- </section>
77
-
78
- <!-- ── Loading ──────────────────────────────────────────────── -->
79
- <div id="loadingIndicator" class="loading hidden">
80
- <div class="loading-content">
81
- <div class="spinner"></div>
82
- <p class="loading-text">Reasoning about your question…</p>
83
- <div class="loading-steps">
84
- <span class="step active">Understanding</span>
85
- <span class="step-arrow">β†’</span>
86
- <span class="step">Analyzing Schema</span>
87
- <span class="step-arrow">β†’</span>
88
- <span class="step">Planning Query</span>
89
- <span class="step-arrow">β†’</span>
90
- <span class="step">Generating SQL</span>
91
- <span class="step-arrow">β†’</span>
92
- <span class="step">Executing</span>
93
- <span class="step-arrow">β†’</span>
94
- <span class="step">Interpreting</span>
95
- </div>
96
  </div>
97
- </div>
 
 
 
 
 
 
 
 
 
 
98
 
99
- <!-- ── Results Section ──────────────────────────────────────── -->
100
- <div id="resultsSection" class="results-section hidden">
101
-
102
- <!-- SQL Card -->
103
- <div class="result-card glass" id="sqlCard">
104
- <div class="card-header">
105
- <div class="card-icon sql-icon">
106
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
107
- <polyline points="16 18 22 12 16 6"/>
108
- <polyline points="8 6 2 12 8 18"/>
109
- </svg>
 
 
 
 
 
110
  </div>
111
- <h2>Generated SQL</h2>
112
- <button class="copy-btn" id="copySqlBtn" title="Copy SQL">
113
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
114
- <rect x="9" y="9" width="13" height="13" rx="2"/>
115
- <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
116
- </svg>
117
- </button>
118
- </div>
119
- <pre class="sql-code"><code id="sqlOutput"></code></pre>
120
- </div>
121
 
122
- <!-- Data Card -->
123
- <div class="result-card glass" id="dataCard">
124
- <div class="card-header">
125
- <div class="card-icon data-icon">
126
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
127
- <ellipse cx="12" cy="5" rx="9" ry="3"/>
128
- <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
129
- <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
130
- </svg>
 
 
 
 
131
  </div>
132
- <h2>Query Results</h2>
133
- <span class="row-count" id="rowCount"></span>
134
- </div>
135
- <div class="table-wrapper" id="tableWrapper">
136
- <!-- Table injected by JS -->
137
- </div>
138
- </div>
139
 
140
- <!-- Answer Card -->
141
- <div class="result-card glass" id="answerCard">
142
- <div class="card-header">
143
- <div class="card-icon answer-icon">
144
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
145
- <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
146
- </svg>
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  </div>
148
- <h2>Explanation</h2>
149
- </div>
150
- <p class="answer-text" id="answerOutput"></p>
151
- </div>
152
 
153
- <!-- Insights Card -->
154
- <div class="result-card glass" id="insightsCard">
155
- <div class="card-header">
156
- <div class="card-icon insights-icon">
157
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
158
- <path d="M12 2a7 7 0 017 7c0 2.38-1.19 4.47-3 5.74V17a1 1 0 01-1 1H9a1 1 0 01-1-1v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 017-7z"/>
159
- <line x1="9" y1="21" x2="15" y2="21"/>
160
- </svg>
 
 
 
 
 
 
 
 
 
 
161
  </div>
162
- <h2>Insights</h2>
163
  </div>
164
- <div class="insights-text" id="insightsOutput"></div>
165
- </div>
166
 
167
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
- <!-- ── Error ────────────────────────────────────────────────── -->
170
- <div id="errorSection" class="error-section hidden">
171
- <div class="result-card glass error-card">
172
- <div class="card-header">
173
- <div class="card-icon error-icon">
174
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
175
- <circle cx="12" cy="12" r="10"/>
176
- <line x1="15" y1="9" x2="9" y2="15"/>
177
- <line x1="9" y1="9" x2="15" y2="15"/>
178
- </svg>
 
 
 
 
179
  </div>
180
- <h2>Error</h2>
181
  </div>
182
- <p class="error-text" id="errorOutput"></p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  </div>
 
184
  </div>
185
-
186
  </div>
187
 
188
  <script src="/static/script.js"></script>
 
11
  <link rel="stylesheet" href="/static/style.css" />
12
  </head>
13
  <body>
14
+
15
+ <!-- ── Background particles ──────────────────────────────────────────── -->
16
  <div class="bg-effects">
17
  <div class="orb orb-1"></div>
18
  <div class="orb orb-2"></div>
19
  <div class="orb orb-3"></div>
20
  </div>
21
 
22
+ <!-- ── App shell (sidebar + main) ───────────────────────────────────── -->
23
+ <div class="app-shell">
24
 
25
+ <!-- ── Sidebar ──────────────────────────────────────────────────── -->
26
+ <aside class="sidebar" id="sidebar">
27
+ <div class="sidebar-header">
28
+ <span class="sidebar-title">History</span>
29
+ <button class="sidebar-new-btn" id="newChatBtn" title="New conversation">
30
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
31
+ <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
 
32
  </svg>
33
+ </button>
 
 
 
 
34
  </div>
35
+ <div class="sidebar-list" id="sidebarList">
36
+ <p class="sidebar-empty">No history yet.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  </div>
38
+ </aside>
39
+
40
+ <!-- ── Main content ─────────────────────────────────────────────── -->
41
+ <div class="main-content">
42
+
43
+ <!-- Sidebar toggle -->
44
+ <button class="sidebar-toggle" id="sidebarToggle" title="Toggle history">
45
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
46
+ <line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
47
+ </svg>
48
+ </button>
49
 
50
+ <div class="container">
51
+
52
+ <!-- ── Header ─────────────────────────────────────────── -->
53
+ <header class="header">
54
+ <div class="logo">
55
+ <div class="logo-icon">
56
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
57
+ <path d="M12 2L2 7l10 5 10-5-10-5z"/>
58
+ <path d="M2 17l10 5 10-5"/>
59
+ <path d="M2 12l10 5 10-5"/>
60
+ </svg>
61
+ </div>
62
+ <div>
63
+ <h1>AI SQL Analyst</h1>
64
+ <p class="tagline">Intelligent Data Explorer</p>
65
+ </div>
66
  </div>
 
 
 
 
 
 
 
 
 
 
67
 
68
+ <!-- ── Model Switcher ──────────────────────────── -->
69
+ <div class="model-switcher">
70
+ <span class="switcher-label">Model</span>
71
+ <div class="switcher-track" id="modelSwitcher">
72
+ <button class="switcher-btn active" data-provider="groq">
73
+ <span class="btn-dot"></span>
74
+ Groq
75
+ </button>
76
+ <button class="switcher-btn" data-provider="openai">
77
+ <span class="btn-dot"></span>
78
+ OpenAI
79
+ </button>
80
+ </div>
81
  </div>
82
+ </header>
 
 
 
 
 
 
83
 
84
+ <!-- ── Input Section ──────────────────────────────────── -->
85
+ <section class="input-section">
86
+ <div class="input-card glass">
87
+ <label for="questionInput" class="input-label">
88
+ Ask a question about your data
89
+ </label>
90
+ <div class="input-wrapper">
91
+ <textarea
92
+ id="questionInput"
93
+ rows="3"
94
+ placeholder="e.g. What are the top 10 customers by total revenue?"
95
+ spellcheck="false"
96
+ ></textarea>
97
+ <button id="submitBtn" class="submit-btn" title="Send question">
98
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
99
+ <line x1="22" y1="2" x2="11" y2="13"/>
100
+ <polygon points="22 2 15 22 11 13 2 9 22 2"/>
101
+ </svg>
102
+ </button>
103
+ </div>
104
  </div>
105
+ </section>
 
 
 
106
 
107
+ <!-- ── Loading ────────────────────────────────────────── -->
108
+ <div id="loadingIndicator" class="loading hidden">
109
+ <div class="loading-content">
110
+ <div class="spinner"></div>
111
+ <p class="loading-text">Reasoning about your question…</p>
112
+ <div class="loading-steps">
113
+ <span class="step active">Understanding</span>
114
+ <span class="step-arrow">β†’</span>
115
+ <span class="step">Analyzing Schema</span>
116
+ <span class="step-arrow">β†’</span>
117
+ <span class="step">Planning Query</span>
118
+ <span class="step-arrow">β†’</span>
119
+ <span class="step">Generating SQL</span>
120
+ <span class="step-arrow">β†’</span>
121
+ <span class="step">Executing</span>
122
+ <span class="step-arrow">β†’</span>
123
+ <span class="step">Interpreting</span>
124
+ </div>
125
  </div>
 
126
  </div>
 
 
127
 
128
+ <!-- ── Results Section ────────────────────────────────── -->
129
+ <div id="resultsSection" class="results-section hidden">
130
+
131
+ <!-- SQL Card -->
132
+ <div class="result-card glass" id="sqlCard">
133
+ <div class="card-header">
134
+ <div class="card-icon sql-icon">
135
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
136
+ <polyline points="16 18 22 12 16 6"/>
137
+ <polyline points="8 6 2 12 8 18"/>
138
+ </svg>
139
+ </div>
140
+ <h2>Generated SQL</h2>
141
+ <button class="copy-btn" id="copySqlBtn" title="Copy SQL">
142
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
143
+ <rect x="9" y="9" width="13" height="13" rx="2"/>
144
+ <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
145
+ </svg>
146
+ </button>
147
+ </div>
148
+ <pre class="sql-code"><code id="sqlOutput"></code></pre>
149
+ </div>
150
+
151
+ <!-- Data Card -->
152
+ <div class="result-card glass" id="dataCard">
153
+ <div class="card-header">
154
+ <div class="card-icon data-icon">
155
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
156
+ <ellipse cx="12" cy="5" rx="9" ry="3"/>
157
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
158
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
159
+ </svg>
160
+ </div>
161
+ <h2>Query Results</h2>
162
+ <span class="row-count" id="rowCount"></span>
163
+ </div>
164
+ <div class="table-wrapper" id="tableWrapper"></div>
165
+ </div>
166
+
167
+ <!-- Answer Card -->
168
+ <div class="result-card glass" id="answerCard">
169
+ <div class="card-header">
170
+ <div class="card-icon answer-icon">
171
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
172
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
173
+ </svg>
174
+ </div>
175
+ <h2>Explanation</h2>
176
+ </div>
177
+ <p class="answer-text" id="answerOutput"></p>
178
+ </div>
179
+
180
+ <!-- Insights Card -->
181
+ <div class="result-card glass" id="insightsCard">
182
+ <div class="card-header">
183
+ <div class="card-icon insights-icon">
184
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
185
+ <path d="M12 2a7 7 0 017 7c0 2.38-1.19 4.47-3 5.74V17a1 1 0 01-1 1H9a1 1 0 01-1-1v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 017-7z"/>
186
+ <line x1="9" y1="21" x2="15" y2="21"/>
187
+ </svg>
188
+ </div>
189
+ <h2>Insights</h2>
190
+ </div>
191
+ <div class="insights-text" id="insightsOutput"></div>
192
+ </div>
193
+
194
+ </div>
195
 
196
+ <!-- ── Error ──────────────────────────────────────────── -->
197
+ <div id="errorSection" class="error-section hidden">
198
+ <div class="result-card glass error-card">
199
+ <div class="card-header">
200
+ <div class="card-icon error-icon">
201
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
202
+ <circle cx="12" cy="12" r="10"/>
203
+ <line x1="15" y1="9" x2="9" y2="15"/>
204
+ <line x1="9" y1="9" x2="15" y2="15"/>
205
+ </svg>
206
+ </div>
207
+ <h2>Error</h2>
208
+ </div>
209
+ <p class="error-text" id="errorOutput"></p>
210
  </div>
 
211
  </div>
212
+
213
+ </div><!-- /.container -->
214
+ </div><!-- /.main-content -->
215
+ </div><!-- /.app-shell -->
216
+
217
+ <!-- ── History detail modal ──────────────────────────────────────────── -->
218
+ <div class="modal-overlay hidden" id="historyModal">
219
+ <div class="modal-box glass">
220
+ <div class="modal-header">
221
+ <h3 class="modal-title" id="modalTitle">Conversation Turn</h3>
222
+ <button class="modal-close" id="modalClose" title="Close">
223
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
224
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
225
+ </svg>
226
+ </button>
227
  </div>
228
+ <div class="modal-body" id="modalBody"></div>
229
  </div>
 
230
  </div>
231
 
232
  <script src="/static/script.js"></script>
frontend/script.js CHANGED
@@ -22,6 +22,18 @@
22
 
23
  const modelSwitcher = document.getElementById("modelSwitcher");
24
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  let selectedProvider = "groq";
26
  let loadingStepTimer = null;
27
 
@@ -36,6 +48,148 @@
36
  window.localStorage.setItem("sqlbot_conversation_id", conversationId);
37
  }
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  // ── Model Switcher ───────────────────────────────────────────────────
40
  modelSwitcher.addEventListener("click", (e) => {
41
  const btn = e.target.closest(".switcher-btn");
@@ -80,6 +234,7 @@
80
 
81
  const data = await res.json();
82
  renderResults(data);
 
83
  } catch (err) {
84
  showError(err.message || "Something went wrong. Please try again.");
85
  } finally {
 
22
 
23
  const modelSwitcher = document.getElementById("modelSwitcher");
24
 
25
+ // ── Sidebar DOM refs ──────────────────────────────────────────────────
26
+ const sidebar = document.getElementById("sidebar");
27
+ const sidebarList = document.getElementById("sidebarList");
28
+ const sidebarToggle = document.getElementById("sidebarToggle");
29
+ const newChatBtn = document.getElementById("newChatBtn");
30
+
31
+ // ── Modal DOM refs ────────────────────────────────────────────────────
32
+ const historyModal = document.getElementById("historyModal");
33
+ const modalBody = document.getElementById("modalBody");
34
+ const modalTitle = document.getElementById("modalTitle");
35
+ const modalClose = document.getElementById("modalClose");
36
+
37
  let selectedProvider = "groq";
38
  let loadingStepTimer = null;
39
 
 
48
  window.localStorage.setItem("sqlbot_conversation_id", conversationId);
49
  }
50
 
51
+ // ── Sidebar toggle ───────────────────────────────────────────────────
52
+ let sidebarOpen = true;
53
+
54
+ function setSidebar(open) {
55
+ sidebarOpen = open;
56
+ if (open) {
57
+ sidebar.classList.remove("collapsed");
58
+ } else {
59
+ sidebar.classList.add("collapsed");
60
+ }
61
+ }
62
+
63
+ sidebarToggle.addEventListener("click", () => setSidebar(!sidebarOpen));
64
+
65
+ // ── New chat ─────────────────────────────────────────────────────────
66
+ newChatBtn.addEventListener("click", () => {
67
+ // Generate a brand-new conversation id and clear the UI
68
+ if (window.crypto && window.crypto.randomUUID) {
69
+ conversationId = window.crypto.randomUUID();
70
+ } else {
71
+ conversationId = "conv-" + Date.now().toString(36);
72
+ }
73
+ window.localStorage.setItem("sqlbot_conversation_id", conversationId);
74
+ hideResults();
75
+ hideError();
76
+ questionInput.value = "";
77
+ renderSidebar([]);
78
+ loadSidebarHistory();
79
+ });
80
+
81
+ // ── Sidebar history ───────────────────────────────────────────────────
82
+ async function loadSidebarHistory() {
83
+ try {
84
+ const res = await fetch(`/history?conversation_id=${encodeURIComponent(conversationId)}`);
85
+ if (!res.ok) return;
86
+ const turns = await res.json();
87
+ renderSidebar(turns);
88
+ } catch (_) { /* non-critical */ }
89
+ }
90
+
91
+ function renderSidebar(turns) {
92
+ if (!turns || turns.length === 0) {
93
+ sidebarList.innerHTML = '<p class="sidebar-empty">No history yet.</p>';
94
+ return;
95
+ }
96
+ sidebarList.innerHTML = "";
97
+ // Most-recent first
98
+ [...turns].reverse().forEach((turn, idx) => {
99
+ const item = document.createElement("button");
100
+ item.className = "sidebar-item";
101
+ item.dataset.idx = idx;
102
+
103
+ const date = new Date(turn.created_at);
104
+ const timeStr = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
105
+ const dateStr = date.toLocaleDateString([], { month: "short", day: "numeric" });
106
+
107
+ item.innerHTML = `
108
+ <div class="sidebar-item-icon">
109
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
110
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
111
+ </svg>
112
+ </div>
113
+ <div class="sidebar-item-content">
114
+ <div class="sidebar-item-question">${escapeHtml(turn.question)}</div>
115
+ <div class="sidebar-item-meta">${dateStr} Β· ${timeStr}</div>
116
+ </div>
117
+ <button class="sidebar-delete-btn" title="Delete" data-id="${turn.id}">
118
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
119
+ <polyline points="3 6 5 6 21 6"/>
120
+ <path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/>
121
+ <path d="M10 11v6M14 11v6"/>
122
+ <path d="M9 6V4h6v2"/>
123
+ </svg>
124
+ </button>
125
+ `;
126
+ item.querySelector(".sidebar-item-icon, .sidebar-item-content")
127
+ item.addEventListener("click", (e) => {
128
+ if (e.target.closest(".sidebar-delete-btn")) return;
129
+ openHistoryModal(turn);
130
+ });
131
+ item.querySelector(".sidebar-delete-btn").addEventListener("click", async (e) => {
132
+ e.stopPropagation();
133
+ await deleteTurn(turn.id);
134
+ });
135
+ sidebarList.appendChild(item);
136
+ });
137
+ }
138
+
139
+ // ── Delete turn ──────────────────────────────────────────────────────
140
+ async function deleteTurn(turnId) {
141
+ try {
142
+ await fetch(`/history/${turnId}`, { method: "DELETE" });
143
+ loadSidebarHistory();
144
+ } catch (_) { /* non-critical */ }
145
+ }
146
+
147
+ // ── History modal ─────────────────────────────────────────────────────
148
+ function openHistoryModal(turn) {
149
+ modalTitle.textContent = turn.question.length > 60
150
+ ? turn.question.slice(0, 60) + "…"
151
+ : turn.question;
152
+
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>
159
+ <div class="modal-section-content">${escapeHtml(turn.question)}</div>
160
+ </div>
161
+ ${turn.sql_query ? `
162
+ <div class="modal-section">
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>
169
+ </div>
170
+ <div class="modal-time">${timeStr}</div>
171
+ `;
172
+
173
+ historyModal.classList.remove("hidden");
174
+ document.body.style.overflow = "hidden";
175
+ }
176
+
177
+ function closeModal() {
178
+ historyModal.classList.add("hidden");
179
+ document.body.style.overflow = "";
180
+ }
181
+
182
+ modalClose.addEventListener("click", closeModal);
183
+ historyModal.addEventListener("click", (e) => {
184
+ if (e.target === historyModal) closeModal();
185
+ });
186
+ document.addEventListener("keydown", (e) => {
187
+ if (e.key === "Escape") closeModal();
188
+ });
189
+
190
+ // Load sidebar on startup
191
+ loadSidebarHistory();
192
+
193
  // ── Model Switcher ───────────────────────────────────────────────────
194
  modelSwitcher.addEventListener("click", (e) => {
195
  const btn = e.target.closest(".switcher-btn");
 
234
 
235
  const data = await res.json();
236
  renderResults(data);
237
+ loadSidebarHistory(); // refresh sidebar after each answer
238
  } catch (err) {
239
  showError(err.message || "Something went wrong. Please try again.");
240
  } finally {
frontend/style.css CHANGED
@@ -67,6 +67,362 @@ body {
67
  position: relative;
68
  }
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  /* ── Background Effects ─────────────────────────────────────────────────── */
71
 
72
  .bg-effects {
@@ -126,9 +482,11 @@ body {
126
  .container {
127
  position: relative;
128
  z-index: 1;
 
129
  max-width: 1100px;
130
  margin: 0 auto;
131
  padding: 2rem 1.5rem 4rem;
 
132
  }
133
 
134
  /* ── Glass Card Base ────────────────────────────────────────────────────── */
@@ -606,6 +964,21 @@ tr:hover td {
606
 
607
  /* ── Responsive ─────────────────────────────────────────────────────────── */
608
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  @media (max-width: 640px) {
610
  .container {
611
  padding: 1rem;
 
67
  position: relative;
68
  }
69
 
70
+ /* ── App Shell (sidebar + main) ─────────────────────────────────────────── */
71
+
72
+ .app-shell {
73
+ display: flex;
74
+ min-height: 100vh;
75
+ position: relative;
76
+ z-index: 1;
77
+ }
78
+
79
+ .main-content {
80
+ flex: 1;
81
+ display: flex;
82
+ flex-direction: column;
83
+ min-width: 0;
84
+ position: relative;
85
+ }
86
+
87
+ /* ── Sidebar ────────────────────────────────────────────────────────────── */
88
+
89
+ .sidebar {
90
+ width: 260px;
91
+ min-width: 260px;
92
+ background: rgba(255,255,255,0.92);
93
+ backdrop-filter: blur(20px);
94
+ -webkit-backdrop-filter: blur(20px);
95
+ border-right: 1px solid var(--border-subtle);
96
+ display: flex;
97
+ flex-direction: column;
98
+ height: 100vh;
99
+ position: sticky;
100
+ top: 0;
101
+ overflow: hidden;
102
+ transition: width var(--transition-smooth), min-width var(--transition-smooth);
103
+ z-index: 10;
104
+ }
105
+
106
+ .sidebar.collapsed {
107
+ width: 0;
108
+ min-width: 0;
109
+ border-right: none;
110
+ }
111
+
112
+ .sidebar-header {
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: space-between;
116
+ padding: 1.25rem 1rem 1rem;
117
+ border-bottom: 1px solid var(--border-subtle);
118
+ flex-shrink: 0;
119
+ }
120
+
121
+ .sidebar-title {
122
+ font-size: 0.8rem;
123
+ font-weight: 700;
124
+ text-transform: uppercase;
125
+ letter-spacing: 0.08em;
126
+ color: var(--text-muted);
127
+ }
128
+
129
+ .sidebar-new-btn {
130
+ width: 30px;
131
+ height: 30px;
132
+ border: 1px solid var(--border-subtle);
133
+ background: transparent;
134
+ border-radius: var(--radius-sm);
135
+ color: var(--accent-green-dark);
136
+ cursor: pointer;
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ transition: all var(--transition-fast);
141
+ }
142
+
143
+ .sidebar-new-btn:hover {
144
+ background: rgba(16,185,129,0.08);
145
+ border-color: var(--accent-green);
146
+ }
147
+
148
+ .sidebar-new-btn svg {
149
+ width: 16px;
150
+ height: 16px;
151
+ }
152
+
153
+ .sidebar-list {
154
+ flex: 1;
155
+ overflow-y: auto;
156
+ padding: 0.75rem 0.5rem;
157
+ display: flex;
158
+ flex-direction: column;
159
+ gap: 4px;
160
+ }
161
+
162
+ .sidebar-empty {
163
+ font-size: 0.8rem;
164
+ color: var(--text-muted);
165
+ text-align: center;
166
+ padding: 2rem 1rem;
167
+ }
168
+
169
+ .sidebar-item {
170
+ display: flex;
171
+ align-items: flex-start;
172
+ gap: 0.6rem;
173
+ padding: 0.65rem 0.75rem;
174
+ border-radius: var(--radius-md);
175
+ cursor: pointer;
176
+ border: 1px solid transparent;
177
+ transition: all var(--transition-fast);
178
+ background: transparent;
179
+ text-align: left;
180
+ width: 100%;
181
+ }
182
+
183
+ .sidebar-item:hover {
184
+ background: rgba(16,185,129,0.06);
185
+ border-color: var(--border-subtle);
186
+ }
187
+
188
+ .sidebar-item.active {
189
+ background: rgba(16,185,129,0.1);
190
+ border-color: var(--border-glow);
191
+ }
192
+
193
+ .sidebar-item-icon {
194
+ width: 28px;
195
+ height: 28px;
196
+ border-radius: var(--radius-sm);
197
+ background: rgba(16,185,129,0.1);
198
+ color: var(--accent-green-dark);
199
+ display: flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ flex-shrink: 0;
203
+ }
204
+
205
+ .sidebar-item-icon svg {
206
+ width: 14px;
207
+ height: 14px;
208
+ }
209
+
210
+ .sidebar-item-content {
211
+ flex: 1;
212
+ min-width: 0;
213
+ }
214
+
215
+ .sidebar-item-question {
216
+ font-size: 0.8rem;
217
+ font-weight: 500;
218
+ color: var(--text-primary);
219
+ white-space: nowrap;
220
+ overflow: hidden;
221
+ text-overflow: ellipsis;
222
+ line-height: 1.4;
223
+ }
224
+
225
+ .sidebar-item-meta {
226
+ font-size: 0.7rem;
227
+ color: var(--text-muted);
228
+ margin-top: 2px;
229
+ }
230
+
231
+ .sidebar-delete-btn {
232
+ flex-shrink: 0;
233
+ width: 26px;
234
+ height: 26px;
235
+ border: none;
236
+ background: transparent;
237
+ border-radius: var(--radius-sm);
238
+ color: var(--text-muted);
239
+ cursor: pointer;
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ opacity: 0;
244
+ transition: opacity var(--transition-fast), background var(--transition-fast), color var(--transition-fast);
245
+ padding: 0;
246
+ }
247
+
248
+ .sidebar-delete-btn svg {
249
+ width: 13px;
250
+ height: 13px;
251
+ }
252
+
253
+ .sidebar-item:hover .sidebar-delete-btn {
254
+ opacity: 1;
255
+ }
256
+
257
+ .sidebar-delete-btn:hover {
258
+ background: rgba(244, 63, 94, 0.1);
259
+ color: var(--accent-rose);
260
+ }
261
+
262
+ /* ── Sidebar Toggle Button ───────────────────────────────────────────────── */
263
+
264
+ .sidebar-toggle {
265
+ position: absolute;
266
+ top: 1.1rem;
267
+ left: 1rem;
268
+ z-index: 20;
269
+ width: 36px;
270
+ height: 36px;
271
+ border: 1px solid var(--border-subtle);
272
+ background: rgba(255,255,255,0.9);
273
+ border-radius: var(--radius-sm);
274
+ color: var(--text-secondary);
275
+ cursor: pointer;
276
+ display: flex;
277
+ align-items: center;
278
+ justify-content: center;
279
+ transition: all var(--transition-fast);
280
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
281
+ }
282
+
283
+ .sidebar-toggle:hover {
284
+ background: rgba(16,185,129,0.08);
285
+ border-color: var(--accent-green);
286
+ color: var(--accent-green-dark);
287
+ }
288
+
289
+ .sidebar-toggle svg {
290
+ width: 18px;
291
+ height: 18px;
292
+ }
293
+
294
+ /* ── History Modal ───────────────────────────────────────────────────────── */
295
+
296
+ .modal-overlay {
297
+ position: fixed;
298
+ inset: 0;
299
+ background: rgba(15,23,42,0.45);
300
+ backdrop-filter: blur(4px);
301
+ z-index: 100;
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ padding: 1.5rem;
306
+ animation: fadeIn 0.2s ease;
307
+ }
308
+
309
+ @keyframes fadeIn {
310
+ from { opacity: 0; }
311
+ to { opacity: 1; }
312
+ }
313
+
314
+ .modal-box {
315
+ width: 100%;
316
+ max-width: 780px;
317
+ max-height: 85vh;
318
+ display: flex;
319
+ flex-direction: column;
320
+ border-radius: var(--radius-xl) !important;
321
+ padding: 0 !important;
322
+ overflow: hidden;
323
+ animation: slideUp 0.25s ease;
324
+ }
325
+
326
+ @keyframes slideUp {
327
+ from { transform: translateY(24px); opacity: 0; }
328
+ to { transform: translateY(0); opacity: 1; }
329
+ }
330
+
331
+ .modal-header {
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: space-between;
335
+ padding: 1.25rem 1.5rem;
336
+ border-bottom: 1px solid var(--border-subtle);
337
+ flex-shrink: 0;
338
+ }
339
+
340
+ .modal-title {
341
+ font-size: 1rem;
342
+ font-weight: 700;
343
+ color: var(--text-primary);
344
+ flex: 1;
345
+ white-space: nowrap;
346
+ overflow: hidden;
347
+ text-overflow: ellipsis;
348
+ margin-right: 1rem;
349
+ }
350
+
351
+ .modal-close {
352
+ width: 32px;
353
+ height: 32px;
354
+ border: 1px solid var(--border-subtle);
355
+ background: transparent;
356
+ border-radius: var(--radius-sm);
357
+ color: var(--text-muted);
358
+ cursor: pointer;
359
+ display: flex;
360
+ align-items: center;
361
+ justify-content: center;
362
+ transition: all var(--transition-fast);
363
+ flex-shrink: 0;
364
+ }
365
+
366
+ .modal-close:hover {
367
+ border-color: var(--accent-rose);
368
+ color: var(--accent-rose);
369
+ background: rgba(244,63,94,0.06);
370
+ }
371
+
372
+ .modal-close svg {
373
+ width: 14px;
374
+ height: 14px;
375
+ }
376
+
377
+ .modal-body {
378
+ overflow-y: auto;
379
+ padding: 1.5rem;
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: 1.25rem;
383
+ }
384
+
385
+ .modal-section {
386
+ display: flex;
387
+ flex-direction: column;
388
+ gap: 0.5rem;
389
+ }
390
+
391
+ .modal-section-label {
392
+ font-size: 0.7rem;
393
+ font-weight: 700;
394
+ text-transform: uppercase;
395
+ letter-spacing: 0.08em;
396
+ color: var(--text-muted);
397
+ }
398
+
399
+ .modal-section-content {
400
+ font-size: 0.9rem;
401
+ line-height: 1.7;
402
+ color: var(--text-secondary);
403
+ background: var(--bg-secondary);
404
+ border: 1px solid var(--border-subtle);
405
+ border-radius: var(--radius-md);
406
+ padding: 0.85rem 1rem;
407
+ }
408
+
409
+ .modal-sql {
410
+ font-family: var(--font-mono);
411
+ font-size: 0.82rem;
412
+ background: #f0fdf4;
413
+ border-color: rgba(16,185,129,0.15);
414
+ color: var(--accent-emerald);
415
+ white-space: pre-wrap;
416
+ overflow-x: auto;
417
+ }
418
+
419
+ .modal-time {
420
+ font-size: 0.72rem;
421
+ color: var(--text-muted);
422
+ text-align: right;
423
+ margin-top: -0.5rem;
424
+ }
425
+
426
  /* ── Background Effects ─────────────────────────────────────────────────── */
427
 
428
  .bg-effects {
 
482
  .container {
483
  position: relative;
484
  z-index: 1;
485
+ flex: 1;
486
  max-width: 1100px;
487
  margin: 0 auto;
488
  padding: 2rem 1.5rem 4rem;
489
+ width: 100%;
490
  }
491
 
492
  /* ── Glass Card Base ────────────────────────────────────────────────────── */
 
964
 
965
  /* ── Responsive ─────────────────────────────────────────────────────────── */
966
 
967
+ @media (max-width: 768px) {
968
+ .sidebar {
969
+ position: fixed;
970
+ left: 0;
971
+ top: 0;
972
+ height: 100vh;
973
+ box-shadow: 4px 0 24px rgba(0,0,0,0.08);
974
+ }
975
+
976
+ .sidebar.collapsed {
977
+ width: 0;
978
+ min-width: 0;
979
+ }
980
+ }
981
+
982
  @media (max-width: 640px) {
983
  .container {
984
  padding: 1rem;