qalita 2.3.2__py3-none-any.whl → 2.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. qalita/__main__.py +213 -9
  2. qalita/commands/{agent.py → worker.py} +89 -89
  3. qalita/internal/config.py +26 -19
  4. qalita/internal/utils.py +1 -1
  5. qalita/web/app.py +97 -14
  6. qalita/web/blueprints/context.py +13 -60
  7. qalita/web/blueprints/dashboard.py +35 -76
  8. qalita/web/blueprints/helpers.py +154 -63
  9. qalita/web/blueprints/sources.py +29 -61
  10. qalita/web/blueprints/{agents.py → workers.py} +108 -185
  11. qalita-2.5.2.dist-info/METADATA +66 -0
  12. qalita-2.5.2.dist-info/RECORD +24 -0
  13. {qalita-2.3.2.dist-info → qalita-2.5.2.dist-info}/WHEEL +1 -1
  14. qalita-2.5.2.dist-info/entry_points.txt +2 -0
  15. qalita/web/blueprints/studio.py +0 -1294
  16. qalita/web/public/chatgpt.svg +0 -3
  17. qalita/web/public/claude.png +0 -0
  18. qalita/web/public/favicon.ico +0 -0
  19. qalita/web/public/gemini.png +0 -0
  20. qalita/web/public/logo-no-slogan.png +0 -0
  21. qalita/web/public/logo-white-no-slogan.svg +0 -11
  22. qalita/web/public/mistral.svg +0 -1
  23. qalita/web/public/noise.webp +0 -0
  24. qalita/web/public/ollama.png +0 -0
  25. qalita/web/public/platform.png +0 -0
  26. qalita/web/public/sources-logos/alloy-db.png +0 -0
  27. qalita/web/public/sources-logos/amazon-athena.png +0 -0
  28. qalita/web/public/sources-logos/amazon-rds.png +0 -0
  29. qalita/web/public/sources-logos/api.svg +0 -2
  30. qalita/web/public/sources-logos/avro.svg +0 -20
  31. qalita/web/public/sources-logos/azure-database-mysql.png +0 -0
  32. qalita/web/public/sources-logos/azure-database-postgresql.png +0 -0
  33. qalita/web/public/sources-logos/azure-sql-database.png +0 -0
  34. qalita/web/public/sources-logos/azure-sql-managed-instance.png +0 -0
  35. qalita/web/public/sources-logos/azure-synapse-analytics.png +0 -0
  36. qalita/web/public/sources-logos/azure_blob.svg +0 -1
  37. qalita/web/public/sources-logos/bigquery.png +0 -0
  38. qalita/web/public/sources-logos/cassandra.svg +0 -254
  39. qalita/web/public/sources-logos/clickhouse.png +0 -0
  40. qalita/web/public/sources-logos/cloud-sql.png +0 -0
  41. qalita/web/public/sources-logos/cockroach-db.png +0 -0
  42. qalita/web/public/sources-logos/csv.svg +0 -1
  43. qalita/web/public/sources-logos/database.svg +0 -3
  44. qalita/web/public/sources-logos/databricks.png +0 -0
  45. qalita/web/public/sources-logos/duckdb.png +0 -0
  46. qalita/web/public/sources-logos/elasticsearch.svg +0 -1
  47. qalita/web/public/sources-logos/excel.svg +0 -1
  48. qalita/web/public/sources-logos/file.svg +0 -1
  49. qalita/web/public/sources-logos/folder.svg +0 -6
  50. qalita/web/public/sources-logos/gcs.png +0 -0
  51. qalita/web/public/sources-logos/hdfs.svg +0 -1
  52. qalita/web/public/sources-logos/ibm-db2.png +0 -0
  53. qalita/web/public/sources-logos/json.png +0 -0
  54. qalita/web/public/sources-logos/maria-db.png +0 -0
  55. qalita/web/public/sources-logos/mongodb.svg +0 -1
  56. qalita/web/public/sources-logos/mssql.svg +0 -1
  57. qalita/web/public/sources-logos/mysql.svg +0 -7
  58. qalita/web/public/sources-logos/oracle.svg +0 -4
  59. qalita/web/public/sources-logos/parquet.svg +0 -16
  60. qalita/web/public/sources-logos/picture.png +0 -0
  61. qalita/web/public/sources-logos/postgresql.svg +0 -22
  62. qalita/web/public/sources-logos/questdb.png +0 -0
  63. qalita/web/public/sources-logos/redshift.png +0 -0
  64. qalita/web/public/sources-logos/s3.svg +0 -34
  65. qalita/web/public/sources-logos/sap-hana.png +0 -0
  66. qalita/web/public/sources-logos/sftp.png +0 -0
  67. qalita/web/public/sources-logos/single-store.png +0 -0
  68. qalita/web/public/sources-logos/snowflake.png +0 -0
  69. qalita/web/public/sources-logos/sqlite.svg +0 -104
  70. qalita/web/public/sources-logos/sqlserver.png +0 -0
  71. qalita/web/public/sources-logos/starburst.png +0 -0
  72. qalita/web/public/sources-logos/stream.png +0 -0
  73. qalita/web/public/sources-logos/teradata.png +0 -0
  74. qalita/web/public/sources-logos/timescale.png +0 -0
  75. qalita/web/public/sources-logos/xls.svg +0 -1
  76. qalita/web/public/sources-logos/xlsx.svg +0 -1
  77. qalita/web/public/sources-logos/yugabyte-db.png +0 -0
  78. qalita/web/public/studio-logo.svg +0 -10
  79. qalita/web/public/studio.css +0 -304
  80. qalita/web/public/studio.png +0 -0
  81. qalita/web/public/styles.css +0 -682
  82. qalita/web/templates/dashboard.html +0 -373
  83. qalita/web/templates/navbar.html +0 -40
  84. qalita/web/templates/sources/added.html +0 -57
  85. qalita/web/templates/sources/edit.html +0 -411
  86. qalita/web/templates/sources/select-source.html +0 -128
  87. qalita/web/templates/studio/agent-panel.html +0 -828
  88. qalita/web/templates/studio/context-panel.html +0 -300
  89. qalita/web/templates/studio/index.html +0 -79
  90. qalita/web/templates/studio/navbar.html +0 -14
  91. qalita/web/templates/studio/view-panel.html +0 -529
  92. qalita-2.3.2.dist-info/METADATA +0 -58
  93. qalita-2.3.2.dist-info/RECORD +0 -101
  94. qalita-2.3.2.dist-info/entry_points.txt +0 -3
  95. {qalita-2.3.2.dist-info → qalita-2.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,828 +0,0 @@
1
- <div id="agent_panel_root" style="display:flex; flex-direction:column; flex:1; min-height:0; position:relative;">
2
- <div style="display:flex; flex-direction:row; justify-content:space-between; margin-bottom:2px;">
3
- <div id="provider_selector" style="display:flex; align-items:center; gap:8px;">
4
- <p for="agent_provider" style="min-width: 110px; margin: 0px;">Agent Provider</p>
5
- <select id="agent_provider" class="studio-input" style="max-width:260px; margin-top: 0px;">
6
- <option value="local">Ollama</option>
7
- <option value="openai">ChatGPT</option>
8
- <option value="mistral">Mistral</option>
9
- <option value="claude">Claude</option>
10
- <option value="gemini">Gemini</option>
11
- </select>
12
- <img id="provider_logo_inline" src="/static/ollama.png" alt="Provider Logo"
13
- style="width: 24px; height: 24px; object-fit: contain; margin-left: 4px;" />
14
- <div id="ollama_status_dot" title="Checking Ollama..."
15
- style="width:28px; height:10px; border-radius:50%; background:#9ca3af; box-shadow:0 0 0 1px #e5e7eb;">
16
- </div>
17
- </div>
18
- <div id="controls_top" style="display:flex; align-items:center; gap:8px;">
19
- <button id="btn_mcp" title="Configure MCP"
20
- style="top:4px; right:74px; background:transparent; border:0; cursor:pointer; padding:4px;">
21
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
22
- <path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z" stroke="#6b7280" stroke-width="2" />
23
- <path d="M3 12h2M19 12h2M12 3v2M12 19v2M5 5l1.5 1.5M17.5 17.5L19 19M5 19l1.5-1.5M17.5 6.5L19 5"
24
- stroke="#6b7280" stroke-width="2" stroke-linecap="round" />
25
- </svg>
26
- </button>
27
- </div>
28
- </div>
29
- <div
30
- style="padding:4px 0; margin-bottom:8px; overflow-x:auto; overflow-y:hidden; display:flex; align-items:center; gap:8px; border-bottom:1px solid #e5e7eb;">
31
- <button id="btn_history" title="Conversations"
32
- style="top:4px; right:26px; background:transparent; border:0; cursor:pointer; padding:4px;">
33
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
34
- <path d="M21 12a9 9 0 1 1-3.51-7.07" stroke="#6b7280" stroke-width="2" stroke-linecap="round"
35
- stroke-linejoin="round" />
36
- <path d="M21 3v6h-6" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
37
- <path d="M12 7v5l3 3" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
38
- </svg>
39
- </button>
40
- <div id="agent_tabs" style="display:flex; align-items:center; gap:6px;">
41
- </div>
42
- </div>
43
- <div id="agent_onboarding" class="studio-setup-card" style="display:none;">
44
- <div style="display: flex; justify-content: space-between; align-items: start; gap: 10px;">
45
- <div>
46
- <div class="studio-setup-title">Setup local provider</div>
47
- <p class="studio-setup-text" style="margin-top: 0;">Let's setup your local AI environment.</p>
48
- </div>
49
- <img src="/static/ollama.png" alt="Ollama Logo" style="width: 30px; height: auto;" />
50
- </div>
51
- <ol style="margin-top:3px;">
52
- <li>Install Ollama: <a href="https://ollama.com" target="_blank" rel="noreferrer">https://ollama.com</a></li>
53
- <li>Start the service locally, then verify below.</li>
54
- <li>Pick a default model (ex: <code>gpt-oss:20b</code>).</li>
55
- <li>Save your Studio configuration.</li>
56
- </ol>
57
- <div class="studio-setup-actions">
58
- <button id="btn_check_ollama" class="btn outlined">Check Ollama</button>
59
- <input id="inp_model" class="studio-input" type="text" placeholder="gpt-oss:20b"
60
- style="max-width:260px; margin-top: 0px;" />
61
- <button id="btn_save_studio" class="btn">Save</button>
62
- </div>
63
- <div id="onboarding_status" class="muted" style="margin-top:8px;"></div>
64
- </div>
65
- <div id="agent_chat" style="display:flex; flex-direction:column; min-height:0;">
66
- <div id="chat_log"
67
- style="flex:1; overflow:auto; border:1px solid #e5e7eb; border-radius:4px; padding:5px; background:#fff; min-height:0; height: calc(100vh - 200px);">
68
- </div>
69
- <div class="actions" style="margin-top:12px;">
70
- <input id="chat_input" type="text" placeholder="Ask something..." style="flex:1; width:100%;" />
71
- <button id="chat_send" class="btn" style="margin-top:5px;">Send</button>
72
- <button id="chat_stop" class="btn secondary" style="margin-top:5px; display:none;">Stop</button>
73
- </div>
74
- </div>
75
- <div id="agent_remote_setup" class="studio-setup-card" style="display:none;">
76
- <div style="display: flex; justify-content: space-between; align-items: start; gap: 10px;">
77
- <div>
78
- <div class="studio-setup-title">Setup remote provider</div>
79
- <p class="studio-setup-text" style="margin-top: 0;">Configure your API provider credentials.</p>
80
- </div>
81
- <img id="provider_logo_remote" src="/static/chatgpt.svg" alt="Provider Logo" style="width: 30px; height: auto;" />
82
- </div>
83
- <ol id="remote_help" style="margin-top:10px;">
84
- <li id="help_openai_1" style="display:none;">Create an API key in your <a
85
- href="https://platform.openai.com/api-keys" target="_blank" rel="noreferrer">OpenAI dashboard</a>.</li>
86
- <li id="help_openai_2" style="display:none;">Paste the API key above and choose a model (e.g.,
87
- <code>gpt-4o</code>).
88
- </li>
89
- <li id="help_mistral_1" style="display:none;">Create an API key in your <a
90
- href="https://admin.mistral.ai/organization/api-keys" target="_blank" rel="noreferrer">Mistral dashboard</a>.
91
- </li>
92
- <li id="help_mistral_2" style="display:none;">Paste the API key above and choose a model (e.g.,
93
- <code>mistral-large</code>).
94
- </li>
95
- <li id="help_claude_1" style="display:none;">Create an API key in your <a
96
- href="https://docs.anthropic.com/en/docs/api-keys" target="_blank" rel="noreferrer">Anthropic dashboard</a>.
97
- </li>
98
- <li id="help_claude_2" style="display:none;">Paste the API key above and choose a model (e.g.,
99
- <code>claude-3-5-sonnet-20240620</code>).
100
- </li>
101
- <li id="help_gemini_1" style="display:none;">Create an API key in your <a href="https://ai.google/"
102
- target="_blank" rel="noreferrer">Google AI Studio</a>.</li>
103
- <li id="help_gemini_2" style="display:none;">Paste the API key above and choose a model (e.g.,
104
- <code>gemini-1.5-pro</code>).
105
- </li>
106
- </ol>
107
- <div class="studio-setup-actions">
108
- <input id="inp_remote_api_key" class="studio-input" type="password" placeholder="API Key"
109
- style="min-width:260px;" />
110
- <input id="inp_remote_model" class="studio-input" type="text" placeholder="Model (e.g., gpt-4o, mistral-large)"
111
- style="min-width:260px;" />
112
- <button id="btn_check_remote" class="btn outlined">Check</button>
113
- <button id="btn_save_remote" class="btn">Save</button>
114
- </div>
115
- <div id="remote_status" class="muted" style="margin-top:8px;"></div>
116
- </div>
117
- <div id="history_modal"
118
- style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.35); align-items:center; justify-content:center; z-index:50;">
119
- <div
120
- style="background:#ffffff; border:1px solid #e5e7eb; border-radius:10px; width: min(520px, 92vw); max-width: 520px; box-shadow: 0 10px 30px rgba(0,0,0,.25);">
121
- <div
122
- style="padding:12px 14px; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; justify-content:space-between;">
123
- <div style="font-weight:600; color:#111827;">Conversations</div>
124
- <button id="history_close" class="btn outlined" style="padding:2px 8px;">Close</button>
125
- </div>
126
- <div style="padding:14px; display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
127
- <select id="history_select" class="studio-input" style="min-width:280px;"></select>
128
- <button id="history_load" class="btn">Load</button>
129
- </div>
130
- <div id="history_status" class="muted" style="padding:0 14px 14px 14px;"></div>
131
- </div>
132
- </div>
133
-
134
- <div id="mcp_modal"
135
- style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.35); align-items:center; justify-content:center; z-index:50;">
136
- <div
137
- style="background:#ffffff; border:1px solid #e5e7eb; border-radius:10px; width: min(700px, 92vw); max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,.25);">
138
- <div
139
- style="padding:12px 14px; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; justify-content:space-between;">
140
- <div style="font-weight:600; color:#111827;">MCP Configuration</div>
141
- <button id="mcp_close" class="btn outlined" style="padding:2px 8px;">Close</button>
142
- </div>
143
- <div style="padding:14px; display:flex; flex-direction:column; gap:8px;">
144
- <div class="muted">Enter the MCP configuration (JSON). It will be saved in
145
- <code>~/.qalita/.studio</code> under the provider <code>mcp</code>.
146
- </div>
147
- <textarea id="mcp_textarea" class="studio-input"
148
- style="min-height:220px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;"></textarea>
149
- <div style="display:flex; gap:8px; align-items:center; justify-content:flex-end;">
150
- <div id="mcp_status" class="muted" style="margin-right:auto;"></div>
151
- <button id="mcp_save" class="btn">Save</button>
152
- </div>
153
- </div>
154
- </div>
155
- </div>
156
- </div>
157
-
158
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
159
- <script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
160
- <script>
161
- (function () {
162
- const providerSelect = document.getElementById('agent_provider');
163
- const providerLogoInline = document.getElementById('provider_logo_inline');
164
- const providerLogoRemote = document.getElementById('provider_logo_remote');
165
- const elOnboarding = document.getElementById('agent_onboarding');
166
- const elChat = document.getElementById('agent_chat');
167
- const elRemote = document.getElementById('agent_remote_setup');
168
- const status = document.getElementById('onboarding_status');
169
- const modelInp = document.getElementById('inp_model');
170
- const btnCheck = document.getElementById('btn_check_ollama');
171
- const btnSave = document.getElementById('btn_save_studio');
172
- const log = document.getElementById('chat_log');
173
- const input = document.getElementById('chat_input');
174
- const send = document.getElementById('chat_send');
175
- const stop = document.getElementById('chat_stop');
176
- const statusDot = document.getElementById('ollama_status_dot');
177
- const btnHistory = document.getElementById('btn_history');
178
- const historyModal = document.getElementById('history_modal');
179
- const historyClose = document.getElementById('history_close');
180
- const historySelect = document.getElementById('history_select');
181
- const historyLoad = document.getElementById('history_load');
182
- const historyStatus = document.getElementById('history_status');
183
- const btnMcp = document.getElementById('btn_mcp');
184
- const mcpModal = document.getElementById('mcp_modal');
185
- const mcpClose = document.getElementById('mcp_close');
186
- const mcpTextarea = document.getElementById('mcp_textarea');
187
- const mcpSave = document.getElementById('mcp_save');
188
- const mcpStatus = document.getElementById('mcp_status');
189
- const agentTabsEl = document.getElementById('agent_tabs');
190
- const btnClearIssue = document.getElementById('btn_clear_issue');
191
- let currentConvId = '';
192
- function updateUrlParam(key, value) {
193
- try {
194
- const params = new URLSearchParams(window.location.search);
195
- if (value == null || value === '') params.delete(key); else params.set(key, String(value));
196
- const newUrl = window.location.pathname + (params.toString() ? ('?' + params.toString()) : '');
197
- window.history.replaceState({}, '', newUrl);
198
- } catch (e) { /* noop */ }
199
- }
200
- function readUrlParam(key) {
201
- try { return new URLSearchParams(window.location.search).get(key) || ''; } catch (e) { return ''; }
202
- }
203
- function generateConvId() {
204
- const d = new Date();
205
- const pad = (n) => String(n).padStart(2, '0');
206
- const id = `conv_${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
207
- return id;
208
- }
209
- let lastStatus = null;
210
- let streamingController = null;
211
- let isStreaming = false;
212
-
213
- function readAgentTabs() {
214
- try { return JSON.parse(sessionStorage.getItem('studio_agent_tabs') || '[]'); } catch (e) { return []; }
215
- }
216
- function writeAgentTabs(list) {
217
- try { sessionStorage.setItem('studio_agent_tabs', JSON.stringify(Array.isArray(list) ? list : [])); } catch (e) { }
218
- }
219
- function ensureAgentTab(id) {
220
- if (!id) return;
221
- var tabs = readAgentTabs();
222
- if (tabs.indexOf(id) === -1) { tabs.unshift(id); writeAgentTabs(tabs); }
223
- renderAgentTabs();
224
- }
225
- function renderAgentTabs() {
226
- var root = agentTabsEl; if (!root) return;
227
- root.innerHTML = '';
228
- // Plus button on the left
229
- var plus = document.createElement('button');
230
- plus.title = 'Nouvelle conversation';
231
- plus.textContent = '+';
232
- plus.style.cssText = 'border:1px solid #e5e7eb; background:#ffffff; color:#374151; padding:4px 8px; border-radius:6px; cursor:pointer;';
233
- plus.addEventListener('click', function () {
234
- try {
235
- log.innerHTML = '';
236
- currentConvId = generateConvId();
237
- updateUrlParam('conv_id', currentConvId);
238
- ensureAgentTab(currentConvId);
239
- } catch (e) { }
240
- });
241
- root.appendChild(plus);
242
- // Tabs
243
- var tabs = readAgentTabs();
244
- function removeAgentTab(id) {
245
- var list = readAgentTabs().filter(function (t) { return t !== id; });
246
- writeAgentTabs(list);
247
- // If closing active tab
248
- if (id === currentConvId) {
249
- if (list.length) {
250
- loadConversationById(list[0], true);
251
- } else {
252
- currentConvId = '';
253
- updateUrlParam('conv_id', '');
254
- if (log) log.innerHTML = '';
255
- renderAgentTabs();
256
- }
257
- } else {
258
- renderAgentTabs();
259
- }
260
- }
261
- tabs.forEach(function (id) {
262
- var container = document.createElement('div');
263
- container.style.cssText = 'display:inline-flex; align-items:center; gap:4px; border:1px solid #e5e7eb; background:' + (id === currentConvId ? '#eef2ff' : '#ffffff') + '; color:#111827; padding:2px 4px; border-radius:6px; white-space:nowrap;';
264
- var labelBtn = document.createElement('button');
265
- var label = String(id);
266
- labelBtn.textContent = label;
267
- labelBtn.title = label;
268
- labelBtn.style.cssText = 'background:transparent; border:0; color:inherit; padding:2px 4px; cursor:pointer;';
269
- labelBtn.addEventListener('click', function () { if (id !== currentConvId) { loadConversationById(id, true); } });
270
- var closeBtn = document.createElement('button');
271
- closeBtn.textContent = '×';
272
- closeBtn.title = 'Fermer';
273
- closeBtn.style.cssText = 'background:transparent; border:0; color:#6b7280; padding:2px 4px; cursor:pointer;';
274
- closeBtn.addEventListener('click', function (ev) { ev.stopPropagation(); removeAgentTab(id); });
275
- container.appendChild(labelBtn);
276
- container.appendChild(closeBtn);
277
- root.appendChild(container);
278
- });
279
- }
280
-
281
- function append(role, text) {
282
- const wrapper = document.createElement('div');
283
- wrapper.className = 'msg ' + (role === 'You' ? 'user' : 'agent');
284
- const bubble = document.createElement('div');
285
- bubble.className = 'bubble';
286
- if (role === 'You') {
287
- bubble.textContent = text || '';
288
- } else {
289
- const raw = text || '';
290
- let html = '';
291
- try {
292
- html = (window.marked && window.marked.parse) ? window.marked.parse(raw) : raw.replace(/</g, '&lt;');
293
- } catch (e) {
294
- html = raw.replace(/</g, '&lt;');
295
- }
296
- try {
297
- if (window.DOMPurify && window.DOMPurify.sanitize) {
298
- html = window.DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
299
- }
300
- } catch (e) { /* ignore */ }
301
- bubble.innerHTML = html;
302
- }
303
- wrapper.appendChild(bubble);
304
- log.appendChild(wrapper);
305
- log.scrollTop = log.scrollHeight;
306
- return role === 'You' ? bubble : bubble;
307
- }
308
-
309
- async function refreshStatus() {
310
- try {
311
- const r = await fetch('/studio/status');
312
- const j = await r.json();
313
- lastStatus = j;
314
- // Rebuild provider options depending on cloud flag
315
- setProviderOptions(!!j.cloud_enabled);
316
- const current = j.current_provider || 'local';
317
- if (providerSelect) providerSelect.value = current;
318
- updateProviderUI(current, j);
319
- } catch (e) { /* ignore */ }
320
- }
321
-
322
- function openMcpModal() {
323
- try {
324
- if (!mcpModal) return;
325
- // Load existing MCP config from status if available
326
- const j = lastStatus || {};
327
- let payload = {};
328
- try {
329
- const configs = (j && j.config && j.config.providers) ? j.config.providers : (j.configs || {});
330
- const current = (configs && typeof configs === 'object') ? (configs['mcp'] || {}) : {};
331
- payload = current || {};
332
- } catch (e) { payload = {}; }
333
- if (mcpTextarea) mcpTextarea.value = JSON.stringify(payload, null, 2);
334
- if (mcpStatus) mcpStatus.textContent = '';
335
- mcpModal.style.display = 'flex';
336
- } catch (e) { /* noop */ }
337
- }
338
- function closeMcpModal() { if (mcpModal) mcpModal.style.display = 'none'; }
339
- async function saveMcpConfig() {
340
- if (!mcpTextarea) return;
341
- let obj = null;
342
- try {
343
- obj = JSON.parse(mcpTextarea.value || '{}');
344
- } catch (e) {
345
- if (mcpStatus) mcpStatus.textContent = 'Invalid JSON';
346
- return;
347
- }
348
- if (mcpStatus) mcpStatus.textContent = 'Saving...';
349
- try {
350
- const r = await fetch('/studio/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider: 'mcp', config: obj, set_current: false }) });
351
- const j = await r.json();
352
- if (mcpStatus) mcpStatus.textContent = j.ok ? 'Saved' : (j.message || 'Save failed');
353
- // Refresh status to reflect saved config
354
- refreshStatus();
355
- } catch (e) {
356
- if (mcpStatus) mcpStatus.textContent = 'Save failed';
357
- }
358
- }
359
-
360
- async function checkOllama() {
361
- status.textContent = 'Checking Ollama...';
362
- if (statusDot) { statusDot.style.background = '#9ca3af'; statusDot.title = 'Checking Ollama...'; }
363
- try {
364
- const r = await fetch('/studio/check-ollama');
365
- const j = await r.json();
366
- status.textContent = j.ok ? 'Ollama is reachable on http://127.0.0.1:11434' : 'Ollama is not reachable';
367
- if (statusDot) { statusDot.style.background = j.ok ? '#10b981' : '#ef4444'; statusDot.title = j.ok ? 'Ollama reachable' : 'Ollama not reachable'; }
368
- } catch (e) {
369
- status.textContent = 'Ollama check failed';
370
- if (statusDot) { statusDot.style.background = '#ef4444'; statusDot.title = 'Ollama check failed'; }
371
- }
372
- }
373
-
374
- async function saveStudio() {
375
- const payload = { model: modelInp.value || 'deepseek-r1:8b' };
376
- try {
377
- const r = await fetch('/studio/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider: 'local', config: payload, set_current: true }) });
378
- const j = await r.json();
379
- status.textContent = j.ok ? 'Saved. You can now use the chat.' : (j.message || 'Save failed');
380
- await refreshStatus();
381
- } catch (e) { status.textContent = 'Save failed'; }
382
- }
383
-
384
- async function saveRemote() {
385
- const keyEl = document.getElementById('inp_remote_api_key');
386
- const modelEl = document.getElementById('inp_remote_model');
387
- const key = keyEl && keyEl.value ? keyEl.value : '';
388
- const model = modelEl && modelEl.value ? modelEl.value : '';
389
- const provider = providerSelect ? providerSelect.value : 'openai';
390
- const cfg = { api_key: key, model: model };
391
- const msg = document.getElementById('remote_status');
392
- if (msg) msg.textContent = 'Saving...';
393
- try {
394
- const r = await fetch('/studio/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider: provider, config: cfg, set_current: true }) });
395
- const j = await r.json();
396
- if (msg) msg.textContent = j.ok ? 'Saved.' : (j.message || 'Save failed');
397
- } catch (e) { if (msg) msg.textContent = 'Save failed'; }
398
- }
399
-
400
- async function checkRemote() {
401
- const keyEl = document.getElementById('inp_remote_api_key');
402
- const modelEl = document.getElementById('inp_remote_model');
403
- const key = keyEl && keyEl.value ? keyEl.value : '';
404
- const model = modelEl && modelEl.value ? modelEl.value : '';
405
- const provider = providerSelect ? providerSelect.value : 'openai';
406
- const msg = document.getElementById('remote_status');
407
- if (msg) msg.textContent = 'Checking...';
408
- try {
409
- const r = await fetch('/studio/check-remote', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider, api_key: key, model }) });
410
- const j = await r.json();
411
- if (msg) msg.textContent = j.ok ? 'Connectivity OK' : (j.message || (j.error && (j.error.detail || j.error.message || j.error.error)) || 'Connectivity failed');
412
- } catch (e) { if (msg) msg.textContent = 'Connectivity failed'; }
413
- }
414
-
415
- function setProviderOptions(cloudEnabled) {
416
- try {
417
- if (!providerSelect) return;
418
- // Desired list depending on cloud flag
419
- var desired = cloudEnabled ? [
420
- { v: 'local', t: 'Ollama' },
421
- { v: 'openai', t: 'ChatGPT' },
422
- { v: 'mistral', t: 'Mistral' },
423
- { v: 'claude', t: 'Claude' },
424
- { v: 'gemini', t: 'Gemini' }
425
- ] : [
426
- { v: 'local', t: 'Ollama' }
427
- ];
428
- // If options already match, skip rebuild (lightweight check by values set)
429
- var currentValues = Array.prototype.map.call(providerSelect.options || [], function (o) { return o && o.value; });
430
- var desiredValues = desired.map(function (d) { return d.v; });
431
- var equal = currentValues.length === desiredValues.length && currentValues.every(function (v, i) { return v === desiredValues[i]; });
432
- if (!equal) {
433
- providerSelect.innerHTML = '';
434
- desired.forEach(function (d) {
435
- var opt = document.createElement('option');
436
- opt.value = d.v; opt.textContent = d.t;
437
- providerSelect.appendChild(opt);
438
- });
439
- }
440
- // Force selection to local if cloud is disabled
441
- if (!cloudEnabled) {
442
- providerSelect.value = 'local';
443
- }
444
- } catch (e) { /* noop */ }
445
- }
446
-
447
- function updateProviderUI(provider, statusObj) {
448
- // Update logo
449
- let src = '/static/ollama.png';
450
- let alt = 'Ollama Logo';
451
- if (provider === 'openai') { src = '/static/chatgpt.svg'; alt = 'ChatGPT Logo'; }
452
- else if (provider === 'mistral') { src = '/static/mistral.svg'; alt = 'Mistral Logo'; }
453
- else if (provider === 'claude') { src = '/static/claude.png'; alt = 'Claude Logo'; }
454
- else if (provider === 'gemini') { src = '/static/gemini.png'; alt = 'Gemini Logo'; }
455
- if (providerLogoInline) { providerLogoInline.src = src; providerLogoInline.alt = alt; }
456
- if (providerLogoRemote) { providerLogoRemote.src = src; providerLogoRemote.alt = alt; }
457
- // Toggle setup cards
458
- var cloudEnabled = !!(statusObj && statusObj.cloud_enabled);
459
- if (provider !== 'local' && !cloudEnabled) {
460
- // Force local UI when cloud disabled
461
- provider = 'local';
462
- if (providerSelect) providerSelect.value = 'local';
463
- }
464
- if (provider === 'local') {
465
- const isConfigured = !!(statusObj && statusObj.configured);
466
- elOnboarding.style.display = isConfigured ? 'none' : 'block';
467
- elRemote.style.display = 'none';
468
- elChat.style.display = isConfigured ? 'block' : 'none';
469
- } else {
470
- // Only show remote setup when cloud is enabled
471
- if (cloudEnabled) {
472
- elOnboarding.style.display = 'none';
473
- elRemote.style.display = 'block';
474
- elChat.style.display = 'none';
475
- } else {
476
- const isConfigured = !!(statusObj && statusObj.configured);
477
- elOnboarding.style.display = isConfigured ? 'none' : 'block';
478
- elRemote.style.display = 'none';
479
- elChat.style.display = isConfigured ? 'block' : 'none';
480
- }
481
- }
482
- // Toggle provider-specific help
483
- const h11 = document.getElementById('help_openai_1');
484
- const h12 = document.getElementById('help_openai_2');
485
- const h21 = document.getElementById('help_mistral_1');
486
- const h22 = document.getElementById('help_mistral_2');
487
- const c11 = document.getElementById('help_claude_1');
488
- const c12 = document.getElementById('help_claude_2');
489
- const g11 = document.getElementById('help_gemini_1');
490
- const g12 = document.getElementById('help_gemini_2');
491
- var showHelp = function (cond) { return cond ? '' : 'none'; };
492
- var allowRemoteHelp = cloudEnabled;
493
- if (h11) h11.style.display = allowRemoteHelp && provider === 'openai' ? '' : 'none';
494
- if (h12) h12.style.display = allowRemoteHelp && provider === 'openai' ? '' : 'none';
495
- if (h21) h21.style.display = allowRemoteHelp && provider === 'mistral' ? '' : 'none';
496
- if (h22) h22.style.display = allowRemoteHelp && provider === 'mistral' ? '' : 'none';
497
- if (c11) c11.style.display = allowRemoteHelp && provider === 'claude' ? '' : 'none';
498
- if (c12) c12.style.display = allowRemoteHelp && provider === 'claude' ? '' : 'none';
499
- if (g11) g11.style.display = allowRemoteHelp && provider === 'gemini' ? '' : 'none';
500
- if (g12) g12.style.display = allowRemoteHelp && provider === 'gemini' ? '' : 'none';
501
- }
502
-
503
- // --- Source pre-prompt helpers ---
504
- function buildDomainPreprompt() {
505
- return 'Vous êtes un expert en qualité des données. Votre but est d\'aider à détecter, expliquer et corriger des anomalies de qualité (ex: doublons, valeurs manquantes, incohérences, dérive, outliers, règles métier).';
506
- }
507
- async function fetchSourceDetails(sourceId) {
508
- try {
509
- const url = '/sources/preview?source_id=' + encodeURIComponent(sourceId) + '&verbose=1&limit=1';
510
- const r = await fetch(url);
511
- const j = await r.json();
512
- return j || null;
513
- } catch (e) { return null; }
514
- }
515
- function buildSourceExcerpt(view) {
516
- try {
517
- if (!view || typeof view !== 'object') return '';
518
- const type = String(view.type || '').toLowerCase();
519
- if (type === 'table') {
520
- const rows = Array.isArray(view.rows) ? view.rows : (Array.isArray(view.items) ? view.items : []);
521
- const headers = Array.isArray(view.headers) ? view.headers : (rows.length && typeof rows[0] === 'object' ? Object.keys(rows[0]) : []);
522
- // Normalize rows to array of arrays
523
- let outRows = [];
524
- if (rows.length && typeof rows[0] === 'object' && !Array.isArray(rows[0])) {
525
- outRows = rows.map(function (it) { return headers.map(function (h) { var v = it[h]; return v == null ? '' : String(v); }); });
526
- } else if (rows.length && Array.isArray(rows[0])) {
527
- outRows = rows;
528
- }
529
- const maxRows = 5;
530
- const head = headers.join(' | ');
531
- const sep = headers.map(function () { return '---'; }).join(' | ');
532
- const body = outRows.slice(0, maxRows).map(function (r) { return r.map(function (c) { return String(c); }).join(' | '); }).join('\n');
533
- const more = outRows.length > maxRows ? '\n…' : '';
534
- return 'Aperçu des données (table):\n' + head + '\n' + sep + '\n' + body + more;
535
- }
536
- if (type === 'text' && typeof view.text === 'string') {
537
- const s = view.text.slice(0, 1000);
538
- return 'Aperçu du texte:\n' + s + (view.text.length > 1000 ? '\n…' : '');
539
- }
540
- if (type === 'json' && (view.data != null)) {
541
- let s = '';
542
- try { s = JSON.stringify(view.data, null, 2); } catch (e) { s = String(view.data); }
543
- s = s.slice(0, 1000);
544
- return 'Aperçu JSON:\n' + s + (s.length === 1000 ? '\n…' : '');
545
- }
546
- if (typeof view.csv === 'string' && view.csv) {
547
- const s = view.csv.split(/\r?\n/).slice(0, 8).join('\n');
548
- return 'Aperçu CSV:\n' + s + (view.csv.split(/\r?\n/).length > 8 ? '\n…' : '');
549
- }
550
- return '';
551
- } catch (e) { return ''; }
552
- }
553
- function extractSourceInfo(j) {
554
- let src = null;
555
- try {
556
- if (j && j.debug && j.debug.source) src = j.debug.source;
557
- else if (j && j.source) src = j.source;
558
- else if (j && j.data && j.data.source) src = j.data.source;
559
- else if (j && j.view && j.view.source) src = j.view.source;
560
- else if (j && j.data) src = j.data;
561
- } catch (e) { /* noop */ }
562
- let type = '';
563
- let config = {};
564
- let id = '';
565
- let name = '';
566
- try { type = (src && (src.type || (src.source && src.source.type))) || (j && j.type) || ''; } catch (e) { }
567
- try { config = (src && (src.config || (src.source && src.source.config))) || (j && j.config) || {}; } catch (e) { }
568
- try { id = String((src && (src.id || src.source_id)) || (j && j.id) || ''); } catch (e) { }
569
- try { name = (src && src.name) || (j && j.name) || ''; } catch (e) { }
570
- return { type: String(type || '').toLowerCase(), config: config || {}, id, name };
571
- }
572
- function buildPrepromptFromInfo(info) {
573
- if (!info || !info.type) return '';
574
- const t = info.type;
575
- const c = info.config || {};
576
- const kv = [];
577
- function pushIf(k, label) { try { if (c && c[k] != null && c[k] !== '') { kv.push(label + ': ' + String(c[k])); } } catch (e) { } }
578
- let header = '';
579
- if (t === 'csv' || t === 'excel' || t === 'json' || t === 'parquet' || t === 'file' || t === 'xls' || t === 'xlsx') {
580
- header = 'La source est de type "' + t + '" (fichier).';
581
- pushIf('path', 'chemin');
582
- } else if (t === 'folder') {
583
- header = 'La source est de type "folder" (dossier).';
584
- pushIf('path', 'chemin');
585
- pushIf('pattern', 'pattern');
586
- } else if (t === 's3') {
587
- header = 'La source est de type "s3".';
588
- pushIf('bucket', 'bucket');
589
- pushIf('prefix', 'prefix');
590
- pushIf('path', 'chemin');
591
- } else if (t === 'gcs') {
592
- header = 'La source est de type "gcs".';
593
- pushIf('bucket', 'bucket');
594
- pushIf('prefix', 'prefix');
595
- pushIf('path', 'chemin');
596
- } else if (t === 'azure_blob') {
597
- header = 'La source est de type "azure_blob".';
598
- pushIf('container', 'container');
599
- pushIf('prefix', 'prefix');
600
- pushIf('path', 'chemin');
601
- } else if (t === 'hdfs') {
602
- header = 'La source est de type "hdfs".';
603
- pushIf('path', 'chemin');
604
- } else {
605
- header = 'La source est de type "' + t + '" (base de données).';
606
- pushIf('host', 'host');
607
- pushIf('port', 'port');
608
- pushIf('database', 'database');
609
- pushIf('schema', 'schema');
610
- // Intentionally avoid sensitive fields (username, password, connection_string)
611
- }
612
- pushIf('table_or_query', 'table_or_query');
613
- const details = kv.length ? (' ' + kv.join(', ') + '.') : '';
614
- return header + details + '\nVeuillez utiliser ces informations pour lire le fichier ou requêter la base de données si nécessaire.';
615
- }
616
- async function buildSourcePreprompt(sourceId) {
617
- try {
618
- if (!sourceId) return '';
619
- const cacheKey = 'studio_source_preprompt_' + sourceId;
620
- try { const cached = sessionStorage.getItem(cacheKey); if (cached) return cached; } catch (e) { }
621
- const j = await fetchSourceDetails(sourceId);
622
- const info = extractSourceInfo(j);
623
- const pre = buildPrepromptFromInfo(info);
624
- // Try include a compact excerpt from preview view
625
- let excerpt = '';
626
- try { if (j && j.view) { excerpt = buildSourceExcerpt(j.view); } } catch (e) { excerpt = ''; }
627
- const domain = buildDomainPreprompt();
628
- const parts = [];
629
- if (domain) parts.push(domain);
630
- if (pre) parts.push(pre);
631
- if (excerpt) parts.push(excerpt);
632
- const full = parts.join('\n\n');
633
- try { sessionStorage.setItem(cacheKey, full); } catch (e) { }
634
- return full;
635
- } catch (e) { return ''; }
636
- }
637
-
638
- async function sendMessage() {
639
- const text = input.value.trim();
640
- if (!text) return;
641
- append('You', text);
642
- input.value = '';
643
- if (isStreaming) return;
644
- isStreaming = true;
645
- if (!currentConvId) { currentConvId = generateConvId(); updateUrlParam('conv_id', currentConvId); ensureAgentTab(currentConvId); }
646
- send.disabled = true; if (stop) stop.style.display = 'inline-block';
647
- const agentDiv = append('Agent', '');
648
- const decoder = new TextDecoder();
649
- streamingController = new AbortController();
650
- let accumulated = '';
651
- function render(text) {
652
- const raw = text || '';
653
- let html = '';
654
- try {
655
- html = (window.marked && window.marked.parse) ? window.marked.parse(raw) : raw.replace(/</g, '&lt;');
656
- } catch (e) {
657
- html = raw.replace(/</g, '&lt;');
658
- }
659
- try {
660
- if (window.DOMPurify && window.DOMPurify.sanitize) {
661
- html = window.DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
662
- }
663
- } catch (e) { /* ignore */ }
664
- agentDiv.innerHTML = html;
665
- log.scrollTop = log.scrollHeight;
666
- }
667
- try {
668
- const issueId = readUrlParam('issue_id');
669
- const sourceId = readUrlParam('source_id');
670
- const domainPre = buildDomainPreprompt();
671
- let preprompt = '';
672
- try { if (sourceId) { preprompt = await buildSourcePreprompt(sourceId); } } catch (e) { preprompt = ''; }
673
- let issueDetails = null;
674
- try { if (issueId) { const s = sessionStorage.getItem('studio_issue_' + issueId); if (s) issueDetails = JSON.parse(s); } } catch (e) { }
675
- const outgoingPrompt = [domainPre, preprompt, text].filter(Boolean).join('\n\n');
676
- const r = await fetch('/studio/chat?stream=1', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: outgoingPrompt, conv_id: currentConvId, issue_id: issueId, source_id: sourceId, issue_details: issueDetails }), signal: streamingController.signal });
677
- if (!r.ok) {
678
- // Non-streaming error response
679
- try {
680
- const j = await r.json();
681
- let show = j.response || (j.error && (typeof j.error === 'string' ? j.error : (j.error.detail || j.error.message || j.error.error))) || j.message || ('HTTP ' + r.status);
682
- render(show);
683
- } catch (_) {
684
- const t = await r.text();
685
- render(t || ('HTTP ' + r.status));
686
- }
687
- } else if (r.body) {
688
- const reader = r.body.getReader();
689
- while (true) {
690
- const { value, done } = await reader.read();
691
- if (done) break;
692
- const chunk = decoder.decode(value, { stream: true });
693
- accumulated += chunk;
694
- render(accumulated);
695
- }
696
- } else {
697
- render('(no response)');
698
- }
699
- } catch (e) {
700
- if (e && e.name === 'AbortError') {
701
- render(accumulated || '(stopped)');
702
- } else {
703
- render('Request failed');
704
- }
705
- } finally {
706
- isStreaming = false;
707
- send.disabled = false; if (stop) stop.style.display = 'none';
708
- try {
709
- const issueId = readUrlParam('issue_id');
710
- if (issueId && currentConvId) {
711
- await fetch('/studio/upload-conversation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conv_id: currentConvId, issue_id: issueId }) });
712
- }
713
- } catch (e) { /* ignore */ }
714
- streamingController = null;
715
- }
716
- }
717
-
718
- async function openHistory() {
719
- if (!historyModal) return;
720
- try {
721
- if (historyStatus) historyStatus.textContent = '';
722
- const r = await fetch('/studio/conversations');
723
- const j = await r.json();
724
- if (!j.ok) throw new Error(j.message || 'Failed to list conversations');
725
- if (historySelect) {
726
- historySelect.innerHTML = '';
727
- (j.items || []).forEach(function (it) {
728
- const opt = document.createElement('option');
729
- opt.value = it.id;
730
- opt.textContent = it.id + ' (' + (it.lines || 0) + ' msgs)';
731
- historySelect.appendChild(opt);
732
- });
733
- }
734
- } catch (e) {
735
- if (historyStatus) historyStatus.textContent = 'Failed to load conversations';
736
- }
737
- historyModal.style.display = 'flex';
738
- }
739
-
740
- async function loadSelectedConversation() {
741
- if (!historySelect || !historySelect.value) return;
742
- const id = historySelect.value;
743
- await loadConversationById(id, true);
744
- }
745
- async function loadConversationById(id, closeModal) {
746
- try {
747
- if (historyStatus) historyStatus.textContent = 'Loading...';
748
- const r = await fetch('/studio/conversation?id=' + encodeURIComponent(id));
749
- const j = await r.json();
750
- if (!j.ok) throw new Error(j.message || 'Failed to load conversation');
751
- log.innerHTML = '';
752
- (j.messages || []).forEach(function (m) {
753
- if (m.role === 'user') append('You', m.text || '');
754
- else append('Agent', m.text || '');
755
- });
756
- currentConvId = j.id || id;
757
- updateUrlParam('conv_id', currentConvId);
758
- ensureAgentTab(currentConvId);
759
- if (historyStatus) historyStatus.textContent = 'Loaded';
760
- if (closeModal && historyModal) historyModal.style.display = 'none';
761
- } catch (e) {
762
- if (historyStatus) historyStatus.textContent = 'Failed to load conversation';
763
- }
764
- }
765
-
766
- btnCheck && btnCheck.addEventListener('click', checkOllama);
767
- btnSave && btnSave.addEventListener('click', saveStudio);
768
- (document.getElementById('btn_save_remote')) && document.getElementById('btn_save_remote').addEventListener('click', saveRemote);
769
- (document.getElementById('btn_check_remote')) && document.getElementById('btn_check_remote').addEventListener('click', checkRemote);
770
- providerSelect && providerSelect.addEventListener('change', function () {
771
- try {
772
- var cloudEnabled = !!(lastStatus && lastStatus.cloud_enabled);
773
- var target = cloudEnabled ? providerSelect.value : 'local';
774
- if (providerSelect.value !== target) providerSelect.value = target;
775
- updateProviderUI(target, lastStatus);
776
- } catch (e) { updateProviderUI('local', lastStatus); }
777
- });
778
- send && send.addEventListener('click', sendMessage);
779
- stop && stop.addEventListener('click', function () { if (streamingController) { try { streamingController.abort(); } catch (e) { } } });
780
- input && input.addEventListener('keydown', function (e) { if (e.key === 'Enter') sendMessage(); });
781
- btnHistory && btnHistory.addEventListener('click', openHistory);
782
- historyClose && historyClose.addEventListener('click', function () { if (historyModal) historyModal.style.display = 'none'; });
783
- historyLoad && historyLoad.addEventListener('click', loadSelectedConversation);
784
- (document.getElementById('btn_new_conv')) && document.getElementById('btn_new_conv').addEventListener('click', function () {
785
- try {
786
- // Clear UI
787
- log.innerHTML = '';
788
- // Generate a new conversation id and update URL
789
- currentConvId = generateConvId();
790
- updateUrlParam('conv_id', currentConvId);
791
- ensureAgentTab(currentConvId);
792
- } catch (e) { /* noop */ }
793
- });
794
- btnMcp && btnMcp.addEventListener('click', openMcpModal);
795
- mcpClose && mcpClose.addEventListener('click', closeMcpModal);
796
- mcpSave && mcpSave.addEventListener('click', saveMcpConfig);
797
- btnClearIssue && btnClearIssue.addEventListener('click', function () {
798
- try {
799
- const params = new URLSearchParams(window.location.search);
800
- params.delete('issue_id');
801
- const newUrl = window.location.pathname + (params.toString() ? ('?' + params.toString()) : '');
802
- window.history.replaceState({}, '', newUrl);
803
- } catch (e) { }
804
- });
805
-
806
- renderAgentTabs();
807
- // Default to local-only providers until status says otherwise
808
- try { setProviderOptions(false); } catch (e) { }
809
- refreshStatus();
810
- checkOllama();
811
- // Load conversation if present in URL
812
- (function () {
813
- const cid = readUrlParam('conv_id');
814
- if (cid) {
815
- currentConvId = cid;
816
- loadConversationById(cid, false);
817
- }
818
- })();
819
- // Keep highlight/state on browser navigation
820
- window.addEventListener('popstate', function () {
821
- const cid = readUrlParam('conv_id');
822
- if (cid && cid !== currentConvId) {
823
- currentConvId = cid;
824
- loadConversationById(cid, false);
825
- }
826
- });
827
- })();
828
- </script>