crewlyze 3.1.0
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.
- package/.dockerignore +12 -0
- package/.gitattributes +2 -0
- package/CHANGELOG.md +86 -0
- package/Dockerfile +21 -0
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/USAGE.md +106 -0
- package/agents/__init__.py +0 -0
- package/agents/cleaner.py +38 -0
- package/agents/insights.py +44 -0
- package/agents/relation.py +36 -0
- package/agents/visualizer.py +41 -0
- package/assets/badge_crewai.svg +4 -0
- package/assets/badge_matplotlib.svg +4 -0
- package/assets/badge_ollama.svg +4 -0
- package/assets/badge_pandas.svg +4 -0
- package/assets/badge_seaborn.svg +4 -0
- package/assets/branding_image.png +0 -0
- package/assets/complete_workflow.svg +216 -0
- package/assets/favicon.png +0 -0
- package/assets/logo.png +0 -0
- package/assets/stars.svg +12 -0
- package/bin/crewlyze.js +79 -0
- package/config/README.md +129 -0
- package/config/__init__.py +1 -0
- package/config/context.py +16 -0
- package/config/llm_config.py +300 -0
- package/config/metrics_tracker.py +70 -0
- package/crew.py +870 -0
- package/crewlyze-3.1.0.tgz +0 -0
- package/fix_syntax.py +54 -0
- package/main.py +1279 -0
- package/package.json +22 -0
- package/pyproject.toml +32 -0
- package/requirements.txt +33 -0
- package/tools/__init__.py +0 -0
- package/tools/dataset_tools.py +803 -0
- package/ui/__init__.py +3 -0
- package/ui/copilot.py +200 -0
- package/ui/export.py +800 -0
- package/update_appjs.py +54 -0
- package/update_llm.py +21 -0
- package/update_main.py +20 -0
- package/web/app.js +3142 -0
- package/web/index.html +1105 -0
- package/web/style.css +2561 -0
- package/workflows/__init__.py +0 -0
- package/workflows/pipeline.py +254 -0
package/web/app.js
ADDED
|
@@ -0,0 +1,3142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crewlyze — Web App JavaScript
|
|
3
|
+
* Connects to the FastAPI backend at the same origin.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Multi-project sidebar with create / delete / switch
|
|
7
|
+
* - LLM provider + model picker with test-connection
|
|
8
|
+
* - Smart task-card selection with dependency enforcement
|
|
9
|
+
* - CSV upload + drag-and-drop
|
|
10
|
+
* - SSE-based live log streaming
|
|
11
|
+
* - Collapsible data preview table
|
|
12
|
+
* - Interactive Plotly chart rendering from JSON
|
|
13
|
+
* - AI Copilot chat with /column slash-command picker
|
|
14
|
+
* - PDF & CSV download
|
|
15
|
+
* - Toast notifications
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_THUMBNAIL_SVG = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMjAiIGhlaWdodD0iMTgwIiB2aWV3Qm94PSIwIDAgMzIwIDE4MCI+PHJlY3Qgd2lkdGg9IjMyMCIgaGVpZ2h0PSIxODAiIGZpbGw9IiMxODE4MWIiLz48Y2lyY2xlIGN4PSIxNjAiIGN5PSI5MCIgcj0iNDAiIGZpbGw9IiM3YzNhZWQiIGZpbGwtb3BhY2l0eT0iMC4xIi8+PHBhdGggZD0iTTE0MCAxMDAgTDE2MCA4MCBMMTgwIDEwMCIgc3Ryb2tlPSIjN2MzYWVkIiBzdHJva2Utd2lkdGg9IjMiIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xMzAgMTE1IEwxOTAgMTE1IiBzdHJva2U9IiMyMmQzZWUiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHRleHQgeD0iMTYwIiB5PSIxNTAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjYTFhMWFhIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5BR0VOVElDIExPR1M8L3RleHQ+PC9zdmc+';
|
|
21
|
+
|
|
22
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Model catalogue (mirrors the sidebar in the old Streamlit app)
|
|
24
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
const MODEL_OPTIONS = {
|
|
26
|
+
nvidia: [
|
|
27
|
+
'nvidia_nim/meta/llama-3.1-8b-instruct',
|
|
28
|
+
'nvidia_nim/meta/llama-3.1-70b-instruct',
|
|
29
|
+
'nvidia_nim/nvidia/mistral-nemo-minitron-8b-8k-instruct',
|
|
30
|
+
'nvidia_nim/mistralai/mistral-large-2407',
|
|
31
|
+
],
|
|
32
|
+
minimax: ['minimaxai/minimax-m3'],
|
|
33
|
+
groq: ['groq/llama-3.1-8b-instant','groq/llama-3.3-70b-versatile','groq/mixtral-8x7b-32768','groq/gemma2-9b-it'],
|
|
34
|
+
openai: ['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo'],
|
|
35
|
+
anthropic: ['claude-3-5-sonnet-20241022','claude-3-opus-20240229','claude-3-sonnet-20240229','claude-3-haiku-20240307'],
|
|
36
|
+
gemini: ['gemini/gemini-pro','gemini/gemini-1.5-pro','gemini/gemini-1.5-flash'],
|
|
37
|
+
mistral: ['mistral/mistral-tiny','mistral/mistral-small','mistral/mistral-medium','mistral/mistral-large-latest'],
|
|
38
|
+
huggingface: ['huggingface/HuggingFaceH4/zephyr-7b-beta','huggingface/meta-llama/Llama-2-7b-chat-hf'],
|
|
39
|
+
ollama: ['ollama/llama3','ollama/mistral','ollama/gemma2'],
|
|
40
|
+
custom: ['custom/model'],
|
|
41
|
+
custom: ['gpt-3.5-turbo', 'gpt-4'],
|
|
42
|
+
cohere: ['cohere/command-r-plus', 'cohere/command-r', 'cohere/command-light'],
|
|
43
|
+
together: ['together_ai/meta-llama/Llama-3-70b-chat-hf', 'together_ai/meta-llama/Llama-3-8b-chat-hf', 'together_ai/mistralai/Mixtral-8x7B-Instruct-v0.1'],
|
|
44
|
+
openrouter: ['openrouter/google/gemma-2-9b-it', 'openrouter/meta-llama/llama-3-8b-instruct', 'openrouter/anthropic/claude-3.5-sonnet'],
|
|
45
|
+
deepseek: ['deepseek/deepseek-chat', 'deepseek/deepseek-coder'],
|
|
46
|
+
perplexity: ['perplexity/llama-3-sonar-large-32k-chat', 'perplexity/llama-3-sonar-small-32k-chat'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// App state
|
|
51
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
const state = {
|
|
53
|
+
projects: [], // array of project metadata from API
|
|
54
|
+
activeProject: null, // currently viewed project
|
|
55
|
+
uploadedFile: null, // File object pending analysis
|
|
56
|
+
uploadedSession: null, // session_id returned after upload
|
|
57
|
+
results: null, // latest results JSON
|
|
58
|
+
columns: [], // column names from last preview
|
|
59
|
+
colTypes: {}, // column → dtype string
|
|
60
|
+
chatHistory: [], // [{role, content, plot_url}]
|
|
61
|
+
previewMinimized: false,
|
|
62
|
+
sseSource: null, // current EventSource
|
|
63
|
+
resultsCache: {}, // session_id -> results JSON cache
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// DOM refs
|
|
68
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
const $ = id => document.getElementById(id);
|
|
70
|
+
const $$ = sel => document.querySelectorAll(sel);
|
|
71
|
+
|
|
72
|
+
const els = {
|
|
73
|
+
sidebar: $('sidebar'),
|
|
74
|
+
sidebarToggle: $('sidebarToggle'),
|
|
75
|
+
mobileSidebarBtn: $('mobileSidebarBtn'),
|
|
76
|
+
projectsList: $('projectsList'),
|
|
77
|
+
newProjectBtn: $('newProjectBtn'),
|
|
78
|
+
sidebarLogo: $('sidebarLogo'),
|
|
79
|
+
newProjectWizard: $('newProjectWizard'),
|
|
80
|
+
wizardStep0: $('wizardStep0'),
|
|
81
|
+
startWizardCard: $('startWizardCard'),
|
|
82
|
+
wizardStep1: $('wizardStep1'),
|
|
83
|
+
wizardBack1Btn: $('wizardBack1Btn'),
|
|
84
|
+
wizardStep2: $('wizardStep2'),
|
|
85
|
+
wizardProjectName: $('wizardProjectName'),
|
|
86
|
+
wizardNextBtn: $('wizardNextBtn'),
|
|
87
|
+
wizardBackBtn: $('wizardBackBtn'),
|
|
88
|
+
wizardUploadTitle: $('wizardUploadTitle'),
|
|
89
|
+
dashboardProjectsGrid: $('dashboardProjectsGrid'),
|
|
90
|
+
dashboardProjectsCount: $('dashboardProjectsCount'),
|
|
91
|
+
|
|
92
|
+
// Settings Modal elements
|
|
93
|
+
sidebarSettingsBtn: $('sidebarSettingsBtn'),
|
|
94
|
+
settingsModal: $('settingsModal'),
|
|
95
|
+
closeSettingsModal: $('closeSettingsModal'),
|
|
96
|
+
cancelSettingsBtn: $('cancelSettingsBtn'),
|
|
97
|
+
saveSettingsBtn: $('saveSettingsBtn'),
|
|
98
|
+
settingsCooldown: $('settingsCooldown'),
|
|
99
|
+
settingsCooldownVal: $('settingsCooldownVal'),
|
|
100
|
+
settingsTestConnectionBtn: $('settingsTestConnectionBtn'),
|
|
101
|
+
settingsConnectionStatus: $('settingsConnectionStatus'),
|
|
102
|
+
keyNvidia: $('keyNvidia'),
|
|
103
|
+
keyGroq: $('keyGroq'),
|
|
104
|
+
keyOpenai: $('keyOpenai'),
|
|
105
|
+
keyAnthropic: $('keyAnthropic'),
|
|
106
|
+
keyGemini: $('keyGemini'),
|
|
107
|
+
keyMistral: $('keyMistral'),
|
|
108
|
+
keyHuggingface: $('keyHuggingface'),
|
|
109
|
+
urlOllama: $('urlOllama'),
|
|
110
|
+
urlCustom: $('urlCustom'),
|
|
111
|
+
keyCustom: $('keyCustom'),
|
|
112
|
+
showCustom: $('showCustom'),
|
|
113
|
+
keyCohere: $('keyCohere'),
|
|
114
|
+
keyTogether: $('keyTogether'),
|
|
115
|
+
keyOpenrouter: $('keyOpenrouter'),
|
|
116
|
+
keyDeepseek: $('keyDeepseek'),
|
|
117
|
+
keyPerplexity: $('keyPerplexity'),
|
|
118
|
+
|
|
119
|
+
// Checkboxes
|
|
120
|
+
showNvidia: $('showNvidia'),
|
|
121
|
+
showGroq: $('showGroq'),
|
|
122
|
+
showOpenai: $('showOpenai'),
|
|
123
|
+
showAnthropic: $('showAnthropic'),
|
|
124
|
+
showGemini: $('showGemini'),
|
|
125
|
+
showMistral: $('showMistral'),
|
|
126
|
+
showHuggingface: $('showHuggingface'),
|
|
127
|
+
showOllama: $('showOllama'),
|
|
128
|
+
showCohere: $('showCohere'),
|
|
129
|
+
showTogether: $('showTogether'),
|
|
130
|
+
showOpenrouter: $('showOpenrouter'),
|
|
131
|
+
showDeepseek: $('showDeepseek'),
|
|
132
|
+
showPerplexity: $('showPerplexity'),
|
|
133
|
+
|
|
134
|
+
llmProvider: $('llmProvider'),
|
|
135
|
+
llmModel: $('llmModel'),
|
|
136
|
+
apiKey: $('apiKey'),
|
|
137
|
+
toggleKey: $('toggleKey'),
|
|
138
|
+
keyLabel: $('keyLabel'),
|
|
139
|
+
cooldown: $('cooldown'),
|
|
140
|
+
cooldownVal: $('cooldownVal'),
|
|
141
|
+
testConnectionBtn: $('testConnectionBtn'),
|
|
142
|
+
connectionStatus: $('connectionStatus'),
|
|
143
|
+
|
|
144
|
+
statusPill: $('statusPill'),
|
|
145
|
+
breadcrumb: $('breadcrumb'),
|
|
146
|
+
|
|
147
|
+
// Screens
|
|
148
|
+
landingScreen: $('landingScreen'),
|
|
149
|
+
runningScreen: $('runningScreen'),
|
|
150
|
+
resultsScreen: $('resultsScreen'),
|
|
151
|
+
|
|
152
|
+
// Upload
|
|
153
|
+
uploadZone: $('uploadZone'),
|
|
154
|
+
fileInput: $('fileInput'),
|
|
155
|
+
uploadedFileMeta: $('uploadedFileMeta'),
|
|
156
|
+
uploadedFileActions: $('uploadedFileActions'),
|
|
157
|
+
startAnalysisBtn: $('startAnalysisBtn'),
|
|
158
|
+
|
|
159
|
+
// Config modal
|
|
160
|
+
configModal: $('configModal'),
|
|
161
|
+
closeConfigModal: $('closeConfigModal'),
|
|
162
|
+
cancelConfigBtn: $('cancelConfigBtn'),
|
|
163
|
+
runAnalysisBtn: $('runAnalysisBtn'),
|
|
164
|
+
reportTitle: $('reportTitle'),
|
|
165
|
+
|
|
166
|
+
// Task cards
|
|
167
|
+
taskCleaning: $('taskCleaning'),
|
|
168
|
+
taskRelations: $('taskRelations'),
|
|
169
|
+
taskInsights: $('taskInsights'),
|
|
170
|
+
taskViz: $('taskViz'),
|
|
171
|
+
taskCardCleaning: $('taskCardCleaning'),
|
|
172
|
+
taskCardRelations: $('taskCardRelations'),
|
|
173
|
+
taskCardInsights: $('taskCardInsights'),
|
|
174
|
+
taskCardViz: $('taskCardViz'),
|
|
175
|
+
|
|
176
|
+
// Running
|
|
177
|
+
runningTitle: $('runningTitle'),
|
|
178
|
+
logOutput: $('logOutput'),
|
|
179
|
+
clearLogBtn: $('clearLogBtn'),
|
|
180
|
+
|
|
181
|
+
// Results
|
|
182
|
+
statsRow: $('statsRow'),
|
|
183
|
+
tabBtns: $$('.tab-btn'),
|
|
184
|
+
tabPanels: $$('.tab-panel'),
|
|
185
|
+
|
|
186
|
+
// Preview
|
|
187
|
+
previewTable: $('previewTable'),
|
|
188
|
+
togglePreviewBtn: $('togglePreviewBtn'),
|
|
189
|
+
previewTableWrap: $('previewTableWrap'),
|
|
190
|
+
|
|
191
|
+
// Panels
|
|
192
|
+
cleaningContent: $('cleaningContent'),
|
|
193
|
+
relationsContent: $('relationsContent'),
|
|
194
|
+
insightsContent: $('insightsContent'),
|
|
195
|
+
plotlyChartsWrap: $('plotlyChartsWrap'),
|
|
196
|
+
pngChartsWrap: $('pngChartsWrap'),
|
|
197
|
+
pngCharts: $('pngCharts'),
|
|
198
|
+
vizCodeBlock: $('vizCodeBlock'),
|
|
199
|
+
vizCodeDetails: $('vizCodeDetails'),
|
|
200
|
+
|
|
201
|
+
// Chat
|
|
202
|
+
chatMessages: $('chatMessages'),
|
|
203
|
+
chatBackBtn: $('chatBackBtn'),
|
|
204
|
+
chatInput: $('chatInput'),
|
|
205
|
+
sendChatBtn: $('sendChatBtn'),
|
|
206
|
+
clearChatBtn: $('clearChatBtn'),
|
|
207
|
+
colPickerDropdown: $('colPickerDropdown'),
|
|
208
|
+
|
|
209
|
+
// Export
|
|
210
|
+
exportPdfBtn: $('exportPdfBtn'),
|
|
211
|
+
downloadCsvBtn: $('downloadCsvBtn'),
|
|
212
|
+
reRunBtn: $('reRunBtn'),
|
|
213
|
+
|
|
214
|
+
// Sidebar Export
|
|
215
|
+
sidebarProjectActions: $('sidebarProjectActions'),
|
|
216
|
+
sidebarExportPdfBtn: $('sidebarExportPdfBtn'),
|
|
217
|
+
sidebarExportZipBtn: $('sidebarExportZipBtn'),
|
|
218
|
+
sidebarDownloadCsvBtn: $('sidebarDownloadCsvBtn'),
|
|
219
|
+
sidebarReRunBtn: $('sidebarReRunBtn'),
|
|
220
|
+
|
|
221
|
+
toastContainer: $('toastContainer'),
|
|
222
|
+
|
|
223
|
+
// API Warning Modal
|
|
224
|
+
apiWarningModal: $('apiWarningModal'),
|
|
225
|
+
warningProviderName: $('warningProviderName'),
|
|
226
|
+
guideToApiBtn: $('guideToApiBtn'),
|
|
227
|
+
closeApiWarningBtn: $('closeApiWarningBtn'),
|
|
228
|
+
|
|
229
|
+
// Custom dialog modal elements
|
|
230
|
+
customDialogModal: $('customDialogModal'),
|
|
231
|
+
customDialogTitle: $('customDialogTitle'),
|
|
232
|
+
customDialogMessage: $('customDialogMessage'),
|
|
233
|
+
customDialogPromptContainer: $('customDialogPromptContainer'),
|
|
234
|
+
customDialogInput: $('customDialogInput'),
|
|
235
|
+
customDialogCancelBtn: $('customDialogCancelBtn'),
|
|
236
|
+
customDialogConfirmBtn: $('customDialogConfirmBtn'),
|
|
237
|
+
closeCustomDialogBtn: $('closeCustomDialogBtn'),
|
|
238
|
+
stagePovPanel: $('stagePovPanel'),
|
|
239
|
+
wizardReportTitle: $('wizardReportTitle'),
|
|
240
|
+
wizardProjectGoal: $('wizardProjectGoal'),
|
|
241
|
+
importProjectCard: $('importProjectCard'),
|
|
242
|
+
importZipFileInput: $('importZipFileInput'),
|
|
243
|
+
exportZipBtn: $('exportZipBtn'),
|
|
244
|
+
btnSectionChat: $('btnSectionChat'),
|
|
245
|
+
btnSectionAgentic: $('btnSectionAgentic'),
|
|
246
|
+
areaChat: $('areaChat'),
|
|
247
|
+
areaAgentic: $('areaAgentic'),
|
|
248
|
+
areaHub: $('areaHub'),
|
|
249
|
+
btnEnterChat: $('btnEnterChat'),
|
|
250
|
+
btnEnterAgentic: $('btnEnterAgentic'),
|
|
251
|
+
btnRenameColQuick: $('btnRenameColQuick'),
|
|
252
|
+
btnDeleteColQuick: $('btnDeleteColQuick'),
|
|
253
|
+
chatPreviewDims: $('chatPreviewDims'),
|
|
254
|
+
relationModal: $('relationModal'),
|
|
255
|
+
relationModalXSelect: $('relationModalXSelect'),
|
|
256
|
+
relationModalYSelect: $('relationModalYSelect'),
|
|
257
|
+
relationModalTypeSelect: $('relationModalTypeSelect'),
|
|
258
|
+
relationModalDetails: $('relationModalDetails'),
|
|
259
|
+
relationModalConfirmBtn: $('relationModalConfirmBtn'),
|
|
260
|
+
relationModalCancelBtn: $('relationModalCancelBtn'),
|
|
261
|
+
closeRelationModalBtn: $('closeRelationModalBtn'),
|
|
262
|
+
agenticPlaceholder: $('agenticPlaceholder'),
|
|
263
|
+
agenticTabsBar: $('agenticTabsBar'),
|
|
264
|
+
agenticTabPanels: $('agenticTabPanels'),
|
|
265
|
+
btnRunAgenticPipeline: $('btnRunAgenticPipeline'),
|
|
266
|
+
sidebarMetricsBtn: $('sidebarMetricsBtn'),
|
|
267
|
+
metricsScreen: $('metricsScreen'),
|
|
268
|
+
backToDashboardBtn: $('backToDashboardBtn'),
|
|
269
|
+
metricsTableBody: $('metricsTableBody'),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
273
|
+
// Screen management
|
|
274
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
275
|
+
function showScreen(name) {
|
|
276
|
+
['landingScreen','runningScreen','resultsScreen','metricsScreen'].forEach(id => {
|
|
277
|
+
const el = $(id);
|
|
278
|
+
if (el) el.classList.toggle('active', id === name + 'Screen');
|
|
279
|
+
});
|
|
280
|
+
if (name !== 'results' && name !== 'metrics') {
|
|
281
|
+
if (els.sidebarProjectActions) els.sidebarProjectActions.classList.add('hidden');
|
|
282
|
+
} else if (name === 'results') {
|
|
283
|
+
updateSidebarProjectActionsVisibility();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
288
|
+
// Status pill
|
|
289
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
290
|
+
function setStatus(label, cls) {
|
|
291
|
+
els.statusPill.textContent = label;
|
|
292
|
+
els.statusPill.className = `status-pill ${cls}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
296
|
+
// Toast notifications
|
|
297
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
298
|
+
function toast(msg, type = 'info', durationMs = 3500) {
|
|
299
|
+
const t = document.createElement('div');
|
|
300
|
+
t.className = `toast ${type}`;
|
|
301
|
+
t.textContent = msg;
|
|
302
|
+
els.toastContainer.appendChild(t);
|
|
303
|
+
setTimeout(() => t.remove(), durationMs);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
307
|
+
// Custom Dialog (Alert / Confirm / Prompt)
|
|
308
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
309
|
+
function showCustomDialog({ title, message, type = 'confirm', placeholder = '', defaultValue = '' }) {
|
|
310
|
+
return new Promise((resolve) => {
|
|
311
|
+
const modal = els.customDialogModal;
|
|
312
|
+
const titleEl = els.customDialogTitle;
|
|
313
|
+
const messageEl = els.customDialogMessage;
|
|
314
|
+
const promptContainer = els.customDialogPromptContainer;
|
|
315
|
+
const inputEl = els.customDialogInput;
|
|
316
|
+
const cancelBtn = els.customDialogCancelBtn;
|
|
317
|
+
const confirmBtn = els.customDialogConfirmBtn;
|
|
318
|
+
const closeBtn = els.closeCustomDialogBtn;
|
|
319
|
+
|
|
320
|
+
titleEl.textContent = title || (type === 'confirm' ? 'Confirm' : type === 'prompt' ? 'Input Required' : 'Alert');
|
|
321
|
+
messageEl.textContent = message || '';
|
|
322
|
+
|
|
323
|
+
if (type === 'prompt') {
|
|
324
|
+
promptContainer.classList.remove('hidden');
|
|
325
|
+
inputEl.value = defaultValue;
|
|
326
|
+
inputEl.placeholder = placeholder;
|
|
327
|
+
} else {
|
|
328
|
+
promptContainer.classList.add('hidden');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (type === 'alert') {
|
|
332
|
+
cancelBtn.classList.add('hidden');
|
|
333
|
+
confirmBtn.textContent = 'OK';
|
|
334
|
+
} else {
|
|
335
|
+
cancelBtn.classList.remove('hidden');
|
|
336
|
+
confirmBtn.textContent = type === 'confirm' ? 'Confirm' : 'Submit';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
modal.classList.remove('hidden');
|
|
340
|
+
if (type === 'prompt') {
|
|
341
|
+
setTimeout(() => inputEl.focus(), 50);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function cleanup() {
|
|
345
|
+
modal.classList.add('hidden');
|
|
346
|
+
confirmBtn.removeEventListener('click', onConfirm);
|
|
347
|
+
cancelBtn.removeEventListener('click', onCancel);
|
|
348
|
+
closeBtn.removeEventListener('click', onCancel);
|
|
349
|
+
inputEl.removeEventListener('keydown', onKeyDown);
|
|
350
|
+
modal.removeEventListener('click', onBackdropClick);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function onConfirm() {
|
|
354
|
+
const val = type === 'prompt' ? inputEl.value : true;
|
|
355
|
+
cleanup();
|
|
356
|
+
resolve(val);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function onCancel() {
|
|
360
|
+
cleanup();
|
|
361
|
+
resolve(type === 'prompt' ? null : false);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function onKeyDown(e) {
|
|
365
|
+
if (e.key === 'Enter') {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
onConfirm();
|
|
368
|
+
} else if (e.key === 'Escape') {
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
onCancel();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function onBackdropClick(e) {
|
|
375
|
+
if (e.target === modal) {
|
|
376
|
+
onCancel();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
confirmBtn.addEventListener('click', onConfirm);
|
|
381
|
+
cancelBtn.addEventListener('click', onCancel);
|
|
382
|
+
closeBtn.addEventListener('click', onCancel);
|
|
383
|
+
inputEl.addEventListener('keydown', onKeyDown);
|
|
384
|
+
modal.addEventListener('click', onBackdropClick);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function customConfirm(message, title = 'Confirm') {
|
|
389
|
+
return showCustomDialog({ title, message, type: 'confirm' });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function customPrompt(message, defaultValue = '', placeholder = '', title = 'Input Required') {
|
|
393
|
+
return showCustomDialog({ title, message, type: 'prompt', defaultValue, placeholder });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function customAlert(message, title = 'Alert') {
|
|
397
|
+
return showCustomDialog({ title, message, type: 'alert' });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
401
|
+
// Fetch Ollama Models
|
|
402
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
403
|
+
async function fetchOllamaModels() {
|
|
404
|
+
try {
|
|
405
|
+
const customUrl = localStorage.getItem('api_url_ollama') || 'http://localhost:11434';
|
|
406
|
+
const res = await fetch(`/api/ollama-models?base_url=${encodeURIComponent(customUrl)}`);
|
|
407
|
+
if (res.ok) {
|
|
408
|
+
const data = await res.json();
|
|
409
|
+
if (data && data.models && data.models.length > 0) {
|
|
410
|
+
// Prepend 'ollama/' prefix to models if not already present
|
|
411
|
+
const processedModels = data.models.map(m => {
|
|
412
|
+
if (!m.startsWith('ollama/')) {
|
|
413
|
+
return `ollama/${m}`;
|
|
414
|
+
}
|
|
415
|
+
return m;
|
|
416
|
+
});
|
|
417
|
+
MODEL_OPTIONS.ollama = processedModels;
|
|
418
|
+
return processedModels;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch (e) {
|
|
422
|
+
console.error('Failed to fetch Ollama models:', e);
|
|
423
|
+
}
|
|
424
|
+
return MODEL_OPTIONS.ollama; // fallback
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
428
|
+
// LLM settings persistence & Settings Modal logic
|
|
429
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
430
|
+
const ALL_PROVIDERS = [
|
|
431
|
+
{ id: 'nvidia', name: 'NVIDIA NIM' },
|
|
432
|
+
{ id: 'groq', name: 'Groq' },
|
|
433
|
+
{ id: 'openai', name: 'OpenAI' },
|
|
434
|
+
{ id: 'anthropic', name: 'Anthropic' },
|
|
435
|
+
{ id: 'gemini', name: 'Google Gemini' },
|
|
436
|
+
{ id: 'mistral', name: 'Mistral' },
|
|
437
|
+
{ id: 'huggingface', name: 'HuggingFace' },
|
|
438
|
+
{ id: 'cohere', name: 'Cohere' },
|
|
439
|
+
{ id: 'together', name: 'TogetherAI' },
|
|
440
|
+
{ id: 'openrouter', name: 'OpenRouter' },
|
|
441
|
+
{ id: 'deepseek', name: 'DeepSeek' },
|
|
442
|
+
{ id: 'perplexity', name: 'Perplexity' },
|
|
443
|
+
{ id: 'ollama', name: 'Ollama (local)' },
|
|
444
|
+
{ id: 'custom', name: 'Custom API' }
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
const PROVIDER_TEST_MODELS = {
|
|
448
|
+
nvidia: 'nvidia_nim/meta/llama-3.1-8b-instruct',
|
|
449
|
+
groq: 'groq/llama-3.1-8b-instant',
|
|
450
|
+
openai: 'gpt-4o-mini',
|
|
451
|
+
anthropic: 'claude-3-5-sonnet-20241022',
|
|
452
|
+
gemini: 'gemini/gemini-pro',
|
|
453
|
+
mistral: 'mistral/mistral-tiny',
|
|
454
|
+
huggingface: 'huggingface/HuggingFaceH4/zephyr-7b-beta',
|
|
455
|
+
ollama: 'ollama/llama3',
|
|
456
|
+
cohere: 'cohere/command-r-plus',
|
|
457
|
+
together: 'together_ai/meta-llama/Llama-3-70b-chat-hf',
|
|
458
|
+
openrouter: 'openrouter/google/gemma-2-9b-it',
|
|
459
|
+
deepseek: 'deepseek/deepseek-chat',
|
|
460
|
+
perplexity: 'perplexity/llama-3-sonar-large-32k-chat'
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
function getSavedKey(provider) {
|
|
464
|
+
if (provider === 'ollama') {
|
|
465
|
+
return localStorage.getItem('api_url_ollama') || 'http://localhost:11434';
|
|
466
|
+
}
|
|
467
|
+
return localStorage.getItem(`api_key_${provider}`) || '';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function syncActiveApiKey() {
|
|
471
|
+
const provider = els.llmProvider.value;
|
|
472
|
+
const key = getSavedKey(provider);
|
|
473
|
+
els.apiKey.value = key;
|
|
474
|
+
|
|
475
|
+
const statusEl = document.getElementById('sidebarApiKeyStatus');
|
|
476
|
+
if (statusEl) {
|
|
477
|
+
if (provider === 'ollama') {
|
|
478
|
+
const url = localStorage.getItem('api_url_ollama') || 'http://localhost:11434';
|
|
479
|
+
statusEl.innerHTML = `<i data-lucide="link" style="width: 14px; height: 14px; color: var(--cyan);"></i> <span style="color: var(--cyan); text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 160px;">${url}</span>`;
|
|
480
|
+
} else if (key) {
|
|
481
|
+
statusEl.innerHTML = `<i data-lucide="check-circle" style="width: 14px; height: 14px; color: var(--emerald);"></i> <span style="color: var(--emerald);">API Key Set</span>`;
|
|
482
|
+
} else {
|
|
483
|
+
statusEl.innerHTML = `<i data-lucide="alert-circle" style="width: 14px; height: 14px; color: var(--amber);"></i> <span style="color: var(--amber);">API Key Not Set</span>`;
|
|
484
|
+
}
|
|
485
|
+
if (window.lucide) {
|
|
486
|
+
lucide.createIcons({ attrs: { class: 'icon-svg' } });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function saveLlmSettings() {
|
|
492
|
+
localStorage.setItem('llm_provider', els.llmProvider.value);
|
|
493
|
+
localStorage.setItem('llm_model', els.llmModel.value);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function populateProvidersDropdown() {
|
|
497
|
+
const currentVal = els.llmProvider.value;
|
|
498
|
+
els.llmProvider.innerHTML = '';
|
|
499
|
+
let selectedExists = false;
|
|
500
|
+
|
|
501
|
+
ALL_PROVIDERS.forEach(p => {
|
|
502
|
+
const isShown = localStorage.getItem(`show_provider_${p.id}`) !== 'false'; // defaults to true
|
|
503
|
+
if (isShown) {
|
|
504
|
+
const opt = document.createElement('option');
|
|
505
|
+
opt.value = p.id;
|
|
506
|
+
opt.textContent = p.name;
|
|
507
|
+
els.llmProvider.appendChild(opt);
|
|
508
|
+
if (p.id === currentVal) selectedExists = true;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (selectedExists) {
|
|
513
|
+
els.llmProvider.value = currentVal;
|
|
514
|
+
} else if (els.llmProvider.options.length > 0) {
|
|
515
|
+
els.llmProvider.selectedIndex = 0;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function loadLlmSettings() {
|
|
520
|
+
try {
|
|
521
|
+
const res = await fetch('/api/config');
|
|
522
|
+
if (res.ok) {
|
|
523
|
+
const backendCfg = await res.json();
|
|
524
|
+
if (backendCfg.NVIDIA_API_KEY) localStorage.setItem('api_key_nvidia', backendCfg.NVIDIA_API_KEY);
|
|
525
|
+
if (backendCfg.GROQ_API_KEY) localStorage.setItem('api_key_groq', backendCfg.GROQ_API_KEY);
|
|
526
|
+
if (backendCfg.OPENAI_API_KEY) localStorage.setItem('api_key_openai', backendCfg.OPENAI_API_KEY);
|
|
527
|
+
if (backendCfg.ANTHROPIC_API_KEY) localStorage.setItem('api_key_anthropic', backendCfg.ANTHROPIC_API_KEY);
|
|
528
|
+
if (backendCfg.GEMINI_API_KEY) localStorage.setItem('api_key_gemini', backendCfg.GEMINI_API_KEY);
|
|
529
|
+
if (backendCfg.MISTRAL_API_KEY) localStorage.setItem('api_key_mistral', backendCfg.MISTRAL_API_KEY);
|
|
530
|
+
if (backendCfg.HUGGINGFACE_API_KEY) localStorage.setItem('api_key_huggingface', backendCfg.HUGGINGFACE_API_KEY);
|
|
531
|
+
if (backendCfg.COHERE_API_KEY) localStorage.setItem('api_key_cohere', backendCfg.COHERE_API_KEY);
|
|
532
|
+
if (backendCfg.TOGETHER_API_KEY) localStorage.setItem('api_key_together', backendCfg.TOGETHER_API_KEY);
|
|
533
|
+
if (backendCfg.OPENROUTER_API_KEY) localStorage.setItem('api_key_openrouter', backendCfg.OPENROUTER_API_KEY);
|
|
534
|
+
if (backendCfg.DEEPSEEK_API_KEY) localStorage.setItem('api_key_deepseek', backendCfg.DEEPSEEK_API_KEY);
|
|
535
|
+
if (backendCfg.PERPLEXITY_API_KEY) localStorage.setItem('api_key_perplexity', backendCfg.PERPLEXITY_API_KEY);
|
|
536
|
+
if (backendCfg.OLLAMA_BASE_URL) localStorage.setItem('api_url_ollama', backendCfg.OLLAMA_BASE_URL);
|
|
537
|
+
if (backendCfg.CUSTOM_BASE_URL) localStorage.setItem('api_url_custom', backendCfg.CUSTOM_BASE_URL);
|
|
538
|
+
if (backendCfg.CUSTOM_API_KEY) localStorage.setItem('api_key_custom', backendCfg.CUSTOM_API_KEY);
|
|
539
|
+
}
|
|
540
|
+
} catch (err) {
|
|
541
|
+
console.warn('Failed to load credentials from backend config:', err);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const savedProvider = localStorage.getItem('llm_provider');
|
|
545
|
+
const savedCooldown = localStorage.getItem('llm_cooldown') || '5';
|
|
546
|
+
|
|
547
|
+
populateProvidersDropdown();
|
|
548
|
+
|
|
549
|
+
if (savedProvider && Array.from(els.llmProvider.options).some(opt => opt.value === savedProvider)) {
|
|
550
|
+
els.llmProvider.value = savedProvider;
|
|
551
|
+
} else if (els.llmProvider.options.length > 0) {
|
|
552
|
+
els.llmProvider.selectedIndex = 0;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
await populateModels(els.llmProvider.value);
|
|
556
|
+
|
|
557
|
+
els.cooldown.value = savedCooldown;
|
|
558
|
+
els.cooldownVal.textContent = savedCooldown;
|
|
559
|
+
|
|
560
|
+
syncActiveApiKey();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Populate Settings Modal text boxes
|
|
564
|
+
function populateSettingsModal() {
|
|
565
|
+
// Checkbox show states
|
|
566
|
+
ALL_PROVIDERS.forEach(p => {
|
|
567
|
+
const showCheck = els[`show${p.id.charAt(0).toUpperCase() + p.id.slice(1)}`];
|
|
568
|
+
if (showCheck) {
|
|
569
|
+
showCheck.checked = localStorage.getItem(`show_provider_${p.id}`) !== 'false';
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Keys
|
|
574
|
+
els.keyNvidia.value = localStorage.getItem('api_key_nvidia') || '';
|
|
575
|
+
els.keyGroq.value = localStorage.getItem('api_key_groq') || '';
|
|
576
|
+
els.keyOpenai.value = localStorage.getItem('api_key_openai') || '';
|
|
577
|
+
els.keyAnthropic.value = localStorage.getItem('api_key_anthropic') || '';
|
|
578
|
+
els.keyGemini.value = localStorage.getItem('api_key_gemini') || '';
|
|
579
|
+
els.keyMistral.value = localStorage.getItem('api_key_mistral') || '';
|
|
580
|
+
els.keyHuggingface.value = localStorage.getItem('api_key_huggingface') || '';
|
|
581
|
+
els.keyCohere.value = localStorage.getItem('api_key_cohere') || '';
|
|
582
|
+
els.keyTogether.value = localStorage.getItem('api_key_together') || '';
|
|
583
|
+
els.keyOpenrouter.value = localStorage.getItem('api_key_openrouter') || '';
|
|
584
|
+
els.keyDeepseek.value = localStorage.getItem('api_key_deepseek') || '';
|
|
585
|
+
els.keyPerplexity.value = localStorage.getItem('api_key_perplexity') || '';
|
|
586
|
+
els.urlOllama.value = localStorage.getItem('api_url_ollama') || 'http://localhost:11434';
|
|
587
|
+
els.urlCustom.value = localStorage.getItem('api_url_custom') || 'https://api.openai.com/v1';
|
|
588
|
+
els.keyCustom.value = localStorage.getItem('api_key_custom') || '';
|
|
589
|
+
// 'http://localhost:11434';
|
|
590
|
+
|
|
591
|
+
const cooldown = localStorage.getItem('llm_cooldown') || '5';
|
|
592
|
+
els.settingsCooldown.value = cooldown;
|
|
593
|
+
els.settingsCooldownVal.textContent = cooldown;
|
|
594
|
+
|
|
595
|
+
// Clear all individual statuses
|
|
596
|
+
document.querySelectorAll('.individual-status').forEach(el => {
|
|
597
|
+
el.textContent = '';
|
|
598
|
+
el.className = 'individual-status';
|
|
599
|
+
el.style.color = '';
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Save Settings Modal inputs
|
|
604
|
+
function saveSettingsModal() {
|
|
605
|
+
// Checkbox show states
|
|
606
|
+
ALL_PROVIDERS.forEach(p => {
|
|
607
|
+
const showCheck = els[`show${p.id.charAt(0).toUpperCase() + p.id.slice(1)}`];
|
|
608
|
+
if (showCheck) {
|
|
609
|
+
localStorage.setItem(`show_provider_${p.id}`, showCheck.checked ? 'true' : 'false');
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Keys
|
|
614
|
+
localStorage.setItem('api_key_nvidia', els.keyNvidia.value.trim());
|
|
615
|
+
localStorage.setItem('api_key_groq', els.keyGroq.value.trim());
|
|
616
|
+
localStorage.setItem('api_key_openai', els.keyOpenai.value.trim());
|
|
617
|
+
localStorage.setItem('api_key_anthropic', els.keyAnthropic.value.trim());
|
|
618
|
+
localStorage.setItem('api_key_gemini', els.keyGemini.value.trim());
|
|
619
|
+
localStorage.setItem('api_key_mistral', els.keyMistral.value.trim());
|
|
620
|
+
localStorage.setItem('api_key_huggingface', els.keyHuggingface.value.trim());
|
|
621
|
+
localStorage.setItem('api_key_cohere', els.keyCohere.value.trim());
|
|
622
|
+
localStorage.setItem('api_key_together', els.keyTogether.value.trim());
|
|
623
|
+
localStorage.setItem('api_key_openrouter', els.keyOpenrouter.value.trim());
|
|
624
|
+
localStorage.setItem('api_key_deepseek', els.keyDeepseek.value.trim());
|
|
625
|
+
localStorage.setItem('api_key_perplexity', els.keyPerplexity.value.trim());
|
|
626
|
+
localStorage.setItem('api_url_ollama', els.urlOllama.value.trim());
|
|
627
|
+
localStorage.setItem('api_url_custom', els.urlCustom.value.trim());
|
|
628
|
+
localStorage.setItem('api_key_custom', els.keyCustom.value.trim());
|
|
629
|
+
|
|
630
|
+
// Save to backend configuration
|
|
631
|
+
const providers_to_save = ['nvidia', 'groq', 'openai', 'anthropic', 'gemini', 'mistral', 'huggingface', 'cohere', 'together', 'openrouter', 'deepseek', 'perplexity', 'ollama', 'custom'];
|
|
632
|
+
providers_to_save.forEach(async (prov) => {
|
|
633
|
+
const fd = new FormData();
|
|
634
|
+
fd.append('provider', prov);
|
|
635
|
+
if (prov === 'ollama') {
|
|
636
|
+
fd.append('base_url', els.urlOllama.value.trim());
|
|
637
|
+
} else if (prov === 'custom') {
|
|
638
|
+
fd.append('base_url', els.urlCustom.value.trim());
|
|
639
|
+
fd.append('api_key', els.keyCustom.value.trim());
|
|
640
|
+
} else {
|
|
641
|
+
const capId = prov.charAt(0).toUpperCase() + prov.slice(1);
|
|
642
|
+
const val = (els[`key${capId}`] ? els[`key${capId}`].value.trim() : '');
|
|
643
|
+
fd.append('api_key', val);
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
await fetch('/api/config', { method: 'POST', body: fd });
|
|
647
|
+
} catch (err) {
|
|
648
|
+
console.error('Failed to sync settings to backend for', prov, err);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const cooldown = els.settingsCooldown.value;
|
|
653
|
+
localStorage.setItem('llm_cooldown', cooldown);
|
|
654
|
+
els.cooldown.value = cooldown;
|
|
655
|
+
els.cooldownVal.textContent = cooldown;
|
|
656
|
+
|
|
657
|
+
// Re-populate sidebar provider list
|
|
658
|
+
populateProvidersDropdown();
|
|
659
|
+
syncActiveApiKey();
|
|
660
|
+
|
|
661
|
+
toast('Settings saved permanently!', 'success');
|
|
662
|
+
els.settingsModal.classList.add('hidden');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Wire Settings Modal Buttons & Navigation
|
|
666
|
+
if (els.sidebarSettingsBtn) {
|
|
667
|
+
els.sidebarSettingsBtn.addEventListener('click', () => {
|
|
668
|
+
populateSettingsModal();
|
|
669
|
+
els.settingsModal.classList.remove('hidden');
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
if (els.closeSettingsModal) {
|
|
673
|
+
els.closeSettingsModal.addEventListener('click', () => {
|
|
674
|
+
els.settingsModal.classList.add('hidden');
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
if (els.cancelSettingsBtn) {
|
|
678
|
+
els.cancelSettingsBtn.addEventListener('click', () => {
|
|
679
|
+
els.settingsModal.classList.add('hidden');
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
if (els.settingsModal) {
|
|
683
|
+
els.settingsModal.addEventListener('click', e => {
|
|
684
|
+
if (e.target === els.settingsModal) els.settingsModal.classList.add('hidden');
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
if (els.saveSettingsBtn) {
|
|
688
|
+
els.saveSettingsBtn.addEventListener('click', saveSettingsModal);
|
|
689
|
+
}
|
|
690
|
+
if (els.settingsCooldown) {
|
|
691
|
+
els.settingsCooldown.addEventListener('input', () => {
|
|
692
|
+
els.settingsCooldownVal.textContent = els.settingsCooldown.value;
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Eye button toggle for Settings modal
|
|
697
|
+
document.querySelectorAll('.toggle-setting-key').forEach(btn => {
|
|
698
|
+
btn.addEventListener('click', e => {
|
|
699
|
+
e.preventDefault();
|
|
700
|
+
const input = btn.previousElementSibling;
|
|
701
|
+
if (input) {
|
|
702
|
+
input.type = input.type === 'password' ? 'text' : 'password';
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Test Connection individually inside Settings Modal
|
|
708
|
+
document.querySelectorAll('.test-individual-btn').forEach(btn => {
|
|
709
|
+
btn.addEventListener('click', async (e) => {
|
|
710
|
+
e.preventDefault();
|
|
711
|
+
const provider = btn.dataset.provider;
|
|
712
|
+
const model = PROVIDER_TEST_MODELS[provider];
|
|
713
|
+
|
|
714
|
+
// Find the input element and status element for this provider
|
|
715
|
+
let apiKey = '';
|
|
716
|
+
const capId = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
717
|
+
const inputEl = els[`key${capId}`] || els[`url${capId}`] || document.getElementById(`key${capId}`) || document.getElementById(`url${capId}`);
|
|
718
|
+
const statusEl = document.getElementById(`status${capId}`);
|
|
719
|
+
|
|
720
|
+
if (inputEl) {
|
|
721
|
+
apiKey = inputEl.value.trim();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (statusEl) {
|
|
725
|
+
statusEl.className = 'individual-status loading';
|
|
726
|
+
statusEl.textContent = 'Testing connection...';
|
|
727
|
+
statusEl.style.color = 'var(--text-secondary)';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const fd = new FormData();
|
|
731
|
+
fd.append('provider', provider);
|
|
732
|
+
fd.append('model', model);
|
|
733
|
+
fd.append('api_key', apiKey);
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const res = await fetch('/api/validate-key', { method: 'POST', body: fd });
|
|
737
|
+
if (res.ok) {
|
|
738
|
+
if (statusEl) {
|
|
739
|
+
statusEl.className = 'individual-status ok';
|
|
740
|
+
statusEl.textContent = '✓ Connection successful';
|
|
741
|
+
statusEl.style.color = 'var(--emerald)';
|
|
742
|
+
}
|
|
743
|
+
toast(`${provider.toUpperCase()} connection verified!`, 'success');
|
|
744
|
+
} else {
|
|
745
|
+
const data = await res.json().catch(() => ({}));
|
|
746
|
+
if (statusEl) {
|
|
747
|
+
statusEl.className = 'individual-status error';
|
|
748
|
+
statusEl.textContent = '✗ ' + (data.detail || 'Connection failed');
|
|
749
|
+
statusEl.style.color = 'var(--rose)';
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
} catch (err) {
|
|
753
|
+
if (statusEl) {
|
|
754
|
+
statusEl.className = 'individual-status error';
|
|
755
|
+
statusEl.textContent = '✗ Network error';
|
|
756
|
+
statusEl.style.color = 'var(--rose)';
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
763
|
+
// LLM Provider & Model selector
|
|
764
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
765
|
+
async function populateModels(provider) {
|
|
766
|
+
let models = MODEL_OPTIONS[provider] || [];
|
|
767
|
+
|
|
768
|
+
if (provider === 'ollama') {
|
|
769
|
+
const sel = els.llmModel;
|
|
770
|
+
sel.innerHTML = '<option value="">🔄 Loading Ollama models...</option>';
|
|
771
|
+
models = await fetchOllamaModels();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const sel = els.llmModel;
|
|
775
|
+
sel.innerHTML = '';
|
|
776
|
+
models.forEach(m => {
|
|
777
|
+
const opt = document.createElement('option');
|
|
778
|
+
opt.value = m; opt.textContent = m;
|
|
779
|
+
sel.appendChild(opt);
|
|
780
|
+
});
|
|
781
|
+
// Add custom option
|
|
782
|
+
const customOpt = document.createElement('option');
|
|
783
|
+
customOpt.value = '__custom__';
|
|
784
|
+
customOpt.textContent = '✏️ Custom model…';
|
|
785
|
+
sel.appendChild(customOpt);
|
|
786
|
+
|
|
787
|
+
// Restore saved model selection if applicable
|
|
788
|
+
const savedModel = localStorage.getItem('llm_model');
|
|
789
|
+
if (savedModel && provider === localStorage.getItem('llm_provider')) {
|
|
790
|
+
const modelOptions = Array.from(sel.options).map(opt => opt.value);
|
|
791
|
+
if (!modelOptions.includes(savedModel) && savedModel !== '__custom__') {
|
|
792
|
+
const opt = document.createElement('option');
|
|
793
|
+
opt.value = savedModel;
|
|
794
|
+
opt.textContent = savedModel;
|
|
795
|
+
sel.insertBefore(opt, sel.lastElementChild);
|
|
796
|
+
}
|
|
797
|
+
sel.value = savedModel;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
els.llmProvider.addEventListener('change', async () => {
|
|
802
|
+
await populateModels(els.llmProvider.value);
|
|
803
|
+
syncActiveApiKey();
|
|
804
|
+
saveLlmSettings();
|
|
805
|
+
});
|
|
806
|
+
els.llmModel.addEventListener('change', async () => {
|
|
807
|
+
if (els.llmModel.value === '__custom__') {
|
|
808
|
+
const custom = await customPrompt('Enter full model identifier:', '', 'e.g. gpt-4o-mini', 'Custom Model');
|
|
809
|
+
if (custom) {
|
|
810
|
+
const opt = document.createElement('option');
|
|
811
|
+
opt.value = custom; opt.textContent = custom; opt.selected = true;
|
|
812
|
+
els.llmModel.insertBefore(opt, els.llmModel.lastElementChild);
|
|
813
|
+
els.llmModel.value = custom;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
saveLlmSettings();
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
821
|
+
// Sidebar collapse
|
|
822
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
823
|
+
els.sidebarToggle.addEventListener('click', () => {
|
|
824
|
+
const collapsed = els.sidebar.classList.toggle('collapsed');
|
|
825
|
+
els.sidebarToggle.textContent = collapsed ? '›' : '‹';
|
|
826
|
+
});
|
|
827
|
+
els.mobileSidebarBtn.addEventListener('click', () => {
|
|
828
|
+
els.sidebar.classList.toggle('mobile-open');
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
832
|
+
// Test LLM Connection
|
|
833
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
834
|
+
els.testConnectionBtn.addEventListener('click', async () => {
|
|
835
|
+
const provider = els.llmProvider.value;
|
|
836
|
+
const model = els.llmModel.value;
|
|
837
|
+
const apiKey = els.apiKey.value.trim();
|
|
838
|
+
|
|
839
|
+
els.connectionStatus.className = 'connection-status loading';
|
|
840
|
+
els.connectionStatus.textContent = 'Testing…';
|
|
841
|
+
|
|
842
|
+
const fd = new FormData();
|
|
843
|
+
fd.append('provider', provider);
|
|
844
|
+
fd.append('model', model);
|
|
845
|
+
fd.append('api_key', apiKey);
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
const res = await fetch('/api/validate-key', { method: 'POST', body: fd });
|
|
849
|
+
if (res.ok) {
|
|
850
|
+
els.connectionStatus.className = 'connection-status ok';
|
|
851
|
+
els.connectionStatus.textContent = '✓ Connection successful';
|
|
852
|
+
toast('LLM connection verified!', 'success');
|
|
853
|
+
} else {
|
|
854
|
+
const data = await res.json().catch(() => ({}));
|
|
855
|
+
els.connectionStatus.className = 'connection-status error';
|
|
856
|
+
els.connectionStatus.textContent = '✗ ' + (data.detail || 'Connection failed');
|
|
857
|
+
}
|
|
858
|
+
} catch (e) {
|
|
859
|
+
els.connectionStatus.className = 'connection-status error';
|
|
860
|
+
els.connectionStatus.textContent = '✗ Network error';
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
865
|
+
// Projects sidebar & Local Storage Tracking
|
|
866
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
867
|
+
function getMyProjects() {
|
|
868
|
+
try {
|
|
869
|
+
return JSON.parse(localStorage.getItem('owned_projects') || '[]');
|
|
870
|
+
} catch {
|
|
871
|
+
return [];
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function addMyProject(id) {
|
|
876
|
+
const projects = getMyProjects();
|
|
877
|
+
if (!projects.includes(id)) {
|
|
878
|
+
projects.push(id);
|
|
879
|
+
localStorage.setItem('owned_projects', JSON.stringify(projects));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function removeMyProject(id) {
|
|
884
|
+
let projects = getMyProjects();
|
|
885
|
+
projects = projects.filter(p => p !== id);
|
|
886
|
+
localStorage.setItem('owned_projects', JSON.stringify(projects));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function loadProjects() {
|
|
890
|
+
try {
|
|
891
|
+
const res = await fetch('/api/projects');
|
|
892
|
+
const allProjects = await res.json();
|
|
893
|
+
const myProjectIds = getMyProjects();
|
|
894
|
+
state.projects = allProjects.filter(p => myProjectIds.includes(p.id));
|
|
895
|
+
renderProjectsList();
|
|
896
|
+
} catch {
|
|
897
|
+
// Server not ready yet, retry
|
|
898
|
+
setTimeout(loadProjects, 2000);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function formatBytes(bytes) {
|
|
903
|
+
if (!bytes) return '0 Bytes';
|
|
904
|
+
const k = 1024;
|
|
905
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
906
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
907
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function renderProjectsList() {
|
|
911
|
+
// 1. Render sidebar list
|
|
912
|
+
const list = els.projectsList;
|
|
913
|
+
if (list) {
|
|
914
|
+
if (!state.projects.length) {
|
|
915
|
+
list.innerHTML = '<div class="projects-empty">No projects yet.<br>Upload a CSV to begin.</div>';
|
|
916
|
+
} else {
|
|
917
|
+
list.innerHTML = '';
|
|
918
|
+
state.projects.forEach(p => {
|
|
919
|
+
const item = document.createElement('div');
|
|
920
|
+
item.className = `project-item ${state.activeProject?.id === p.id ? 'active' : ''}`;
|
|
921
|
+
item.dataset.id = p.id;
|
|
922
|
+
item.innerHTML = `
|
|
923
|
+
<div class="project-dot ${p.status}"></div>
|
|
924
|
+
<div class="project-name" title="${p.name}">${p.name}</div>
|
|
925
|
+
<button class="project-del" title="Delete project" data-id="${p.id}">✕</button>
|
|
926
|
+
`;
|
|
927
|
+
item.addEventListener('click', e => {
|
|
928
|
+
if (e.target.classList.contains('project-del')) return;
|
|
929
|
+
switchToProject(p);
|
|
930
|
+
});
|
|
931
|
+
item.querySelector('.project-del').addEventListener('click', e => {
|
|
932
|
+
e.stopPropagation();
|
|
933
|
+
deleteProject(p.id);
|
|
934
|
+
});
|
|
935
|
+
list.appendChild(item);
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// 2. Render dashboard projects section
|
|
941
|
+
const dbGrid = els.dashboardProjectsGrid;
|
|
942
|
+
const dbCount = els.dashboardProjectsCount;
|
|
943
|
+
|
|
944
|
+
if (dbGrid && dbCount) {
|
|
945
|
+
dbCount.textContent = `(${state.projects.length})`;
|
|
946
|
+
|
|
947
|
+
if (!state.projects.length) {
|
|
948
|
+
dbGrid.innerHTML = `
|
|
949
|
+
<div class="projects-empty-card">
|
|
950
|
+
<div class="empty-card-icon"><i data-lucide="folder-open" style="width: 48px; height: 48px; color: var(--violet-light);"></i></div>
|
|
951
|
+
<div class="empty-card-title">No projects yet</div>
|
|
952
|
+
<div class="empty-card-desc">Upload a CSV dataset above to start your first agentic analysis.</div>
|
|
953
|
+
</div>
|
|
954
|
+
`;
|
|
955
|
+
} else {
|
|
956
|
+
dbGrid.innerHTML = '';
|
|
957
|
+
state.projects.forEach(p => {
|
|
958
|
+
const card = document.createElement('div');
|
|
959
|
+
card.className = `dashboard-project-card ${state.activeProject?.id === p.id ? 'active' : ''}`;
|
|
960
|
+
card.dataset.id = p.id;
|
|
961
|
+
|
|
962
|
+
const d = p.created_at ? new Date(p.created_at) : new Date();
|
|
963
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
964
|
+
const formattedDate = `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`;
|
|
965
|
+
|
|
966
|
+
const sizeStr = formatBytes(p.size);
|
|
967
|
+
|
|
968
|
+
card.innerHTML = `
|
|
969
|
+
<div class="db-project-card-header">
|
|
970
|
+
<span class="db-project-date">${formattedDate}</span>
|
|
971
|
+
<span class="db-project-status-badge ${p.status}">${p.status}</span>
|
|
972
|
+
</div>
|
|
973
|
+
<div class="db-project-preview-wrap">
|
|
974
|
+
<img src="${p.thumbnail || DEFAULT_THUMBNAIL_SVG}" class="db-project-preview-img" alt="Project Preview" onerror="this.onerror=null; this.src=DEFAULT_THUMBNAIL_SVG;" />
|
|
975
|
+
<div class="db-project-preview-overlay">
|
|
976
|
+
<div class="db-project-folder-icon ${p.status}">
|
|
977
|
+
<svg viewBox="0 0 24 24" class="folder-svg" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
978
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
979
|
+
</svg>
|
|
980
|
+
</div>
|
|
981
|
+
</div>
|
|
982
|
+
</div>
|
|
983
|
+
<h3 class="db-project-card-title" title="${p.name}">${p.name}</h3>
|
|
984
|
+
<div class="db-project-card-footer">
|
|
985
|
+
<span>SIZE: ${sizeStr}</span>
|
|
986
|
+
<span class="db-project-filename" title="${p.filename || 'dataset.csv'}">${p.filename || 'dataset.csv'}</span>
|
|
987
|
+
</div>
|
|
988
|
+
<button class="db-project-btn-del" title="Delete project">✕</button>
|
|
989
|
+
`;
|
|
990
|
+
|
|
991
|
+
card.addEventListener('click', e => {
|
|
992
|
+
if (e.target.classList.contains('db-project-btn-del')) return;
|
|
993
|
+
switchToProject(p);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
card.querySelector('.db-project-btn-del').addEventListener('click', e => {
|
|
997
|
+
e.stopPropagation();
|
|
998
|
+
deleteProject(p.id);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
dbGrid.appendChild(card);
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async function refreshPreviewData(sessionId) {
|
|
1008
|
+
try {
|
|
1009
|
+
const res = await fetch(`/api/projects/${sessionId}/preview`);
|
|
1010
|
+
if (res.ok) {
|
|
1011
|
+
const data = await res.json();
|
|
1012
|
+
state.columns = data.columns || [];
|
|
1013
|
+
state.colTypes = data.col_types || {};
|
|
1014
|
+
renderPreview(data.preview || []);
|
|
1015
|
+
if (els.chatPreviewDims) {
|
|
1016
|
+
els.chatPreviewDims.textContent = `(${data.rows_count?.toLocaleString() || 0} rows × ${data.cols_count || 0} columns)`;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
console.error('Failed to load dynamic preview:', e);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function updateSidebarProjectActionsVisibility(sec) {
|
|
1025
|
+
const isProjectCompleted = state.activeProject && state.activeProject.status === 'completed';
|
|
1026
|
+
if (els.sidebarProjectActions) {
|
|
1027
|
+
if (isProjectCompleted) {
|
|
1028
|
+
els.sidebarProjectActions.classList.remove('hidden');
|
|
1029
|
+
} else {
|
|
1030
|
+
els.sidebarProjectActions.classList.add('hidden');
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
let preChatSidebarCollapsed = false;
|
|
1036
|
+
|
|
1037
|
+
function switchSection(sec) {
|
|
1038
|
+
const resultsScreen = document.getElementById('resultsScreen');
|
|
1039
|
+
if (sec === 'chat') {
|
|
1040
|
+
els.btnSectionChat.classList.add('active');
|
|
1041
|
+
els.btnSectionAgentic.classList.remove('active');
|
|
1042
|
+
els.areaChat.classList.remove('hidden');
|
|
1043
|
+
els.areaAgentic.classList.add('hidden');
|
|
1044
|
+
|
|
1045
|
+
els.btnSectionChat.style.background = 'var(--violet)';
|
|
1046
|
+
els.btnSectionChat.style.color = '#fff';
|
|
1047
|
+
els.btnSectionAgentic.style.background = 'transparent';
|
|
1048
|
+
els.btnSectionAgentic.style.color = 'var(--text-secondary)';
|
|
1049
|
+
|
|
1050
|
+
if (resultsScreen) resultsScreen.classList.add('ai-chat-mode');
|
|
1051
|
+
|
|
1052
|
+
// Auto-collapse sidebar for full screen chat view
|
|
1053
|
+
preChatSidebarCollapsed = els.sidebar.classList.contains('collapsed');
|
|
1054
|
+
if (!preChatSidebarCollapsed) {
|
|
1055
|
+
els.sidebar.classList.add('collapsed');
|
|
1056
|
+
if (els.sidebarToggle) els.sidebarToggle.textContent = '›';
|
|
1057
|
+
}
|
|
1058
|
+
} else {
|
|
1059
|
+
els.btnSectionChat.classList.remove('active');
|
|
1060
|
+
els.btnSectionAgentic.classList.add('active');
|
|
1061
|
+
els.areaChat.classList.add('hidden');
|
|
1062
|
+
els.areaAgentic.classList.remove('hidden');
|
|
1063
|
+
|
|
1064
|
+
els.btnSectionAgentic.style.background = 'var(--violet)';
|
|
1065
|
+
els.btnSectionAgentic.style.color = '#fff';
|
|
1066
|
+
els.btnSectionChat.style.background = 'transparent';
|
|
1067
|
+
els.btnSectionChat.style.color = 'var(--text-secondary)';
|
|
1068
|
+
|
|
1069
|
+
if (resultsScreen) resultsScreen.classList.remove('ai-chat-mode');
|
|
1070
|
+
|
|
1071
|
+
// Restore sidebar state when exiting chat mode
|
|
1072
|
+
if (preChatSidebarCollapsed === false && els.sidebar.classList.contains('collapsed')) {
|
|
1073
|
+
els.sidebar.classList.remove('collapsed');
|
|
1074
|
+
if (els.sidebarToggle) els.sidebarToggle.textContent = '‹';
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// Resize Plotly charts when switching to agentic area
|
|
1078
|
+
if (sec === 'agentic') {
|
|
1079
|
+
setTimeout(() => Plotly.Plots?.resize?.(), 100);
|
|
1080
|
+
}
|
|
1081
|
+
updateSidebarProjectActionsVisibility(sec);
|
|
1082
|
+
|
|
1083
|
+
if (window.lucide) {
|
|
1084
|
+
lucide.createIcons({ attrs: { class: 'icon-svg' } });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function switchToProject(p) {
|
|
1089
|
+
state.activeProject = p;
|
|
1090
|
+
renderProjectsList();
|
|
1091
|
+
setBreadcrumb(p.name);
|
|
1092
|
+
setStatus('● Loading…', 'running');
|
|
1093
|
+
updateSidebarProjectActionsVisibility();
|
|
1094
|
+
|
|
1095
|
+
if (p.status === 'completed') {
|
|
1096
|
+
if (els.agenticPlaceholder) els.agenticPlaceholder.classList.add('hidden');
|
|
1097
|
+
if (els.agenticTabsBar) els.agenticTabsBar.classList.remove('hidden');
|
|
1098
|
+
if (els.agenticTabPanels) els.agenticTabPanels.classList.remove('hidden');
|
|
1099
|
+
await loadResults(p.id);
|
|
1100
|
+
goToWorkspaceHub();
|
|
1101
|
+
} else if (p.status === 'running') {
|
|
1102
|
+
if (els.agenticPlaceholder) els.agenticPlaceholder.classList.add('hidden');
|
|
1103
|
+
if (els.agenticTabsBar) els.agenticTabsBar.classList.remove('hidden');
|
|
1104
|
+
if (els.agenticTabPanels) els.agenticTabPanels.classList.remove('hidden');
|
|
1105
|
+
showScreen('running');
|
|
1106
|
+
els.runningTitle.textContent = `Analysing "${p.name}"…`;
|
|
1107
|
+
setStatus('● Running', 'running');
|
|
1108
|
+
startSSEStream(p.id);
|
|
1109
|
+
} else {
|
|
1110
|
+
// Idle state
|
|
1111
|
+
if (els.agenticPlaceholder) els.agenticPlaceholder.classList.remove('hidden');
|
|
1112
|
+
if (els.agenticTabsBar) els.agenticTabsBar.classList.add('hidden');
|
|
1113
|
+
if (els.agenticTabPanels) els.agenticTabPanels.classList.add('hidden');
|
|
1114
|
+
|
|
1115
|
+
state.results = null;
|
|
1116
|
+
await refreshPreviewData(p.id);
|
|
1117
|
+
setupExport(p.id);
|
|
1118
|
+
resetChat();
|
|
1119
|
+
goToWorkspaceHub();
|
|
1120
|
+
showScreen('results');
|
|
1121
|
+
setStatus('● Idle', 'idle');
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function deleteProject(id) {
|
|
1126
|
+
const confirmed = await customConfirm('Delete this project and all its data? This action cannot be undone.', 'Delete Project');
|
|
1127
|
+
if (!confirmed) return;
|
|
1128
|
+
delete state.resultsCache[id];
|
|
1129
|
+
await fetch(`/api/projects/${id}`, { method: 'DELETE' });
|
|
1130
|
+
removeMyProject(id);
|
|
1131
|
+
if (state.activeProject?.id === id) {
|
|
1132
|
+
state.activeProject = null;
|
|
1133
|
+
showScreen('landing');
|
|
1134
|
+
setStatus('● Idle', 'idle');
|
|
1135
|
+
}
|
|
1136
|
+
loadProjects();
|
|
1137
|
+
toast('Project deleted.', 'info');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1141
|
+
// Wizard Setup & Navigation
|
|
1142
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1143
|
+
function resetWizardState() {
|
|
1144
|
+
state.newProjectName = '';
|
|
1145
|
+
if (els.wizardProjectName) {
|
|
1146
|
+
const d = new Date();
|
|
1147
|
+
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
1148
|
+
els.wizardProjectName.value = `Analysis - ${d.getDate()} ${months[d.getMonth()]}`;
|
|
1149
|
+
}
|
|
1150
|
+
if (els.wizardStep0) els.wizardStep0.classList.add('active');
|
|
1151
|
+
if (els.wizardStep1) els.wizardStep1.classList.remove('active');
|
|
1152
|
+
if (els.wizardStep2) els.wizardStep2.classList.remove('active');
|
|
1153
|
+
if (els.uploadedFileMeta) els.uploadedFileMeta.classList.add('hidden');
|
|
1154
|
+
if (els.uploadedFileActions) els.uploadedFileActions.classList.add('hidden');
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Import Project ZIP flow
|
|
1158
|
+
if (els.importProjectCard && els.importZipFileInput) {
|
|
1159
|
+
els.importProjectCard.addEventListener('click', () => {
|
|
1160
|
+
els.importZipFileInput.click();
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
els.importZipFileInput.addEventListener('change', async () => {
|
|
1164
|
+
const file = els.importZipFileInput.files[0];
|
|
1165
|
+
if (!file) return;
|
|
1166
|
+
if (!file.name.endsWith('.zip')) {
|
|
1167
|
+
toast('Only ZIP files are supported.', 'error');
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
setStatus('● Importing…', 'running');
|
|
1172
|
+
const fd = new FormData();
|
|
1173
|
+
fd.append('file', file);
|
|
1174
|
+
|
|
1175
|
+
try {
|
|
1176
|
+
const res = await fetch('/api/projects/import-zip', { method: 'POST', body: fd });
|
|
1177
|
+
if (!res.ok) throw new Error((await res.json()).detail || 'Import failed');
|
|
1178
|
+
const data = await res.json();
|
|
1179
|
+
toast(`Project imported successfully: ${data.name}`, 'success');
|
|
1180
|
+
addMyProject(data.id);
|
|
1181
|
+
|
|
1182
|
+
// Reset input
|
|
1183
|
+
els.importZipFileInput.value = '';
|
|
1184
|
+
|
|
1185
|
+
// Reload projects and open the imported project
|
|
1186
|
+
await loadProjects();
|
|
1187
|
+
const importedProj = state.projects.find(p => p.id === data.id);
|
|
1188
|
+
if (importedProj) {
|
|
1189
|
+
switchToProject(importedProj);
|
|
1190
|
+
} else {
|
|
1191
|
+
goToDashboardHome();
|
|
1192
|
+
}
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
toast('Import failed: ' + e.message, 'error');
|
|
1195
|
+
setStatus('● Idle', 'idle');
|
|
1196
|
+
els.importZipFileInput.value = '';
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Wire wizard events
|
|
1202
|
+
if (els.startWizardCard) {
|
|
1203
|
+
els.startWizardCard.addEventListener('click', () => {
|
|
1204
|
+
if (els.wizardStep0) els.wizardStep0.classList.remove('active');
|
|
1205
|
+
if (els.wizardStep1) els.wizardStep1.classList.add('active');
|
|
1206
|
+
if (els.wizardProjectName) els.wizardProjectName.focus();
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (els.wizardBack1Btn) {
|
|
1211
|
+
els.wizardBack1Btn.addEventListener('click', () => {
|
|
1212
|
+
if (els.wizardStep1) els.wizardStep1.classList.remove('active');
|
|
1213
|
+
if (els.wizardStep0) els.wizardStep0.classList.add('active');
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (els.wizardNextBtn) {
|
|
1218
|
+
els.wizardNextBtn.addEventListener('click', () => {
|
|
1219
|
+
const name = els.wizardProjectName.value.trim();
|
|
1220
|
+
if (!name) {
|
|
1221
|
+
toast('Please enter a project name.', 'warning');
|
|
1222
|
+
els.wizardProjectName.focus();
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
state.newProjectName = name;
|
|
1226
|
+
state.newProjectReportTitle = els.wizardReportTitle ? els.wizardReportTitle.value.trim() : '';
|
|
1227
|
+
state.newProjectGoal = els.wizardProjectGoal ? els.wizardProjectGoal.value.trim() : '';
|
|
1228
|
+
|
|
1229
|
+
if (els.wizardUploadTitle) {
|
|
1230
|
+
els.wizardUploadTitle.innerHTML = `Upload CSV for <strong>${escHtml(name)}</strong>`;
|
|
1231
|
+
}
|
|
1232
|
+
if (els.reportTitle) {
|
|
1233
|
+
els.reportTitle.value = state.newProjectReportTitle || `${name} Executive Analysis`;
|
|
1234
|
+
}
|
|
1235
|
+
if (els.wizardStep1) els.wizardStep1.classList.remove('active');
|
|
1236
|
+
if (els.wizardStep2) els.wizardStep2.classList.add('active');
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (els.wizardBackBtn) {
|
|
1241
|
+
els.wizardBackBtn.addEventListener('click', () => {
|
|
1242
|
+
if (els.wizardStep2) els.wizardStep2.classList.remove('active');
|
|
1243
|
+
if (els.wizardStep1) els.wizardStep1.classList.add('active');
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function goToDashboardHome() {
|
|
1248
|
+
resetWizardState();
|
|
1249
|
+
showScreen('landing');
|
|
1250
|
+
state.uploadedFile = null;
|
|
1251
|
+
state.uploadedSession = null;
|
|
1252
|
+
state.activeProject = null;
|
|
1253
|
+
if (els.fileInput) els.fileInput.value = '';
|
|
1254
|
+
setBreadcrumb('New Project');
|
|
1255
|
+
renderProjectsList();
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
window.goToDashboardHome = goToDashboardHome;
|
|
1259
|
+
|
|
1260
|
+
if (els.sidebarLogo) {
|
|
1261
|
+
els.sidebarLogo.addEventListener('click', () => {
|
|
1262
|
+
goToDashboardHome();
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (els.newProjectBtn) {
|
|
1267
|
+
els.newProjectBtn.addEventListener('click', () => {
|
|
1268
|
+
goToDashboardHome();
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function goToWorkspaceHub() {
|
|
1273
|
+
const p = state.activeProject;
|
|
1274
|
+
if (!p) return;
|
|
1275
|
+
|
|
1276
|
+
// Hide sections switcher
|
|
1277
|
+
if (els.btnSectionChat && els.btnSectionChat.parentNode) {
|
|
1278
|
+
els.btnSectionChat.parentNode.classList.add('hidden');
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Show areaHub, hide areaChat and areaAgentic
|
|
1282
|
+
if (els.areaHub) els.areaHub.classList.remove('hidden');
|
|
1283
|
+
if (els.areaChat) els.areaChat.classList.add('hidden');
|
|
1284
|
+
if (els.areaAgentic) els.areaAgentic.classList.add('hidden');
|
|
1285
|
+
|
|
1286
|
+
if (els.resultsScreen) els.resultsScreen.classList.remove('ai-chat-mode');
|
|
1287
|
+
|
|
1288
|
+
// Restore sidebar state when exiting chat mode
|
|
1289
|
+
if (preChatSidebarCollapsed === false && els.sidebar.classList.contains('collapsed')) {
|
|
1290
|
+
els.sidebar.classList.remove('collapsed');
|
|
1291
|
+
if (els.sidebarToggle) els.sidebarToggle.textContent = '‹';
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
setBreadcrumb(p.name);
|
|
1295
|
+
}
|
|
1296
|
+
window.goToWorkspaceHub = goToWorkspaceHub;
|
|
1297
|
+
|
|
1298
|
+
function setBreadcrumb(name, activeSection = '') {
|
|
1299
|
+
if (!activeSection) {
|
|
1300
|
+
els.breadcrumb.innerHTML = `
|
|
1301
|
+
<span class="breadcrumb-item" onclick="goToDashboardHome()" style="cursor:pointer">Dashboard</span>
|
|
1302
|
+
<span class="breadcrumb-sep">›</span>
|
|
1303
|
+
<span class="breadcrumb-item active">${escHtml(name)}</span>
|
|
1304
|
+
`;
|
|
1305
|
+
} else {
|
|
1306
|
+
els.breadcrumb.innerHTML = `
|
|
1307
|
+
<span class="breadcrumb-item" onclick="goToDashboardHome()" style="cursor:pointer">Dashboard</span>
|
|
1308
|
+
<span class="breadcrumb-sep">›</span>
|
|
1309
|
+
<span class="breadcrumb-item" onclick="goToWorkspaceHub()" style="cursor:pointer">${escHtml(name)}</span>
|
|
1310
|
+
<span class="breadcrumb-sep">›</span>
|
|
1311
|
+
<span class="breadcrumb-item active">${escHtml(activeSection)}</span>
|
|
1312
|
+
`;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1317
|
+
// File Upload
|
|
1318
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1319
|
+
['dragover','dragleave','drop'].forEach(evt => {
|
|
1320
|
+
els.uploadZone.addEventListener(evt, e => {
|
|
1321
|
+
e.preventDefault();
|
|
1322
|
+
if (evt === 'dragover') els.uploadZone.classList.add('drag-over');
|
|
1323
|
+
else {
|
|
1324
|
+
els.uploadZone.classList.remove('drag-over');
|
|
1325
|
+
if (evt === 'drop' && e.dataTransfer.files[0]) handleFileSelected(e.dataTransfer.files[0]);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
els.uploadZone.addEventListener('click', () => els.fileInput.click());
|
|
1330
|
+
els.fileInput.addEventListener('change', () => {
|
|
1331
|
+
if (els.fileInput.files[0]) handleFileSelected(els.fileInput.files[0]);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
async function handleFileSelected(file) {
|
|
1335
|
+
if (!file.name.endsWith('.csv')) { toast('Only CSV files are supported.', 'error'); return; }
|
|
1336
|
+
state.uploadedFile = file;
|
|
1337
|
+
|
|
1338
|
+
// Create project immediately via projects endpoint
|
|
1339
|
+
const fd = new FormData();
|
|
1340
|
+
fd.append('name', state.newProjectName || file.name.replace(/\.csv$/i, ''));
|
|
1341
|
+
fd.append('report_title', state.newProjectReportTitle || '');
|
|
1342
|
+
fd.append('goal', state.newProjectGoal || '');
|
|
1343
|
+
fd.append('file', file);
|
|
1344
|
+
|
|
1345
|
+
try {
|
|
1346
|
+
const res = await fetch('/api/projects', { method: 'POST', body: fd });
|
|
1347
|
+
const data = await res.json();
|
|
1348
|
+
state.uploadedSession = data.id;
|
|
1349
|
+
|
|
1350
|
+
els.uploadedFileMeta.textContent = `✓ ${file.name} — ${(file.size / 1024).toFixed(1)} KB uploaded`;
|
|
1351
|
+
els.uploadedFileMeta.classList.remove('hidden');
|
|
1352
|
+
els.uploadedFileActions.classList.remove('hidden');
|
|
1353
|
+
toast(`Project created: ${state.newProjectName || data.name}`, 'success');
|
|
1354
|
+
addMyProject(data.id);
|
|
1355
|
+
|
|
1356
|
+
// Auto-enter project workspace
|
|
1357
|
+
resetWizardState();
|
|
1358
|
+
await loadProjects();
|
|
1359
|
+
const newProj = state.projects.find(p => p.id === data.id);
|
|
1360
|
+
if (newProj) {
|
|
1361
|
+
switchToProject(newProj);
|
|
1362
|
+
}
|
|
1363
|
+
} catch (e) {
|
|
1364
|
+
toast('Project creation failed: ' + e.message, 'error');
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1369
|
+
// API Warning and Validation
|
|
1370
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1371
|
+
function checkApiKeySet() {
|
|
1372
|
+
const provider = els.llmProvider.value;
|
|
1373
|
+
const apiKey = els.apiKey.value.trim();
|
|
1374
|
+
if (provider !== 'ollama' && !apiKey) {
|
|
1375
|
+
const providerNames = {
|
|
1376
|
+
nvidia: 'NVIDIA NIM',
|
|
1377
|
+
groq: 'Groq',
|
|
1378
|
+
openai: 'OpenAI',
|
|
1379
|
+
anthropic: 'Anthropic',
|
|
1380
|
+
gemini: 'Google Gemini',
|
|
1381
|
+
mistral: 'Mistral',
|
|
1382
|
+
huggingface: 'HuggingFace',
|
|
1383
|
+
};
|
|
1384
|
+
els.warningProviderName.textContent = providerNames[provider] || provider.toUpperCase();
|
|
1385
|
+
els.apiWarningModal.classList.remove('hidden');
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// API Warning Modal Event Listeners
|
|
1392
|
+
if (els.closeApiWarningBtn) {
|
|
1393
|
+
els.closeApiWarningBtn.addEventListener('click', () => {
|
|
1394
|
+
els.apiWarningModal.classList.add('hidden');
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
if (els.apiWarningModal) {
|
|
1398
|
+
els.apiWarningModal.addEventListener('click', e => {
|
|
1399
|
+
if (e.target === els.apiWarningModal) els.apiWarningModal.classList.add('hidden');
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
if (els.guideToApiBtn) {
|
|
1403
|
+
els.guideToApiBtn.addEventListener('click', () => {
|
|
1404
|
+
els.apiWarningModal.classList.add('hidden');
|
|
1405
|
+
els.configModal.classList.add('hidden');
|
|
1406
|
+
|
|
1407
|
+
// Open Settings Modal
|
|
1408
|
+
populateSettingsModal();
|
|
1409
|
+
els.settingsModal.classList.remove('hidden');
|
|
1410
|
+
|
|
1411
|
+
// Find the input element for the active provider
|
|
1412
|
+
const provider = els.llmProvider.value;
|
|
1413
|
+
let targetInput = null;
|
|
1414
|
+
if (provider === 'nvidia') targetInput = els.keyNvidia;
|
|
1415
|
+
else if (provider === 'groq') targetInput = els.keyGroq;
|
|
1416
|
+
else if (provider === 'openai') targetInput = els.keyOpenai;
|
|
1417
|
+
else if (provider === 'anthropic') targetInput = els.keyAnthropic;
|
|
1418
|
+
else if (provider === 'gemini') targetInput = els.keyGemini;
|
|
1419
|
+
else if (provider === 'mistral') targetInput = els.keyMistral;
|
|
1420
|
+
else if (provider === 'huggingface') targetInput = els.keyHuggingface;
|
|
1421
|
+
else if (provider === 'ollama') targetInput = els.urlOllama;
|
|
1422
|
+
|
|
1423
|
+
if (targetInput) {
|
|
1424
|
+
targetInput.classList.add('highlight-glowing');
|
|
1425
|
+
targetInput.focus();
|
|
1426
|
+
|
|
1427
|
+
const removeHighlight = () => {
|
|
1428
|
+
targetInput.classList.remove('highlight-glowing');
|
|
1429
|
+
targetInput.removeEventListener('input', removeHighlight);
|
|
1430
|
+
};
|
|
1431
|
+
targetInput.addEventListener('input', removeHighlight);
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
els.startAnalysisBtn.addEventListener('click', () => {
|
|
1437
|
+
if (!state.uploadedSession) { toast('No file uploaded yet.', 'warning'); return; }
|
|
1438
|
+
if (!checkApiKeySet()) return;
|
|
1439
|
+
openConfigModal();
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1443
|
+
// Config Modal
|
|
1444
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1445
|
+
function openConfigModal() {
|
|
1446
|
+
els.configModal.classList.remove('hidden');
|
|
1447
|
+
}
|
|
1448
|
+
function closeConfigModal() {
|
|
1449
|
+
els.configModal.classList.add('hidden');
|
|
1450
|
+
}
|
|
1451
|
+
els.closeConfigModal.addEventListener('click', closeConfigModal);
|
|
1452
|
+
els.cancelConfigBtn.addEventListener('click', closeConfigModal);
|
|
1453
|
+
els.configModal.addEventListener('click', e => {
|
|
1454
|
+
if (e.target === els.configModal) closeConfigModal();
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
// ── Task card dependency logic ───────────────────────────────────────────────
|
|
1458
|
+
function syncTaskCards() {
|
|
1459
|
+
const cleanChecked = els.taskCleaning.checked;
|
|
1460
|
+
const relationsChecked = els.taskRelations.checked;
|
|
1461
|
+
|
|
1462
|
+
// Relations & Insights require Cleaning
|
|
1463
|
+
if (!cleanChecked) {
|
|
1464
|
+
els.taskRelations.checked = false;
|
|
1465
|
+
els.taskInsights.checked = false;
|
|
1466
|
+
}
|
|
1467
|
+
// Viz requires Relations
|
|
1468
|
+
if (!relationsChecked) {
|
|
1469
|
+
els.taskViz.checked = false;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Update visual state
|
|
1473
|
+
[
|
|
1474
|
+
[els.taskCleaning, els.taskCardCleaning, true],
|
|
1475
|
+
[els.taskRelations, els.taskCardRelations, cleanChecked],
|
|
1476
|
+
[els.taskInsights, els.taskCardInsights, cleanChecked],
|
|
1477
|
+
[els.taskViz, els.taskCardViz, els.taskRelations.checked || relationsChecked],
|
|
1478
|
+
].forEach(([chk, card, enabled]) => {
|
|
1479
|
+
card.classList.toggle('checked', chk.checked);
|
|
1480
|
+
card.classList.toggle('disabled', !enabled);
|
|
1481
|
+
if (!enabled) chk.checked = false;
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
['taskCleaning','taskRelations','taskInsights','taskViz'].forEach(id => {
|
|
1486
|
+
$(id).closest('.task-card').addEventListener('click', () => {
|
|
1487
|
+
const chk = $(id);
|
|
1488
|
+
if (!$(id).closest('.task-card').classList.contains('disabled')) {
|
|
1489
|
+
chk.checked = !chk.checked;
|
|
1490
|
+
syncTaskCards();
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
});
|
|
1494
|
+
syncTaskCards();
|
|
1495
|
+
|
|
1496
|
+
// ── Depth selector ───────────────────────────────────────────────────────────
|
|
1497
|
+
$$('.depth-option').forEach(opt => {
|
|
1498
|
+
opt.addEventListener('click', () => {
|
|
1499
|
+
$$('.depth-option').forEach(o => o.classList.remove('active'));
|
|
1500
|
+
opt.classList.add('active');
|
|
1501
|
+
});
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
// ── Run button ───────────────────────────────────────────────────────────────
|
|
1505
|
+
els.runAnalysisBtn.addEventListener('click', async () => {
|
|
1506
|
+
if (!checkApiKeySet()) return;
|
|
1507
|
+
const tasks = [];
|
|
1508
|
+
if (els.taskCleaning.checked) tasks.push('cleaning');
|
|
1509
|
+
if (els.taskRelations.checked) tasks.push('relations');
|
|
1510
|
+
if (els.taskInsights.checked) tasks.push('insights');
|
|
1511
|
+
if (els.taskViz.checked) tasks.push('visualization');
|
|
1512
|
+
|
|
1513
|
+
if (!tasks.length) { toast('Select at least one task.', 'warning'); return; }
|
|
1514
|
+
|
|
1515
|
+
const deep = $$('.depth-option.active')[0]?.querySelector('input')?.value === 'true';
|
|
1516
|
+
|
|
1517
|
+
const provider = els.llmProvider.value;
|
|
1518
|
+
const model = els.llmModel.value === '__custom__' ? '' : els.llmModel.value;
|
|
1519
|
+
const apiKey = els.apiKey.value.trim();
|
|
1520
|
+
const cooldown = parseInt(els.cooldown.value, 10);
|
|
1521
|
+
const title = els.reportTitle.value.trim();
|
|
1522
|
+
|
|
1523
|
+
const fd = new FormData();
|
|
1524
|
+
fd.append('session_id', state.uploadedSession);
|
|
1525
|
+
fd.append('provider', provider);
|
|
1526
|
+
fd.append('model', model);
|
|
1527
|
+
fd.append('api_key', apiKey);
|
|
1528
|
+
fd.append('cooldown', cooldown);
|
|
1529
|
+
fd.append('selected_tasks', tasks.join(','));
|
|
1530
|
+
fd.append('deep_analysis', String(deep));
|
|
1531
|
+
fd.append('report_title', title);
|
|
1532
|
+
|
|
1533
|
+
closeConfigModal();
|
|
1534
|
+
|
|
1535
|
+
try {
|
|
1536
|
+
const res = await fetch('/api/analyze', { method: 'POST', body: fd });
|
|
1537
|
+
if (!res.ok) throw new Error((await res.json()).detail || 'Start failed');
|
|
1538
|
+
|
|
1539
|
+
// Update project metadata locally in-place (or add if new)
|
|
1540
|
+
const projName = title || state.uploadedFile?.name?.replace(/\.csv$/i, '') || 'New Analysis';
|
|
1541
|
+
delete state.resultsCache[state.uploadedSession];
|
|
1542
|
+
const existingIdx = state.projects.findIndex(p => p.id === state.uploadedSession);
|
|
1543
|
+
if (existingIdx !== -1) {
|
|
1544
|
+
state.projects[existingIdx].status = 'running';
|
|
1545
|
+
state.projects[existingIdx].name = projName;
|
|
1546
|
+
state.activeProject = state.projects[existingIdx];
|
|
1547
|
+
} else {
|
|
1548
|
+
const newProj = {
|
|
1549
|
+
id: state.uploadedSession,
|
|
1550
|
+
name: projName,
|
|
1551
|
+
status: 'running',
|
|
1552
|
+
};
|
|
1553
|
+
state.activeProject = newProj;
|
|
1554
|
+
state.projects.unshift(newProj);
|
|
1555
|
+
}
|
|
1556
|
+
renderProjectsList();
|
|
1557
|
+
setBreadcrumb(projName);
|
|
1558
|
+
|
|
1559
|
+
showScreen('running');
|
|
1560
|
+
els.runningTitle.textContent = `Analysing "${projName}"…`;
|
|
1561
|
+
setStatus('● Running', 'running');
|
|
1562
|
+
startSSEStream(state.uploadedSession);
|
|
1563
|
+
toast('Analysis started!', 'success');
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
toast('Failed to start analysis: ' + e.message, 'error');
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1570
|
+
// SSE Live Log Stream
|
|
1571
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1572
|
+
const STAGE_KEYWORDS = {
|
|
1573
|
+
cleaning: ['cleaning', 'cleaned', 'data clean'],
|
|
1574
|
+
relations: ['relation', 'correlation'],
|
|
1575
|
+
insights: ['insight', 'business'],
|
|
1576
|
+
visualization: ['visualization', 'png', 'chart', 'plot'],
|
|
1577
|
+
plotly: ['plotly', 'interactive chart'],
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
function classifyLine(line) {
|
|
1581
|
+
const ll = line.toLowerCase();
|
|
1582
|
+
if (ll.includes('[thought]') || ll.startsWith('thought:')) return 'thought';
|
|
1583
|
+
if (ll.includes('[calling tool]') || ll.startsWith('action:')) return 'action';
|
|
1584
|
+
if (ll.includes('[tool response]') || ll.includes('observation:')) return 'response';
|
|
1585
|
+
if (ll.includes('[task]') || ll.includes('complete')) return 'done';
|
|
1586
|
+
if (ll.includes('[error]') || ll.includes('error')) return 'error';
|
|
1587
|
+
if (ll.includes('[warning]') || ll.includes('warning')) return 'warning';
|
|
1588
|
+
return '';
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function appendLog(line) {
|
|
1592
|
+
const div = document.createElement('div');
|
|
1593
|
+
div.className = `log-line ${classifyLine(line)}`;
|
|
1594
|
+
div.textContent = line;
|
|
1595
|
+
els.logOutput.appendChild(div);
|
|
1596
|
+
els.logOutput.scrollTop = els.logOutput.scrollHeight;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
let povInterval = null;
|
|
1600
|
+
|
|
1601
|
+
function renderStagePov(stage) {
|
|
1602
|
+
if (povInterval) {
|
|
1603
|
+
clearInterval(povInterval);
|
|
1604
|
+
povInterval = null;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
if (!els.stagePovPanel) return;
|
|
1608
|
+
|
|
1609
|
+
if (stage === 'cleaning') {
|
|
1610
|
+
els.stagePovPanel.innerHTML = `
|
|
1611
|
+
<div class="pov-header">
|
|
1612
|
+
<div class="pov-title"><i data-lucide="brush" style="width:18px; height:18px; color:var(--violet-light); vertical-align:middle; margin-right:8px;"></i> Data Sanitizer Active</div>
|
|
1613
|
+
<div class="pov-desc">Scanning column profiles, auditing data formatting anomalies, and executing Python sanitization code...</div>
|
|
1614
|
+
</div>
|
|
1615
|
+
<div class="pov-content">
|
|
1616
|
+
<div class="pov-scanner-wrap" id="povScannerWrap">
|
|
1617
|
+
<div class="pov-scanner-line"></div>
|
|
1618
|
+
<div class="pov-scanner-row active" data-idx="0"><span>[SCAN] Reading source CSV...</span><span>SCANNING</span></div>
|
|
1619
|
+
<div class="pov-scanner-row" data-idx="1"><span>[AUDIT] Assessing columns for missing cells...</span><span>PENDING</span></div>
|
|
1620
|
+
<div class="pov-scanner-row" data-idx="2"><span>[CLEAN] Executing auto-imputation models...</span><span>PENDING</span></div>
|
|
1621
|
+
<div class="pov-scanner-row" data-idx="3"><span>[EXPORT] Committing sanitized dataset...</span><span>PENDING</span></div>
|
|
1622
|
+
</div>
|
|
1623
|
+
</div>`;
|
|
1624
|
+
|
|
1625
|
+
const rows = els.stagePovPanel.querySelectorAll('.pov-scanner-row');
|
|
1626
|
+
let currentIdx = 0;
|
|
1627
|
+
povInterval = setInterval(() => {
|
|
1628
|
+
if (currentIdx < rows.length) {
|
|
1629
|
+
rows.forEach((r, idx) => {
|
|
1630
|
+
r.classList.toggle('active', idx === currentIdx);
|
|
1631
|
+
const statusCol = r.querySelectorAll('span')[1];
|
|
1632
|
+
if (idx < currentIdx) {
|
|
1633
|
+
statusCol.textContent = 'COMPLETE';
|
|
1634
|
+
statusCol.style.color = '#10b981';
|
|
1635
|
+
} else if (idx === currentIdx) {
|
|
1636
|
+
statusCol.textContent = 'RUNNING';
|
|
1637
|
+
statusCol.style.color = '#ffffff';
|
|
1638
|
+
} else {
|
|
1639
|
+
statusCol.textContent = 'PENDING';
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
currentIdx++;
|
|
1643
|
+
} else {
|
|
1644
|
+
rows.forEach(r => {
|
|
1645
|
+
const statusCol = r.querySelectorAll('span')[1];
|
|
1646
|
+
statusCol.textContent = 'COMPLETE';
|
|
1647
|
+
statusCol.style.color = '#10b981';
|
|
1648
|
+
});
|
|
1649
|
+
clearInterval(povInterval);
|
|
1650
|
+
}
|
|
1651
|
+
}, 2500);
|
|
1652
|
+
|
|
1653
|
+
} else if (stage === 'relations') {
|
|
1654
|
+
els.stagePovPanel.innerHTML = `
|
|
1655
|
+
<div class="pov-header">
|
|
1656
|
+
<div class="pov-title"><i data-lucide="link" style="width:18px; height:18px; color:var(--cyan); vertical-align:middle; margin-right:8px;"></i> Correlation Detector Engaged</div>
|
|
1657
|
+
<div class="pov-desc">Computing Pearson/Spearman coefficients, identifying multi-variable dependencies, and mapping relationship strengths...</div>
|
|
1658
|
+
</div>
|
|
1659
|
+
<div class="pov-content">
|
|
1660
|
+
<svg class="pov-chords-svg" viewBox="0 0 160 100">
|
|
1661
|
+
<g class="pov-chords-rotate">
|
|
1662
|
+
<circle cx="80" cy="50" r="30" class="pov-chord-line" stroke-dasharray="2,2"></circle>
|
|
1663
|
+
<circle cx="80" cy="50" r="35" class="pov-chord-line active"></circle>
|
|
1664
|
+
|
|
1665
|
+
<circle cx="50" cy="30" r="4" class="pov-node"></circle>
|
|
1666
|
+
<circle cx="110" cy="30" r="4" class="pov-node"></circle>
|
|
1667
|
+
<circle cx="50" cy="70" r="4" class="pov-node"></circle>
|
|
1668
|
+
<circle cx="110" cy="70" r="4" class="pov-node"></circle>
|
|
1669
|
+
<circle cx="80" cy="15" r="4" class="pov-node"></circle>
|
|
1670
|
+
<circle cx="80" cy="85" r="4" class="pov-node"></circle>
|
|
1671
|
+
|
|
1672
|
+
<path d="M50 30 L110 70" class="pov-chord-line active" stroke-dasharray="none"></path>
|
|
1673
|
+
<path d="M110 30 L50 70" class="pov-chord-line" stroke-dasharray="4,4"></path>
|
|
1674
|
+
<path d="M80 15 L80 85" class="pov-chord-line active" stroke-dasharray="none"></path>
|
|
1675
|
+
</g>
|
|
1676
|
+
</svg>
|
|
1677
|
+
</div>`;
|
|
1678
|
+
|
|
1679
|
+
} else if (stage === 'insights') {
|
|
1680
|
+
els.stagePovPanel.innerHTML = `
|
|
1681
|
+
<div class="pov-header">
|
|
1682
|
+
<div class="pov-title"><i data-lucide="lightbulb" style="width:18px; height:18px; color:var(--amber); vertical-align:middle; margin-right:8px;"></i> Strategic Business Insights</div>
|
|
1683
|
+
<div class="pov-desc">Correlating discovered patterns to business implications and drafting actionable McKinsey-level recommendation strategies...</div>
|
|
1684
|
+
</div>
|
|
1685
|
+
<div class="pov-content">
|
|
1686
|
+
<div class="pov-insight-bulb">
|
|
1687
|
+
<svg class="pov-bulb-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
1688
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
|
|
1689
|
+
</svg>
|
|
1690
|
+
<div class="pov-insight-bullets">
|
|
1691
|
+
<div class="pov-bullet-text">Identifying key driver columns...</div>
|
|
1692
|
+
<div class="pov-bullet-text">Formulating risk matrices...</div>
|
|
1693
|
+
<div class="pov-bullet-text">Generating recommendations...</div>
|
|
1694
|
+
</div>
|
|
1695
|
+
</div>
|
|
1696
|
+
</div>`;
|
|
1697
|
+
|
|
1698
|
+
} else if (stage === 'visualization') {
|
|
1699
|
+
els.stagePovPanel.innerHTML = `
|
|
1700
|
+
<div class="pov-header">
|
|
1701
|
+
<div class="pov-title"><i data-lucide="image" style="width:18px; height:18px; color:var(--rose); vertical-align:middle; margin-right:8px;"></i> Visual Intelligence Compiler</div>
|
|
1702
|
+
<div class="pov-desc">Writing corporate Seaborn/Matplotlib visualization scripts and compiling high-resolution PNG plots...</div>
|
|
1703
|
+
</div>
|
|
1704
|
+
<div class="pov-content">
|
|
1705
|
+
<svg class="pov-compiler-svg" viewBox="0 0 150 90">
|
|
1706
|
+
<line x1="20" y1="10" x2="20" y2="80" class="pov-axis"></line>
|
|
1707
|
+
<line x1="20" y1="80" x2="140" y2="80" class="pov-axis"></line>
|
|
1708
|
+
|
|
1709
|
+
<path d="M20 70 Q 50 20, 80 50 T 140 15" class="pov-compiler-path"></path>
|
|
1710
|
+
|
|
1711
|
+
<circle cx="50" cy="38" r="3" class="pov-dot" style="animation-delay: 0.8s;"></circle>
|
|
1712
|
+
<circle cx="80" cy="50" r="3" class="pov-dot" style="animation-delay: 1.4s;"></circle>
|
|
1713
|
+
<circle cx="110" cy="28" r="3" class="pov-dot" style="animation-delay: 2s;"></circle>
|
|
1714
|
+
<circle cx="130" cy="20" r="3" class="pov-dot" style="animation-delay: 2.6s;"></circle>
|
|
1715
|
+
</svg>
|
|
1716
|
+
</div>`;
|
|
1717
|
+
|
|
1718
|
+
} else if (stage === 'plotly') {
|
|
1719
|
+
els.stagePovPanel.innerHTML = `
|
|
1720
|
+
<div class="pov-header">
|
|
1721
|
+
<div class="pov-title"><i data-lucide="bar-chart-3" style="width:18px; height:18px; color:var(--emerald); vertical-align:middle; margin-right:8px;"></i> Interactive Dashboard Builder</div>
|
|
1722
|
+
<div class="pov-desc">Building zoomable, hoverable Plotly structures and generating the final analytical results suite...</div>
|
|
1723
|
+
</div>
|
|
1724
|
+
<div class="pov-content">
|
|
1725
|
+
<div class="pov-plotly-grid">
|
|
1726
|
+
<div class="pov-skeleton-card">
|
|
1727
|
+
<div class="pov-skeleton-header"></div>
|
|
1728
|
+
<div class="pov-skeleton-body">
|
|
1729
|
+
<div class="pov-skeleton-bar"></div>
|
|
1730
|
+
<div class="pov-skeleton-bar"></div>
|
|
1731
|
+
<div class="pov-skeleton-bar"></div>
|
|
1732
|
+
</div>
|
|
1733
|
+
</div>
|
|
1734
|
+
<div class="pov-skeleton-card">
|
|
1735
|
+
<div class="pov-skeleton-header"></div>
|
|
1736
|
+
<div class="pov-skeleton-body">
|
|
1737
|
+
<div class="pov-skeleton-bar" style="animation-delay:0.2s"></div>
|
|
1738
|
+
<div class="pov-skeleton-bar" style="animation-delay:0.5s"></div>
|
|
1739
|
+
<div class="pov-skeleton-bar" style="animation-delay:0.8s"></div>
|
|
1740
|
+
</div>
|
|
1741
|
+
</div>
|
|
1742
|
+
<div class="pov-skeleton-card">
|
|
1743
|
+
<div class="pov-skeleton-header"></div>
|
|
1744
|
+
<div class="pov-skeleton-body">
|
|
1745
|
+
<div class="pov-skeleton-bar" style="animation-delay:0.4s"></div>
|
|
1746
|
+
<div class="pov-skeleton-bar" style="animation-delay:0.7s"></div>
|
|
1747
|
+
<div class="pov-skeleton-bar" style="animation-delay:1.0s"></div>
|
|
1748
|
+
</div>
|
|
1749
|
+
</div>
|
|
1750
|
+
</div>
|
|
1751
|
+
</div>`;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
if (window.lucide) {
|
|
1755
|
+
lucide.createIcons();
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
function markStage(stage, status) {
|
|
1760
|
+
const el = document.querySelector(`.stage-item[data-stage="${stage}"]`);
|
|
1761
|
+
if (el) {
|
|
1762
|
+
el.classList.remove('active','done');
|
|
1763
|
+
el.classList.add(status);
|
|
1764
|
+
}
|
|
1765
|
+
if (status === 'active') {
|
|
1766
|
+
renderStagePov(stage);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
let _activeStage = null;
|
|
1771
|
+
const STAGE_ORDER = ['cleaning', 'relations', 'insights', 'visualization', 'plotly'];
|
|
1772
|
+
|
|
1773
|
+
function updateProgressTrack(newStage) {
|
|
1774
|
+
const newIdx = STAGE_ORDER.indexOf(newStage);
|
|
1775
|
+
if (newIdx === -1) return;
|
|
1776
|
+
|
|
1777
|
+
const currentIdx = STAGE_ORDER.indexOf(_activeStage);
|
|
1778
|
+
|
|
1779
|
+
// We only progress forward. If newIdx is less than or equal to currentIdx, ignore.
|
|
1780
|
+
if (newIdx <= currentIdx) return;
|
|
1781
|
+
|
|
1782
|
+
// Mark all stages before newStage as done
|
|
1783
|
+
for (let i = 0; i < newIdx; i++) {
|
|
1784
|
+
markStage(STAGE_ORDER[i], 'done');
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// Mark newStage as active
|
|
1788
|
+
_activeStage = newStage;
|
|
1789
|
+
markStage(newStage, 'active');
|
|
1790
|
+
|
|
1791
|
+
// Update notification stage text
|
|
1792
|
+
const labels = {
|
|
1793
|
+
cleaning: 'Cleaning dataset...',
|
|
1794
|
+
relations: 'Mapping relationships...',
|
|
1795
|
+
insights: 'Generating BI insights...',
|
|
1796
|
+
visualization: 'Exporting charts...',
|
|
1797
|
+
plotly: 'Building Plotly charts...'
|
|
1798
|
+
};
|
|
1799
|
+
const text = labels[newStage] || 'Analysing...';
|
|
1800
|
+
const el = $('notifActiveJob');
|
|
1801
|
+
if (el) el.textContent = text;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function inferStageFromLog(line) {
|
|
1805
|
+
const ll = line.toLowerCase();
|
|
1806
|
+
|
|
1807
|
+
// First, check for explicit stage indicators from stdout!
|
|
1808
|
+
if (ll.includes('[stage 1/4]') || ll.includes('running data cleaner') || ll.includes('data clean')) {
|
|
1809
|
+
return 'cleaning';
|
|
1810
|
+
}
|
|
1811
|
+
if (ll.includes('[stage 2/4]') || ll.includes('running relation analyst') || ll.includes('relation analyst + bi analyst')) {
|
|
1812
|
+
return 'relations';
|
|
1813
|
+
}
|
|
1814
|
+
if (ll.includes('running bi analyst') || ll.includes('business intelligence analyst') || ll.includes('generating recommendations') || ll.includes('insights complete')) {
|
|
1815
|
+
return 'insights';
|
|
1816
|
+
}
|
|
1817
|
+
if (ll.includes('[stage 3/4]') || ll.includes('running data visualizer') || ll.includes('visualization complete')) {
|
|
1818
|
+
return 'visualization';
|
|
1819
|
+
}
|
|
1820
|
+
if (ll.includes('[stage 4/4]') || ll.includes('building interactive plotly charts') || ll.includes('plotly_charts') || ll.includes('interactive chart')) {
|
|
1821
|
+
return 'plotly';
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// Fallback to keywords but with strict boundary matching / ordering
|
|
1825
|
+
for (const stage of STAGE_ORDER) {
|
|
1826
|
+
const keywords = STAGE_KEYWORDS[stage];
|
|
1827
|
+
if (keywords.some(k => {
|
|
1828
|
+
if (k === 'chart' || k === 'plot') {
|
|
1829
|
+
return ll.includes(k) && !ll.includes('plotly');
|
|
1830
|
+
}
|
|
1831
|
+
return ll.includes(k);
|
|
1832
|
+
})) {
|
|
1833
|
+
return stage;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return null;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function startSSEStream(sessionId) {
|
|
1840
|
+
// Close any existing stream
|
|
1841
|
+
if (state.sseSource) { state.sseSource.close(); state.sseSource = null; }
|
|
1842
|
+
|
|
1843
|
+
// Reset stage indicators
|
|
1844
|
+
$$('.stage-item').forEach(el => el.classList.remove('active','done'));
|
|
1845
|
+
els.logOutput.innerHTML = '';
|
|
1846
|
+
_activeStage = null;
|
|
1847
|
+
|
|
1848
|
+
if (povInterval) { clearInterval(povInterval); povInterval = null; }
|
|
1849
|
+
if (els.stagePovPanel) {
|
|
1850
|
+
els.stagePovPanel.innerHTML = `
|
|
1851
|
+
<div class="pov-initial-state">
|
|
1852
|
+
<div class="pov-pulse-ring"></div>
|
|
1853
|
+
<p>Awaiting analysis stream...</p>
|
|
1854
|
+
</div>`;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Initialize and show the top-right notification toast
|
|
1858
|
+
const notif = $('analysisNotification');
|
|
1859
|
+
if (notif) {
|
|
1860
|
+
notif.classList.remove('hidden', 'complete');
|
|
1861
|
+
const notifTitle = notif.querySelector('.notif-title');
|
|
1862
|
+
if (notifTitle) notifTitle.textContent = 'Crewlyze Analysis Active';
|
|
1863
|
+
const activeJobEl = $('notifActiveJob');
|
|
1864
|
+
if (activeJobEl) activeJobEl.textContent = 'Initializing...';
|
|
1865
|
+
const timerEl = $('notifTimer');
|
|
1866
|
+
if (timerEl) timerEl.textContent = '00:00';
|
|
1867
|
+
const btnMax = $('btnMaximizeNotif');
|
|
1868
|
+
if (btnMax) {
|
|
1869
|
+
btnMax.textContent = 'View';
|
|
1870
|
+
btnMax.onclick = () => {
|
|
1871
|
+
showScreen('running');
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// Setup the elapsed timer
|
|
1877
|
+
if (window.notifTimerInterval) { clearInterval(window.notifTimerInterval); }
|
|
1878
|
+
window.notifElapsedSeconds = 0;
|
|
1879
|
+
window.notifTimerInterval = setInterval(() => {
|
|
1880
|
+
window.notifElapsedSeconds++;
|
|
1881
|
+
const mins = Math.floor(window.notifElapsedSeconds / 60).toString().padStart(2, '0');
|
|
1882
|
+
const secs = (window.notifElapsedSeconds % 60).toString().padStart(2, '0');
|
|
1883
|
+
const timerEl = $('notifTimer');
|
|
1884
|
+
if (timerEl) timerEl.textContent = `${mins}:${secs}`;
|
|
1885
|
+
}, 1000);
|
|
1886
|
+
|
|
1887
|
+
const src = new EventSource(`/api/analyze/stream?session_id=${sessionId}`);
|
|
1888
|
+
state.sseSource = src;
|
|
1889
|
+
|
|
1890
|
+
src.onmessage = e => {
|
|
1891
|
+
const line = e.data;
|
|
1892
|
+
|
|
1893
|
+
if (line === '[EOF]') {
|
|
1894
|
+
src.close();
|
|
1895
|
+
state.sseSource = null;
|
|
1896
|
+
// Mark all stages as done (green) on successful completion
|
|
1897
|
+
STAGE_ORDER.forEach(s => markStage(s, 'done'));
|
|
1898
|
+
|
|
1899
|
+
// Finalize the notification toast
|
|
1900
|
+
if (window.notifTimerInterval) { clearInterval(window.notifTimerInterval); window.notifTimerInterval = null; }
|
|
1901
|
+
if (notif) {
|
|
1902
|
+
notif.classList.add('complete');
|
|
1903
|
+
const notifTitle = notif.querySelector('.notif-title');
|
|
1904
|
+
if (notifTitle) notifTitle.textContent = 'Analysis Completed!';
|
|
1905
|
+
const activeJobEl = $('notifActiveJob');
|
|
1906
|
+
if (activeJobEl) activeJobEl.textContent = 'Results are ready.';
|
|
1907
|
+
const btnMax = $('btnMaximizeNotif');
|
|
1908
|
+
if (btnMax) {
|
|
1909
|
+
btnMax.textContent = 'View Results';
|
|
1910
|
+
btnMax.onclick = () => {
|
|
1911
|
+
notif.classList.add('hidden');
|
|
1912
|
+
loadResults(sessionId);
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
appendLog('Analysis complete! Loading results…');
|
|
1918
|
+
setStatus('● Loading…', 'running');
|
|
1919
|
+
setTimeout(() => loadResults(sessionId), 1500);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
appendLog(line);
|
|
1924
|
+
|
|
1925
|
+
// Infer stage transitions and update progress track
|
|
1926
|
+
const stage = inferStageFromLog(line);
|
|
1927
|
+
if (stage) {
|
|
1928
|
+
updateProgressTrack(stage);
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1931
|
+
|
|
1932
|
+
src.onerror = () => {
|
|
1933
|
+
src.close();
|
|
1934
|
+
state.sseSource = null;
|
|
1935
|
+
|
|
1936
|
+
// Stop the timer on error
|
|
1937
|
+
if (window.notifTimerInterval) { clearInterval(window.notifTimerInterval); window.notifTimerInterval = null; }
|
|
1938
|
+
if (notif) {
|
|
1939
|
+
notif.classList.add('complete');
|
|
1940
|
+
const notifTitle = notif.querySelector('.notif-title');
|
|
1941
|
+
if (notifTitle) notifTitle.textContent = 'Analysis Finished';
|
|
1942
|
+
const activeJobEl = $('notifActiveJob');
|
|
1943
|
+
if (activeJobEl) activeJobEl.textContent = 'Stream finalized.';
|
|
1944
|
+
const btnMax = $('btnMaximizeNotif');
|
|
1945
|
+
if (btnMax) {
|
|
1946
|
+
btnMax.textContent = 'View Results';
|
|
1947
|
+
btnMax.onclick = () => {
|
|
1948
|
+
notif.classList.add('hidden');
|
|
1949
|
+
loadResults(sessionId);
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// Try to load results anyway
|
|
1955
|
+
setTimeout(() => loadResults(sessionId), 2000);
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
els.clearLogBtn.addEventListener('click', () => { els.logOutput.innerHTML = ''; });
|
|
1960
|
+
|
|
1961
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1962
|
+
// Load & Render Results
|
|
1963
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1964
|
+
async function loadResults(sessionId, retryCount = 0) {
|
|
1965
|
+
// Check if we have cached results for this session
|
|
1966
|
+
if (state.resultsCache[sessionId]) {
|
|
1967
|
+
const data = state.resultsCache[sessionId];
|
|
1968
|
+
state.results = data;
|
|
1969
|
+
if (data.preview && data.preview.length) {
|
|
1970
|
+
state.columns = Object.keys(data.preview[0]);
|
|
1971
|
+
}
|
|
1972
|
+
if (state.activeProject) state.activeProject.status = 'completed';
|
|
1973
|
+
setStatus('● Complete', 'complete');
|
|
1974
|
+
if (els.agenticPlaceholder) els.agenticPlaceholder.classList.add('hidden');
|
|
1975
|
+
if (els.agenticTabsBar) els.agenticTabsBar.classList.remove('hidden');
|
|
1976
|
+
if (els.agenticTabPanels) els.agenticTabPanels.classList.remove('hidden');
|
|
1977
|
+
renderDashboard(data, sessionId);
|
|
1978
|
+
showScreen('results');
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
try {
|
|
1983
|
+
const res = await fetch(`/api/results?session_id=${sessionId}`);
|
|
1984
|
+
if (!res.ok) throw new Error('Results not ready');
|
|
1985
|
+
const data = await res.json();
|
|
1986
|
+
|
|
1987
|
+
if (data.ready === false) {
|
|
1988
|
+
throw new Error('Results pending');
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (data.error) {
|
|
1992
|
+
setStatus('● Error', 'error');
|
|
1993
|
+
toast('Analysis failed: ' + data.error, 'error');
|
|
1994
|
+
if (state.activeProject) {
|
|
1995
|
+
state.activeProject.status = 'failed';
|
|
1996
|
+
loadProjects();
|
|
1997
|
+
}
|
|
1998
|
+
showScreen('landing');
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Cache the retrieved results
|
|
2003
|
+
state.resultsCache[sessionId] = data;
|
|
2004
|
+
|
|
2005
|
+
const wasRunning = els.runningScreen.classList.contains('active');
|
|
2006
|
+
|
|
2007
|
+
state.results = data;
|
|
2008
|
+
|
|
2009
|
+
// Extract column info from preview
|
|
2010
|
+
if (data.preview && data.preview.length) {
|
|
2011
|
+
state.columns = Object.keys(data.preview[0]);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// Update project status
|
|
2015
|
+
if (state.activeProject) state.activeProject.status = 'completed';
|
|
2016
|
+
loadProjects();
|
|
2017
|
+
setStatus('● Complete', 'complete');
|
|
2018
|
+
|
|
2019
|
+
if (els.agenticPlaceholder) els.agenticPlaceholder.classList.add('hidden');
|
|
2020
|
+
if (els.agenticTabsBar) els.agenticTabsBar.classList.remove('hidden');
|
|
2021
|
+
if (els.agenticTabPanels) els.agenticTabPanels.classList.remove('hidden');
|
|
2022
|
+
|
|
2023
|
+
renderDashboard(data, sessionId);
|
|
2024
|
+
|
|
2025
|
+
if (wasRunning) {
|
|
2026
|
+
switchSection('agentic');
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
showScreen('results');
|
|
2030
|
+
} catch (e) {
|
|
2031
|
+
// Results not ready yet, retry up to 15 times (~37 seconds)
|
|
2032
|
+
if (retryCount < 15) {
|
|
2033
|
+
setTimeout(() => loadResults(sessionId, retryCount + 1), 2500);
|
|
2034
|
+
} else {
|
|
2035
|
+
setStatus('● Error', 'error');
|
|
2036
|
+
toast('Analysis failed: Server timed out producing results.', 'error');
|
|
2037
|
+
if (state.activeProject) {
|
|
2038
|
+
state.activeProject.status = 'failed';
|
|
2039
|
+
loadProjects();
|
|
2040
|
+
}
|
|
2041
|
+
showScreen('landing');
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
function renderDashboard(data, sessionId) {
|
|
2047
|
+
renderStats(data);
|
|
2048
|
+
renderPreview(data.preview || []);
|
|
2049
|
+
renderCleaning(data.cleaning_steps || '');
|
|
2050
|
+
renderRelations(data.relations || '');
|
|
2051
|
+
renderInsights(data.insights || '');
|
|
2052
|
+
renderCharts(data.plotly_charts || [], data.png_charts || [], sessionId);
|
|
2053
|
+
renderVizCode(data.code || '');
|
|
2054
|
+
setupExport(sessionId);
|
|
2055
|
+
resetChat();
|
|
2056
|
+
|
|
2057
|
+
// Set dimensions text in Chat Preview
|
|
2058
|
+
if (els.chatPreviewDims) {
|
|
2059
|
+
els.chatPreviewDims.textContent = `(${data.rows_count?.toLocaleString() || 0} rows × ${data.cols_count || 0} columns)`;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// Default to AI Chat section
|
|
2063
|
+
switchSection('chat');
|
|
2064
|
+
|
|
2065
|
+
// Activate first tab inside agentic area
|
|
2066
|
+
activateTab('cleaning');
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// ── Stats row ────────────────────────────────────────────────────────────────
|
|
2070
|
+
function renderStats(data) {
|
|
2071
|
+
const stats = [
|
|
2072
|
+
{ val: (data.rows_count || 0).toLocaleString(), lbl: 'Total Records', color: 'var(--violet)' },
|
|
2073
|
+
{ val: data.cols_count || 0, lbl: 'Total Columns', color: 'var(--cyan)' },
|
|
2074
|
+
{ val: data.numeric_count || 0, lbl: 'Numeric Fields', color: 'var(--emerald)' },
|
|
2075
|
+
{ val: data.cat_count || 0, lbl: 'Categorical Fields', color: 'var(--amber)' },
|
|
2076
|
+
{ val: (data.plotly_charts || []).length, lbl: 'Interactive Charts', color: 'var(--rose)' },
|
|
2077
|
+
];
|
|
2078
|
+
els.statsRow.innerHTML = stats.map(s => `
|
|
2079
|
+
<div class="stat-card" style="--accent:${s.color}">
|
|
2080
|
+
<div class="stat-val">${s.val}</div>
|
|
2081
|
+
<div class="stat-lbl">${s.lbl}</div>
|
|
2082
|
+
<div class="stat-accent-bar"></div>
|
|
2083
|
+
</div>
|
|
2084
|
+
`).join('');
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// ── Data Preview ─────────────────────────────────────────────────────────────
|
|
2088
|
+
function renderPreview(rows) {
|
|
2089
|
+
if (!rows.length) {
|
|
2090
|
+
els.previewTable.innerHTML = '<p style="color:var(--text-muted);padding:16px">No preview data.</p>';
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
const cols = Object.keys(rows[0]);
|
|
2094
|
+
state.columns = cols;
|
|
2095
|
+
|
|
2096
|
+
const thead = `<thead><tr>${cols.map(c => `<th>${c}</th>`).join('')}</tr></thead>`;
|
|
2097
|
+
const tbody = `<tbody>${rows.map(row =>
|
|
2098
|
+
`<tr>${cols.map(c => `<td title="${row[c] ?? ''}">${row[c] ?? ''}</td>`).join('')}</tr>`
|
|
2099
|
+
).join('')}</tbody>`;
|
|
2100
|
+
|
|
2101
|
+
els.previewTable.innerHTML = `<table class="data-table">${thead}${tbody}</table>`;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// Toggle preview minimize
|
|
2105
|
+
if (els.togglePreviewBtn) {
|
|
2106
|
+
els.togglePreviewBtn.addEventListener('click', () => {
|
|
2107
|
+
state.previewMinimized = !state.previewMinimized;
|
|
2108
|
+
if (state.previewMinimized) {
|
|
2109
|
+
els.previewTableWrap.innerHTML = `
|
|
2110
|
+
<div class="preview-minimized">
|
|
2111
|
+
Preview minimized — ${state.results?.rows_count?.toLocaleString() ?? '?'} rows × ${state.columns.length} columns.
|
|
2112
|
+
</div>`;
|
|
2113
|
+
els.togglePreviewBtn.textContent = '🔼 Expand Preview';
|
|
2114
|
+
} else {
|
|
2115
|
+
els.previewTableWrap.innerHTML = '<div id="previewTable" class="data-table-container"></div>';
|
|
2116
|
+
renderPreview(state.results?.preview || []);
|
|
2117
|
+
els.togglePreviewBtn.textContent = '🔽 Minimize';
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// ── Cleaning ─────────────────────────────────────────────────────────────────
|
|
2123
|
+
function renderCleaning(text) {
|
|
2124
|
+
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
|
|
2125
|
+
if (!lines.length) {
|
|
2126
|
+
els.cleaningContent.innerHTML = '<p style="color:var(--text-muted)">No cleaning operations recorded.</p>';
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
els.cleaningContent.innerHTML = lines.map(l =>
|
|
2130
|
+
`<div class="cleaning-item">
|
|
2131
|
+
<div class="cleaning-check" style="display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.2);"><i data-lucide="check" style="width: 12px; height: 12px; color: var(--emerald);"></i></div>
|
|
2132
|
+
<div class="cleaning-text" style="flex: 1;">${escHtml(l.replace(/^[-*•]\s*/, ''))}</div>
|
|
2133
|
+
</div>`
|
|
2134
|
+
).join('');
|
|
2135
|
+
|
|
2136
|
+
if (window.lucide) {
|
|
2137
|
+
lucide.createIcons();
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// ── Relations ─────────────────────────────────────────────────────────────────
|
|
2142
|
+
// ── Relations ─────────────────────────────────────────────────────────────────
|
|
2143
|
+
function renderRelations(text) {
|
|
2144
|
+
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
|
|
2145
|
+
|
|
2146
|
+
// Parse lines into structured state.relationsList
|
|
2147
|
+
state.relationsList = [];
|
|
2148
|
+
lines.forEach(line => {
|
|
2149
|
+
const xMatch = line.match(/X:\s*([^|]+)/i);
|
|
2150
|
+
const yMatch = line.match(/Y:\s*([^|]+)/i);
|
|
2151
|
+
const typeMatch = line.match(/Type:\s*([^|]+)/i);
|
|
2152
|
+
const detailsMatch = line.match(/Details:\s*(.+)/i);
|
|
2153
|
+
|
|
2154
|
+
if (xMatch && yMatch) {
|
|
2155
|
+
state.relationsList.push({
|
|
2156
|
+
xCol: xMatch[1].trim(),
|
|
2157
|
+
yCol: yMatch[1].trim(),
|
|
2158
|
+
typ: typeMatch ? typeMatch[1].trim() : 'Correlation',
|
|
2159
|
+
details: detailsMatch ? detailsMatch[1].trim() : 'Key relationship identified by analyst.'
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2164
|
+
renderRelationsListUI();
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function renderRelationsListUI() {
|
|
2168
|
+
const list = state.relationsList || [];
|
|
2169
|
+
if (!list.length) {
|
|
2170
|
+
els.relationsContent.innerHTML = `
|
|
2171
|
+
<div style="text-align: center; padding: 24px; color: var(--text-secondary); background: var(--bg-card); border-radius: var(--r-md); border: 1px dashed var(--border-mid); width: 100%;">
|
|
2172
|
+
<p style="margin: 0;">No schema relationships mapped yet.</p>
|
|
2173
|
+
</div>`;
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
let html = '<div class="relation-mapper-container">';
|
|
2178
|
+
list.forEach((rel, index) => {
|
|
2179
|
+
const xType = state.colTypes[rel.xCol] || 'Numeric';
|
|
2180
|
+
const yType = state.colTypes[rel.yCol] || 'Numeric';
|
|
2181
|
+
|
|
2182
|
+
html += `
|
|
2183
|
+
<div class="relation-card" style="position: relative; flex: 1 1 calc(50% - 16px); min-width: 380px; box-sizing: border-box;">
|
|
2184
|
+
<div class="relation-node">
|
|
2185
|
+
<span class="relation-node-type">${escHtml(xType)}</span>
|
|
2186
|
+
<strong>${escHtml(rel.xCol)}</strong>
|
|
2187
|
+
</div>
|
|
2188
|
+
|
|
2189
|
+
<div class="relation-connector">
|
|
2190
|
+
<span style="font-size: 0.8rem; font-weight: 600; color: var(--cyan); margin-bottom: 4px;">${escHtml(rel.typ)}</span>
|
|
2191
|
+
<div class="relation-line"></div>
|
|
2192
|
+
<span class="relation-info">${escHtml(rel.details)}</span>
|
|
2193
|
+
</div>
|
|
2194
|
+
|
|
2195
|
+
<div class="relation-node" style="border-color: rgba(34, 211, 238, 0.2);">
|
|
2196
|
+
<span class="relation-node-type" style="color: var(--cyan);">${escHtml(yType)}</span>
|
|
2197
|
+
<strong>${escHtml(rel.yCol)}</strong>
|
|
2198
|
+
</div>
|
|
2199
|
+
</div>
|
|
2200
|
+
`;
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
html += '</div>';
|
|
2204
|
+
els.relationsContent.innerHTML = html;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function showRelationModal(index) {
|
|
2208
|
+
const cols = state.columns || [];
|
|
2209
|
+
els.relationModalXSelect.innerHTML = cols.map(c => `<option value="${escHtml(c)}">${escHtml(c)}</option>`).join('');
|
|
2210
|
+
els.relationModalYSelect.innerHTML = cols.map(c => `<option value="${escHtml(c)}">${escHtml(c)}</option>`).join('');
|
|
2211
|
+
|
|
2212
|
+
if (index !== null && state.relationsList[index]) {
|
|
2213
|
+
const rel = state.relationsList[index];
|
|
2214
|
+
els.relationModalTitle.innerHTML = '<i data-lucide="edit" style="width: 16px; height: 16px; vertical-align: middle; margin-right: 6px;"></i> Edit Relationship Schema';
|
|
2215
|
+
els.relationModalXSelect.value = rel.xCol;
|
|
2216
|
+
els.relationModalYSelect.value = rel.yCol;
|
|
2217
|
+
els.relationModalTypeSelect.value = rel.typ;
|
|
2218
|
+
els.relationModalDetails.value = rel.details;
|
|
2219
|
+
} else {
|
|
2220
|
+
els.relationModalTitle.innerHTML = '<i data-lucide="plus" style="width: 16px; height: 16px; vertical-align: middle; margin-right: 6px;"></i> Add Custom Relationship';
|
|
2221
|
+
if (cols.length > 1) {
|
|
2222
|
+
els.relationModalXSelect.selectedIndex = 0;
|
|
2223
|
+
els.relationModalYSelect.selectedIndex = 1;
|
|
2224
|
+
}
|
|
2225
|
+
els.relationModalTypeSelect.value = 'Scatter Plot';
|
|
2226
|
+
els.relationModalDetails.value = '';
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
els.relationModal.classList.remove('hidden');
|
|
2230
|
+
|
|
2231
|
+
els.relationModalConfirmBtn.onclick = () => {
|
|
2232
|
+
const relObj = {
|
|
2233
|
+
xCol: els.relationModalXSelect.value,
|
|
2234
|
+
yCol: els.relationModalYSelect.value,
|
|
2235
|
+
typ: els.relationModalTypeSelect.value,
|
|
2236
|
+
details: els.relationModalDetails.value.trim() || 'Custom correlation defined by data scientist.'
|
|
2237
|
+
};
|
|
2238
|
+
|
|
2239
|
+
if (index !== null) {
|
|
2240
|
+
state.relationsList[index] = relObj;
|
|
2241
|
+
} else {
|
|
2242
|
+
state.relationsList.push(relObj);
|
|
2243
|
+
}
|
|
2244
|
+
els.relationModal.classList.add('hidden');
|
|
2245
|
+
renderRelationsListUI();
|
|
2246
|
+
};
|
|
2247
|
+
|
|
2248
|
+
els.relationModalCancelBtn.onclick = () => els.relationModal.classList.add('hidden');
|
|
2249
|
+
els.closeRelationModalBtn.onclick = () => els.relationModal.classList.add('hidden');
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function deleteRelationAt(index) {
|
|
2253
|
+
state.relationsList.splice(index, 1);
|
|
2254
|
+
renderRelationsListUI();
|
|
2255
|
+
toast('Relationship removed from schema configuration.', 'info');
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
async function saveTweakedRelations() {
|
|
2259
|
+
const sessionId = state.activeProject?.id || state.uploadedSession;
|
|
2260
|
+
if (!sessionId) { toast('No active project context.', 'error'); return; }
|
|
2261
|
+
|
|
2262
|
+
const textLines = state.relationsList.map(r =>
|
|
2263
|
+
`- X: ${r.xCol} | Y: ${r.yCol} | Type: ${r.typ} | Details: ${r.details}`
|
|
2264
|
+
).join('\n');
|
|
2265
|
+
|
|
2266
|
+
try {
|
|
2267
|
+
const fd = new FormData();
|
|
2268
|
+
fd.append('relations_text', textLines);
|
|
2269
|
+
|
|
2270
|
+
const res = await fetch(`/api/projects/${sessionId}/tweak-relations`, {
|
|
2271
|
+
method: 'POST',
|
|
2272
|
+
body: fd
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
if (res.ok) {
|
|
2276
|
+
toast('Schema relationships committed successfully!', 'success');
|
|
2277
|
+
if (state.results) {
|
|
2278
|
+
state.results.relations = textLines;
|
|
2279
|
+
}
|
|
2280
|
+
} else {
|
|
2281
|
+
toast('Failed to save schema tweaks.', 'error');
|
|
2282
|
+
}
|
|
2283
|
+
} catch (e) {
|
|
2284
|
+
toast('Error saving schema tweaks: ' + e.message, 'error');
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// ── Insights Markdown Formatter ──────────────────────────────────────────────
|
|
2289
|
+
function formatInlineMarkdown(content) {
|
|
2290
|
+
let isBullet = false;
|
|
2291
|
+
let cleanContent = content;
|
|
2292
|
+
if (/^[-*•]\s+/.test(content)) {
|
|
2293
|
+
isBullet = true;
|
|
2294
|
+
cleanContent = content.replace(/^[-*•]\s+/, '');
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
let formatted = escHtml(cleanContent)
|
|
2298
|
+
.replace(/```([\s\S]*?)```/g, '<pre>$1</pre>')
|
|
2299
|
+
.replace(/`([^`]+)`/g, '<code style="background:var(--bg-surface);padding:1px 5px;border-radius:3px;font-family:var(--font-mono);font-size:0.85em">$1</code>')
|
|
2300
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
2301
|
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
2302
|
+
|
|
2303
|
+
if (isBullet) {
|
|
2304
|
+
return `<div style="display: flex; gap: 8px; margin-left: 12px; margin-top: 4px; margin-bottom: 4px;">
|
|
2305
|
+
<span>•</span>
|
|
2306
|
+
<div>${formatted}</div>
|
|
2307
|
+
</div>`;
|
|
2308
|
+
}
|
|
2309
|
+
return formatted;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
// ── Insights ─────────────────────────────────────────────────────────────────
|
|
2313
|
+
function renderInsights(text) {
|
|
2314
|
+
if (!text || !text.trim() || text.toLowerCase().includes('skipped')) {
|
|
2315
|
+
els.insightsContent.innerHTML = '<p style="color:var(--text-muted)">No business insights generated.</p>';
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// Parse sections
|
|
2320
|
+
let objectivesText = "";
|
|
2321
|
+
let statsText = "";
|
|
2322
|
+
let strategicText = "";
|
|
2323
|
+
let warningsText = "";
|
|
2324
|
+
|
|
2325
|
+
const sections = text.split(/###\s+/);
|
|
2326
|
+
sections.forEach(sec => {
|
|
2327
|
+
const lines = sec.split('\n');
|
|
2328
|
+
if (lines.length === 0) return;
|
|
2329
|
+
const header = lines[0].trim().toLowerCase();
|
|
2330
|
+
const content = lines.slice(1).join('\n').trim();
|
|
2331
|
+
|
|
2332
|
+
if (header.includes('objective') || header.includes('goal')) {
|
|
2333
|
+
objectivesText = content;
|
|
2334
|
+
} else if (header.includes('stat')) {
|
|
2335
|
+
statsText = content;
|
|
2336
|
+
} else if (header.includes('insight')) {
|
|
2337
|
+
strategicText = content;
|
|
2338
|
+
} else if (header.includes('warning') || header.includes('alert')) {
|
|
2339
|
+
warningsText = content;
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
// If parsing didn't find specific sections, fallback to parsing as a single block
|
|
2344
|
+
if (!strategicText && !objectivesText) {
|
|
2345
|
+
strategicText = text;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
let html = "";
|
|
2349
|
+
|
|
2350
|
+
// 1. Objectives & Goals Banner
|
|
2351
|
+
if (objectivesText) {
|
|
2352
|
+
html += `
|
|
2353
|
+
<div class="insight-header-card">
|
|
2354
|
+
<div class="insight-header-title"><i data-lucide="target" style="width: 18px; height: 18px; vertical-align: middle; margin-right: 6px; color: var(--violet-light);"></i> Primary Objectives & Goals</div>
|
|
2355
|
+
<div class="insight-header-text">${formatInsightsSubsections(objectivesText)}</div>
|
|
2356
|
+
</div>
|
|
2357
|
+
`;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// 2. Dataset Statistics Grid
|
|
2361
|
+
if (statsText) {
|
|
2362
|
+
const lines = statsText.split('\n').map(l => l.trim()).filter(Boolean);
|
|
2363
|
+
let summaryStats = [];
|
|
2364
|
+
let numericCols = [];
|
|
2365
|
+
let categoricalCols = [];
|
|
2366
|
+
|
|
2367
|
+
let currentSection = "";
|
|
2368
|
+
|
|
2369
|
+
lines.forEach(line => {
|
|
2370
|
+
const cleanLine = line.replace(/^[-*•]\s*/, '').trim();
|
|
2371
|
+
const lowerLine = cleanLine.toLowerCase();
|
|
2372
|
+
|
|
2373
|
+
if (lowerLine.startsWith("numeric column")) {
|
|
2374
|
+
currentSection = "numeric";
|
|
2375
|
+
return;
|
|
2376
|
+
} else if (lowerLine.startsWith("categorical column")) {
|
|
2377
|
+
currentSection = "categorical";
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const colIndex = cleanLine.indexOf(':');
|
|
2382
|
+
if (colIndex > -1) {
|
|
2383
|
+
const key = cleanLine.slice(0, colIndex).trim();
|
|
2384
|
+
const val = cleanLine.slice(colIndex + 1).trim();
|
|
2385
|
+
|
|
2386
|
+
if (lowerLine.startsWith("total row") || lowerLine.startsWith("total record") || lowerLine.startsWith("total col")) {
|
|
2387
|
+
summaryStats.push({ key, val });
|
|
2388
|
+
} else {
|
|
2389
|
+
if (currentSection === "numeric") {
|
|
2390
|
+
numericCols.push({ col: key, stats: val });
|
|
2391
|
+
} else if (currentSection === "categorical") {
|
|
2392
|
+
categoricalCols.push({ col: key, stats: val });
|
|
2393
|
+
} else {
|
|
2394
|
+
if (lowerLine.includes("min") || lowerLine.includes("max") || lowerLine.includes("mean")) {
|
|
2395
|
+
numericCols.push({ col: key, stats: val });
|
|
2396
|
+
} else {
|
|
2397
|
+
categoricalCols.push({ col: key, stats: val });
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
} else {
|
|
2402
|
+
if (cleanLine) {
|
|
2403
|
+
if (currentSection === "numeric") {
|
|
2404
|
+
numericCols.push({ col: cleanLine, stats: "" });
|
|
2405
|
+
} else if (currentSection === "categorical") {
|
|
2406
|
+
categoricalCols.push({ col: cleanLine, stats: "" });
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
// A. Summary Cards
|
|
2413
|
+
if (summaryStats.length > 0) {
|
|
2414
|
+
html += `<div class="insight-stats-grid" style="margin-bottom: 20px;">`;
|
|
2415
|
+
summaryStats.forEach(s => {
|
|
2416
|
+
html += `
|
|
2417
|
+
<div class="insight-stat-card">
|
|
2418
|
+
<div class="insight-stat-val">${escHtml(s.val)}</div>
|
|
2419
|
+
<div class="insight-stat-lbl">${escHtml(s.key)}</div>
|
|
2420
|
+
</div>
|
|
2421
|
+
`;
|
|
2422
|
+
});
|
|
2423
|
+
html += `</div>`;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
// B. Side-by-Side Sections
|
|
2427
|
+
if (numericCols.length > 0 || categoricalCols.length > 0) {
|
|
2428
|
+
html += `
|
|
2429
|
+
<div style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 24px;">
|
|
2430
|
+
<!-- Numeric Columns -->
|
|
2431
|
+
<div class="card" style="flex: 1 1 calc(50% - 10px); min-width: 320px; padding: 18px; background: var(--bg-card); border: 1px solid var(--border-mid); box-sizing: border-box;">
|
|
2432
|
+
<h4 style="margin: 0 0 12px 0; color: var(--cyan); font-size: 0.95rem; display: flex; align-items: center; gap: 6px;">
|
|
2433
|
+
<i data-lucide="binary" style="width: 16px; height: 16px;"></i> Numeric Columns & Distributions
|
|
2434
|
+
</h4>
|
|
2435
|
+
<div style="display: flex; flex-direction: column; gap: 10px;">
|
|
2436
|
+
`;
|
|
2437
|
+
if (numericCols.length > 0) {
|
|
2438
|
+
numericCols.forEach(c => {
|
|
2439
|
+
html += `
|
|
2440
|
+
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed var(--border-low); padding-bottom: 8px; font-size: 0.85rem;">
|
|
2441
|
+
<strong style="color: #fff;">${escHtml(c.col)}</strong>
|
|
2442
|
+
<span style="color: var(--text-secondary); font-family: var(--font-mono); font-size: 0.8rem; text-align: right;">${escHtml(c.stats)}</span>
|
|
2443
|
+
</div>
|
|
2444
|
+
`;
|
|
2445
|
+
});
|
|
2446
|
+
} else {
|
|
2447
|
+
html += `<p style="color: var(--text-muted); font-size: 0.85rem; margin: 0;">None detected.</p>`;
|
|
2448
|
+
}
|
|
2449
|
+
html += `
|
|
2450
|
+
</div>
|
|
2451
|
+
</div>
|
|
2452
|
+
|
|
2453
|
+
<!-- Categorical Columns -->
|
|
2454
|
+
<div class="card" style="flex: 1 1 calc(50% - 10px); min-width: 320px; padding: 18px; background: var(--bg-card); border: 1px solid var(--border-mid); box-sizing: border-box;">
|
|
2455
|
+
<h4 style="margin: 0 0 12px 0; color: var(--amber); font-size: 0.95rem; display: flex; align-items: center; gap: 6px;">
|
|
2456
|
+
<i data-lucide="tags" style="width: 16px; height: 16px;"></i> Categorical Columns
|
|
2457
|
+
</h4>
|
|
2458
|
+
<div style="display: flex; flex-direction: column; gap: 10px;">
|
|
2459
|
+
`;
|
|
2460
|
+
if (categoricalCols.length > 0) {
|
|
2461
|
+
categoricalCols.forEach(c => {
|
|
2462
|
+
const statsLabel = c.stats ? `: ${c.stats}` : '';
|
|
2463
|
+
html += `
|
|
2464
|
+
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed var(--border-low); padding-bottom: 8px; font-size: 0.85rem;">
|
|
2465
|
+
<strong style="color: #fff;">${escHtml(c.col)}</strong>
|
|
2466
|
+
<span style="color: var(--text-secondary); font-family: var(--font-mono); font-size: 0.8rem;">${escHtml(statsLabel)}</span>
|
|
2467
|
+
</div>
|
|
2468
|
+
`;
|
|
2469
|
+
});
|
|
2470
|
+
} else {
|
|
2471
|
+
html += `<p style="color: var(--text-muted); font-size: 0.85rem; margin: 0;">None detected.</p>`;
|
|
2472
|
+
}
|
|
2473
|
+
html += `
|
|
2474
|
+
</div>
|
|
2475
|
+
</div>
|
|
2476
|
+
</div>
|
|
2477
|
+
`;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// 3. Strategic Insights List
|
|
2482
|
+
if (strategicText) {
|
|
2483
|
+
html += `<h4 style="margin-bottom: 12px; color: var(--violet-light); font-size: 1.05rem; display: flex; align-items: center; gap: 6px;"><i data-lucide="lightbulb" style="width: 18px; height: 18px; color: var(--violet-light);"></i> Strategic Business Insights</h4>`;
|
|
2484
|
+
const numberedSplit = strategicText.split(/\n(?=\s*(?:\*{0,2}\d+[.):]\*{0,2}|#{1,3}\s))/);
|
|
2485
|
+
let items = [];
|
|
2486
|
+
if (numberedSplit.length > 1) {
|
|
2487
|
+
items = numberedSplit.map(s => s.replace(/^\s*(?:\*{0,2}\d+[.):]\*{0,2}|#{1,3})\s*/, '').trim()).filter(Boolean);
|
|
2488
|
+
} else {
|
|
2489
|
+
items = strategicText.split(/\n{2,}/).map(s => s.trim()).filter(Boolean);
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
const LABEL_CFG = [
|
|
2493
|
+
{ re: /^(?:observation|finding|pattern)[s]?[:]/i, cls: 'obs', icon: 'search', label: 'Observation' },
|
|
2494
|
+
{ re: /^(?:business\s+)?implication[s]?[:]/i, cls: 'impl', icon: 'briefcase', label: 'Implication' },
|
|
2495
|
+
{ re: /^(?:actionable\s+)?strateg(?:y|ies)[:]/i, cls: 'strat', icon: 'rocket', label: 'Strategy' },
|
|
2496
|
+
{ re: /^(?:recommendation|action|next\s+step)[s]?[:]/i, cls: 'strat', icon: 'check-circle', label: 'Recommendation' },
|
|
2497
|
+
{ re: /^(?:risk|concern|warning)[s]?[:]/i, cls: 'impl', icon: 'alert-triangle', label: 'Risk' },
|
|
2498
|
+
{ re: /^(?:kpi|metric|measure)[s]?[:]/i, cls: 'obs', icon: 'trending-up', label: 'Metric' },
|
|
2499
|
+
];
|
|
2500
|
+
|
|
2501
|
+
html += items.map((item, i) => {
|
|
2502
|
+
const parts = item.split('\n').map(p => p.trim()).filter(Boolean);
|
|
2503
|
+
const sectionHtml = parts.map(p => {
|
|
2504
|
+
for (const cfg of LABEL_CFG) {
|
|
2505
|
+
if (cfg.re.test(p)) {
|
|
2506
|
+
const body = p.replace(cfg.re, '').trim();
|
|
2507
|
+
return `<div class="insight-section">
|
|
2508
|
+
<div class="insight-section-label ${cfg.cls}"><i data-lucide="${cfg.icon}" style="width: 14px; height: 14px; vertical-align: middle; margin-right: 4px;"></i> ${cfg.label}</div>
|
|
2509
|
+
<div class="insight-section-text">${formatInlineMarkdown(body || p)}</div>
|
|
2510
|
+
</div>`;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
if (/^[-*•]/.test(p)) {
|
|
2514
|
+
return `<div class="insight-bullet">${formatInlineMarkdown(p)}</div>`;
|
|
2515
|
+
}
|
|
2516
|
+
if (/^#{1,3}\s/.test(p)) {
|
|
2517
|
+
return `<div class="insight-sub-heading">${escHtml(p.replace(/^#+\s*/, ''))}</div>`;
|
|
2518
|
+
}
|
|
2519
|
+
return `<div class="insight-section-text">${formatInlineMarkdown(p)}</div>`;
|
|
2520
|
+
}).join('');
|
|
2521
|
+
|
|
2522
|
+
return `
|
|
2523
|
+
<div class="insight-card">
|
|
2524
|
+
<div class="insight-num-pill">${i + 1}</div>
|
|
2525
|
+
<div class="insight-body">${sectionHtml}</div>
|
|
2526
|
+
</div>
|
|
2527
|
+
`;
|
|
2528
|
+
}).join('');
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// 4. Warnings & Alerts
|
|
2532
|
+
if (warningsText && !warningsText.toLowerCase().includes('no warnings') && !warningsText.toLowerCase().includes('none')) {
|
|
2533
|
+
html += `
|
|
2534
|
+
<div class="insight-warning-card">
|
|
2535
|
+
<div class="insight-warning-card-icon"><i data-lucide="alert-triangle" style="width: 24px; height: 24px; color: var(--amber);"></i></div>
|
|
2536
|
+
<div class="insight-warning-card-body">
|
|
2537
|
+
<div class="insight-warning-card-title">Business Risks & Data Alerts</div>
|
|
2538
|
+
<div class="insight-warning-card-text">${formatInsightsSubsections(warningsText)}</div>
|
|
2539
|
+
</div>
|
|
2540
|
+
</div>
|
|
2541
|
+
`;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
els.insightsContent.innerHTML = html;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function formatInsightsSubsections(text) {
|
|
2548
|
+
return text.split('\n').map(line => {
|
|
2549
|
+
const trimmed = line.trim();
|
|
2550
|
+
if (trimmed.startsWith('-') || trimmed.startsWith('*')) {
|
|
2551
|
+
return `<div style="margin-left: 12px; margin-top: 4px; display: flex; gap: 6px;">
|
|
2552
|
+
<span>•</span>
|
|
2553
|
+
<span>${formatInlineMarkdown(trimmed.replace(/^[-*•]\s*/, ''))}</span>
|
|
2554
|
+
</div>`;
|
|
2555
|
+
}
|
|
2556
|
+
return `<p style="margin-top: 4px; margin-bottom: 4px;">${formatInlineMarkdown(trimmed)}</p>`;
|
|
2557
|
+
}).join('');
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// ── Charts ────────────────────────────────────────────────────────────────────
|
|
2561
|
+
function renderCharts(plotlyCharts, pngCharts, sessionId) {
|
|
2562
|
+
els.plotlyChartsWrap.innerHTML = '';
|
|
2563
|
+
|
|
2564
|
+
if (plotlyCharts.length) {
|
|
2565
|
+
plotlyCharts.forEach((chart, idx) => {
|
|
2566
|
+
const card = document.createElement('div');
|
|
2567
|
+
card.className = 'chart-card';
|
|
2568
|
+
|
|
2569
|
+
const typeLabel = chart.fig_json?.data?.[0]?.type || 'chart';
|
|
2570
|
+
card.innerHTML = `
|
|
2571
|
+
<div class="chart-card-header">
|
|
2572
|
+
<div class="chart-card-title">${escHtml(chart.title)}</div>
|
|
2573
|
+
<div class="chart-card-type">${typeLabel}</div>
|
|
2574
|
+
</div>
|
|
2575
|
+
<div class="chart-card-body">
|
|
2576
|
+
<div id="plotly_chart_${idx}" style="width:100%;height:380px;"></div>
|
|
2577
|
+
</div>`;
|
|
2578
|
+
els.plotlyChartsWrap.appendChild(card);
|
|
2579
|
+
|
|
2580
|
+
try {
|
|
2581
|
+
const figData = chart.fig_json.data || [];
|
|
2582
|
+
const figLayout = chart.fig_json.layout || {};
|
|
2583
|
+
// Force dark theme
|
|
2584
|
+
figLayout.paper_bgcolor = 'rgba(9,9,11,0)';
|
|
2585
|
+
figLayout.plot_bgcolor = 'rgba(15,23,42,0.4)';
|
|
2586
|
+
figLayout.font = { color: '#e2e8f0', family: 'Inter, sans-serif' };
|
|
2587
|
+
figLayout.margin = figLayout.margin || { l:50, r:20, t:50, b:50 };
|
|
2588
|
+
|
|
2589
|
+
Plotly.newPlot(`plotly_chart_${idx}`, figData, figLayout, {
|
|
2590
|
+
responsive: true,
|
|
2591
|
+
displayModeBar: true,
|
|
2592
|
+
modeBarButtonsToRemove: ['toImage','sendDataToCloud'],
|
|
2593
|
+
displaylogo: false,
|
|
2594
|
+
});
|
|
2595
|
+
} catch (e) {
|
|
2596
|
+
card.querySelector('.chart-card-body').innerHTML =
|
|
2597
|
+
`<div class="chart-card-error">⚠ Could not render chart: ${escHtml(String(e))}</div>`;
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
} else {
|
|
2601
|
+
els.plotlyChartsWrap.innerHTML =
|
|
2602
|
+
'<p style="color:var(--text-muted);padding:8px 0">No interactive charts available. Run with Relationship Analysis enabled.</p>';
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// PNG charts — masonry-style grid with error fallback
|
|
2606
|
+
if (pngCharts.length) {
|
|
2607
|
+
els.pngChartsWrap.classList.remove('hidden');
|
|
2608
|
+
els.pngCharts.innerHTML = pngCharts.map((name, idx) => {
|
|
2609
|
+
const title = name.replace(/\.png$/i, '')
|
|
2610
|
+
.replace(/_/g, ' ')
|
|
2611
|
+
.replace(/\brelation\b/g, '')
|
|
2612
|
+
.replace(/^[a-z]/, c => c.toUpperCase())
|
|
2613
|
+
.trim();
|
|
2614
|
+
return `
|
|
2615
|
+
<div class="chart-card png-chart-card">
|
|
2616
|
+
<div class="chart-card-header">
|
|
2617
|
+
<div class="chart-card-title">${escHtml(title)}</div>
|
|
2618
|
+
<div class="chart-card-type">agent chart</div>
|
|
2619
|
+
</div>
|
|
2620
|
+
<div class="chart-card-body png-chart-body">
|
|
2621
|
+
<img
|
|
2622
|
+
src="/api/charts/${sessionId}/${encodeURIComponent(name)}"
|
|
2623
|
+
alt="${escHtml(title)}"
|
|
2624
|
+
class="png-chart-img"
|
|
2625
|
+
loading="lazy"
|
|
2626
|
+
onerror="this.parentNode.innerHTML='<div class=\'chart-img-error\'>Chart not available yet — rerun analysis to generate.</div>'"
|
|
2627
|
+
/>
|
|
2628
|
+
</div>
|
|
2629
|
+
</div>`;
|
|
2630
|
+
}).join('');
|
|
2631
|
+
} else {
|
|
2632
|
+
els.pngChartsWrap.classList.add('hidden');
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// ── Viz code ─────────────────────────────────────────────────────────────────
|
|
2637
|
+
function renderVizCode(code) {
|
|
2638
|
+
els.vizCodeDetails.classList.add('hidden');
|
|
2639
|
+
if (code && code.trim()) {
|
|
2640
|
+
els.vizCodeBlock.textContent = code;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
2645
|
+
function setupExport(sessionId) {
|
|
2646
|
+
const exportPdf = () => {
|
|
2647
|
+
window.location = `/api/export-pdf?session_id=${sessionId}`;
|
|
2648
|
+
};
|
|
2649
|
+
const exportZip = () => {
|
|
2650
|
+
window.location = `/api/projects/${sessionId}/export-zip`;
|
|
2651
|
+
};
|
|
2652
|
+
const downloadCsv = () => {
|
|
2653
|
+
window.location = `/api/projects/${sessionId}/download-csv`;
|
|
2654
|
+
};
|
|
2655
|
+
const reRun = () => {
|
|
2656
|
+
state.uploadedSession = sessionId;
|
|
2657
|
+
state.uploadedFile = { name: state.activeProject?.filename || 'dataset.csv' };
|
|
2658
|
+
openConfigModal();
|
|
2659
|
+
};
|
|
2660
|
+
|
|
2661
|
+
els.exportPdfBtn.onclick = exportPdf;
|
|
2662
|
+
if (els.sidebarExportPdfBtn) els.sidebarExportPdfBtn.onclick = exportPdf;
|
|
2663
|
+
|
|
2664
|
+
if (els.exportZipBtn) els.exportZipBtn.onclick = exportZip;
|
|
2665
|
+
if (els.sidebarExportZipBtn) els.sidebarExportZipBtn.onclick = exportZip;
|
|
2666
|
+
|
|
2667
|
+
els.downloadCsvBtn.onclick = downloadCsv;
|
|
2668
|
+
if (els.sidebarDownloadCsvBtn) els.sidebarDownloadCsvBtn.onclick = downloadCsv;
|
|
2669
|
+
|
|
2670
|
+
els.reRunBtn.onclick = reRun;
|
|
2671
|
+
if (els.sidebarReRunBtn) els.sidebarReRunBtn.onclick = reRun;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2675
|
+
// Tabs
|
|
2676
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2677
|
+
function activateTab(name) {
|
|
2678
|
+
els.tabBtns.forEach(btn => btn.classList.toggle('active', btn.dataset.tab === name));
|
|
2679
|
+
els.tabPanels.forEach(panel => panel.classList.toggle('active', panel.id === `panel-${name}`));
|
|
2680
|
+
if (name === 'charts') {
|
|
2681
|
+
setTimeout(() => {
|
|
2682
|
+
const chartElements = document.querySelectorAll('[id^="plotly_chart_"]');
|
|
2683
|
+
chartElements.forEach(el => Plotly.Plots?.resize?.(el));
|
|
2684
|
+
}, 100);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
els.tabBtns.forEach(btn => {
|
|
2688
|
+
btn.addEventListener('click', () => activateTab(btn.dataset.tab));
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
// Debounced Window Resize Plotly Listener
|
|
2692
|
+
let resizeTimeout = null;
|
|
2693
|
+
window.addEventListener('resize', () => {
|
|
2694
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
2695
|
+
resizeTimeout = setTimeout(() => {
|
|
2696
|
+
const activeTabBtn = document.querySelector('.tab-btn.active');
|
|
2697
|
+
if (activeTabBtn && activeTabBtn.dataset.tab === 'charts') {
|
|
2698
|
+
const chartElements = document.querySelectorAll('[id^="plotly_chart_"]');
|
|
2699
|
+
chartElements.forEach(el => Plotly.Plots?.resize?.(el));
|
|
2700
|
+
}
|
|
2701
|
+
}, 200);
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2705
|
+
// AI Copilot Chat + /column picker
|
|
2706
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2707
|
+
function resetChat() {
|
|
2708
|
+
state.chatHistory = [];
|
|
2709
|
+
els.chatMessages.innerHTML = '';
|
|
2710
|
+
// Seed with a welcome message
|
|
2711
|
+
appendChatMsg('assistant', `Hi! I'm your AI Data Copilot. Ask me anything about your dataset — aggregations, trends, plots, or specific columns.\n\nType **/** to insert a column name directly.`);
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
function appendChatMsg(role, content, plotUrl = null) {
|
|
2715
|
+
const div = document.createElement('div');
|
|
2716
|
+
div.className = `chat-msg ${role}`;
|
|
2717
|
+
const avatar = role === 'user'
|
|
2718
|
+
? '<i data-lucide="user" style="width: 16px; height: 16px;"></i>'
|
|
2719
|
+
: '<i data-lucide="bot" style="width: 16px; height: 16px;"></i>';
|
|
2720
|
+
|
|
2721
|
+
let formatted = '';
|
|
2722
|
+
// Try to use marked.js, fallback if CDN fails
|
|
2723
|
+
if (typeof marked !== 'undefined') {
|
|
2724
|
+
formatted = marked.parse(content);
|
|
2725
|
+
} else {
|
|
2726
|
+
formatted = escHtml(content)
|
|
2727
|
+
.replace(/```([\s\S]*?)```/g, '<pre>$1</pre>')
|
|
2728
|
+
.replace(/`([^`]+)`/g, '<code style="background:var(--bg-surface);padding:1px 5px;border-radius:3px;font-family:var(--font-mono);font-size:0.85em">$1</code>')
|
|
2729
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
2730
|
+
.replace(/\n/g, '<br>');
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
div.innerHTML = `
|
|
2734
|
+
<div class="chat-avatar">${avatar}</div>
|
|
2735
|
+
<div class="chat-bubble markdown-body">
|
|
2736
|
+
${formatted}
|
|
2737
|
+
${plotUrl ? `<img src="${plotUrl}" alt="Generated chart" style="margin-top:1rem; border-radius:12px; border:1px solid var(--border-color); max-width:100%; box-shadow: 0 4px 15px var(--shadow-color);" />` : ''}
|
|
2738
|
+
</div>`;
|
|
2739
|
+
els.chatMessages.appendChild(div);
|
|
2740
|
+
els.chatMessages.scrollTop = els.chatMessages.scrollHeight;
|
|
2741
|
+
|
|
2742
|
+
if (window.lucide) {
|
|
2743
|
+
lucide.createIcons({ attrs: { class: 'icon-svg' } });
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
function showTypingIndicator() {
|
|
2748
|
+
const div = document.createElement('div');
|
|
2749
|
+
div.className = 'chat-msg assistant';
|
|
2750
|
+
div.id = 'typingIndicator';
|
|
2751
|
+
div.innerHTML = `
|
|
2752
|
+
<div class="chat-avatar"><i data-lucide="bot" style="width: 16px; height: 16px;"></i></div>
|
|
2753
|
+
<div class="chat-bubble" style="color:var(--text-muted)">
|
|
2754
|
+
<span style="animation:pulse 1.2s infinite;display:inline-block">Analysing</span>…
|
|
2755
|
+
</div>`;
|
|
2756
|
+
els.chatMessages.appendChild(div);
|
|
2757
|
+
els.chatMessages.scrollTop = els.chatMessages.scrollHeight;
|
|
2758
|
+
|
|
2759
|
+
if (window.lucide) {
|
|
2760
|
+
lucide.createIcons({ attrs: { class: 'icon-svg' } });
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
function removeTypingIndicator() {
|
|
2765
|
+
const el = $('typingIndicator');
|
|
2766
|
+
if (el) el.remove();
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
async function sendChat() {
|
|
2770
|
+
const query = els.chatInput.value.trim();
|
|
2771
|
+
if (!query) return;
|
|
2772
|
+
|
|
2773
|
+
const sessionId = state.activeProject?.id || state.uploadedSession;
|
|
2774
|
+
if (!sessionId) { toast('No active session. Run an analysis first.', 'warning'); return; }
|
|
2775
|
+
|
|
2776
|
+
// Read API key directly from localStorage (not the hidden input which may be stale)
|
|
2777
|
+
const provider = els.llmProvider.value;
|
|
2778
|
+
const apiKey = getSavedKey(provider);
|
|
2779
|
+
|
|
2780
|
+
// For non-Ollama providers, warn if no key
|
|
2781
|
+
if (provider !== 'ollama' && !apiKey) {
|
|
2782
|
+
toast('No API key set. Go to settings to add your key.', 'warning');
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
els.chatInput.value = '';
|
|
2787
|
+
hideColumnPicker();
|
|
2788
|
+
appendChatMsg('user', query);
|
|
2789
|
+
showTypingIndicator();
|
|
2790
|
+
|
|
2791
|
+
const fd = new FormData();
|
|
2792
|
+
fd.append('session_id', sessionId);
|
|
2793
|
+
fd.append('query', query);
|
|
2794
|
+
fd.append('provider', provider);
|
|
2795
|
+
fd.append('model', els.llmModel.value === '__custom__' ? '' : els.llmModel.value);
|
|
2796
|
+
fd.append('api_key', apiKey);
|
|
2797
|
+
|
|
2798
|
+
try {
|
|
2799
|
+
const res = await fetch('/api/copilot', { method: 'POST', body: fd });
|
|
2800
|
+
removeTypingIndicator();
|
|
2801
|
+
|
|
2802
|
+
if (!res.ok) {
|
|
2803
|
+
// Surface backend HTTP errors (422, 500, etc.)
|
|
2804
|
+
let errMsg = `Server error (${res.status})`;
|
|
2805
|
+
try {
|
|
2806
|
+
const errData = await res.json();
|
|
2807
|
+
errMsg = errData.detail || errData.message || errMsg;
|
|
2808
|
+
} catch (_) {}
|
|
2809
|
+
appendChatMsg('assistant', errMsg);
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
const data = await res.json();
|
|
2814
|
+
const text = data.text && data.text.trim() ? data.text : 'No response returned.';
|
|
2815
|
+
appendChatMsg('assistant', text, data.plot_url || null);
|
|
2816
|
+
|
|
2817
|
+
// Reload dynamic preview if this query modified the dataset
|
|
2818
|
+
const qLower = query.toLowerCase();
|
|
2819
|
+
if (qLower.includes('delete') || qLower.includes('rename') || qLower.includes('replace') || qLower.includes('drop') || qLower.includes('fix') || qLower.includes('clean') || qLower.includes('modify') || qLower.includes('update')) {
|
|
2820
|
+
await refreshPreviewData(sessionId);
|
|
2821
|
+
}
|
|
2822
|
+
} catch (e) {
|
|
2823
|
+
removeTypingIndicator();
|
|
2824
|
+
appendChatMsg('assistant', 'Network error: ' + e.message);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
els.sendChatBtn.addEventListener('click', sendChat);
|
|
2829
|
+
els.chatInput.addEventListener('keydown', e => {
|
|
2830
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); }
|
|
2831
|
+
});
|
|
2832
|
+
|
|
2833
|
+
// Quick hint chips
|
|
2834
|
+
$$('.chat-hint-chip').forEach(chip => {
|
|
2835
|
+
chip.addEventListener('click', () => {
|
|
2836
|
+
els.chatInput.value = chip.dataset.query;
|
|
2837
|
+
activateTab('chat');
|
|
2838
|
+
sendChat();
|
|
2839
|
+
});
|
|
2840
|
+
});
|
|
2841
|
+
|
|
2842
|
+
els.clearChatBtn.addEventListener('click', resetChat);
|
|
2843
|
+
|
|
2844
|
+
// ── /column slash picker ─────────────────────────────────────────────────────
|
|
2845
|
+
function showColumnPicker(filter = '') {
|
|
2846
|
+
const cols = state.columns.filter(c =>
|
|
2847
|
+
c.toLowerCase().includes(filter.toLowerCase())
|
|
2848
|
+
);
|
|
2849
|
+
const picker = els.colPickerDropdown;
|
|
2850
|
+
|
|
2851
|
+
if (!cols.length) { hideColumnPicker(); return; }
|
|
2852
|
+
|
|
2853
|
+
picker.innerHTML = `<div class="col-picker-header"><i data-lucide="hash" style="width: 12px; height: 12px; vertical-align: middle; margin-right: 4px;"></i> Insert Column — type to filter</div>` +
|
|
2854
|
+
cols.slice(0, 20).map(c => {
|
|
2855
|
+
const dtype = state.colTypes[c] || '';
|
|
2856
|
+
return `<div class="col-picker-item" data-col="${escHtml(c)}">
|
|
2857
|
+
${escHtml(c)}
|
|
2858
|
+
${dtype ? `<span class="col-picker-item-type">${escHtml(dtype)}</span>` : ''}
|
|
2859
|
+
</div>`;
|
|
2860
|
+
}).join('');
|
|
2861
|
+
|
|
2862
|
+
picker.querySelectorAll('.col-picker-item').forEach(item => {
|
|
2863
|
+
item.addEventListener('click', () => {
|
|
2864
|
+
const col = item.dataset.col;
|
|
2865
|
+
const textarea = els.chatInput;
|
|
2866
|
+
const val = textarea.value;
|
|
2867
|
+
// Replace the /... part with the column name
|
|
2868
|
+
const slashIdx = val.lastIndexOf('/');
|
|
2869
|
+
textarea.value = (slashIdx >= 0 ? val.slice(0, slashIdx) : val) + `\`${col}\` `;
|
|
2870
|
+
hideColumnPicker();
|
|
2871
|
+
textarea.focus();
|
|
2872
|
+
});
|
|
2873
|
+
});
|
|
2874
|
+
|
|
2875
|
+
picker.classList.remove('hidden');
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
function hideColumnPicker() {
|
|
2879
|
+
els.colPickerDropdown.classList.add('hidden');
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
els.chatInput.addEventListener('input', () => {
|
|
2883
|
+
const val = els.chatInput.value;
|
|
2884
|
+
const slashIdx = val.lastIndexOf('/');
|
|
2885
|
+
if (slashIdx >= 0 && slashIdx === val.length - 1) {
|
|
2886
|
+
// Just typed /
|
|
2887
|
+
showColumnPicker('');
|
|
2888
|
+
} else if (slashIdx >= 0 && slashIdx < val.length) {
|
|
2889
|
+
// Typing after /
|
|
2890
|
+
showColumnPicker(val.slice(slashIdx + 1));
|
|
2891
|
+
} else {
|
|
2892
|
+
hideColumnPicker();
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2895
|
+
|
|
2896
|
+
document.addEventListener('click', e => {
|
|
2897
|
+
if (!els.colPickerDropdown.contains(e.target) && e.target !== els.chatInput) {
|
|
2898
|
+
hideColumnPicker();
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
|
|
2902
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2903
|
+
// Utilities
|
|
2904
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2905
|
+
function escHtml(str) {
|
|
2906
|
+
return String(str ?? '')
|
|
2907
|
+
.replace(/&/g,'&')
|
|
2908
|
+
.replace(/</g,'<')
|
|
2909
|
+
.replace(/>/g,'>')
|
|
2910
|
+
.replace(/"/g,'"');
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2914
|
+
// Initialise
|
|
2915
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2916
|
+
(async function init() {
|
|
2917
|
+
await loadLlmSettings();
|
|
2918
|
+
loadProjects();
|
|
2919
|
+
syncTaskCards();
|
|
2920
|
+
resetWizardState();
|
|
2921
|
+
showScreen('landing');
|
|
2922
|
+
setStatus('● Idle', 'idle');
|
|
2923
|
+
|
|
2924
|
+
if (window.lucide) {
|
|
2925
|
+
lucide.createIcons();
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// Wire Selection Hub Cards
|
|
2929
|
+
if (els.btnEnterChat) {
|
|
2930
|
+
els.btnEnterChat.addEventListener('click', () => {
|
|
2931
|
+
const p = state.activeProject;
|
|
2932
|
+
if (!p) return;
|
|
2933
|
+
if (els.btnSectionChat && els.btnSectionChat.parentNode) {
|
|
2934
|
+
els.btnSectionChat.parentNode.classList.remove('hidden');
|
|
2935
|
+
}
|
|
2936
|
+
if (els.areaHub) els.areaHub.classList.add('hidden');
|
|
2937
|
+
switchSection('chat');
|
|
2938
|
+
setBreadcrumb(p.name, 'AI Data Chat');
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
if (els.btnEnterAgentic) {
|
|
2943
|
+
els.btnEnterAgentic.addEventListener('click', () => {
|
|
2944
|
+
const p = state.activeProject;
|
|
2945
|
+
if (!p) return;
|
|
2946
|
+
if (p.status === 'running') {
|
|
2947
|
+
showScreen('running');
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
if (els.btnSectionChat && els.btnSectionChat.parentNode) {
|
|
2951
|
+
els.btnSectionChat.parentNode.classList.remove('hidden');
|
|
2952
|
+
}
|
|
2953
|
+
if (els.areaHub) els.areaHub.classList.add('hidden');
|
|
2954
|
+
switchSection('agentic');
|
|
2955
|
+
setBreadcrumb(p.name, 'Crew Analysis');
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// Wire section switcher
|
|
2960
|
+
if (els.btnSectionChat) {
|
|
2961
|
+
els.btnSectionChat.addEventListener('click', () => {
|
|
2962
|
+
const p = state.activeProject;
|
|
2963
|
+
if (els.areaHub) els.areaHub.classList.add('hidden');
|
|
2964
|
+
switchSection('chat');
|
|
2965
|
+
if (p) setBreadcrumb(p.name, 'AI Data Chat');
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
if (els.btnSectionAgentic) {
|
|
2969
|
+
els.btnSectionAgentic.addEventListener('click', () => {
|
|
2970
|
+
const p = state.activeProject;
|
|
2971
|
+
if (els.areaHub) els.areaHub.classList.add('hidden');
|
|
2972
|
+
switchSection('agentic');
|
|
2973
|
+
if (p) setBreadcrumb(p.name, 'Crew Analysis');
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
if (els.chatBackBtn) {
|
|
2977
|
+
els.chatBackBtn.addEventListener('click', () => {
|
|
2978
|
+
const p = state.activeProject;
|
|
2979
|
+
if (els.areaHub) els.areaHub.classList.add('hidden');
|
|
2980
|
+
switchSection('agentic');
|
|
2981
|
+
if (p) setBreadcrumb(p.name, 'Crew Analysis');
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// Wire quick action buttons
|
|
2986
|
+
if (els.btnRenameColQuick) {
|
|
2987
|
+
els.btnRenameColQuick.addEventListener('click', async () => {
|
|
2988
|
+
const oldName = await customPrompt('Select or enter the column you want to rename:', '', 'e.g. Q3_Sales', 'Rename Column');
|
|
2989
|
+
if (!oldName) return;
|
|
2990
|
+
if (!state.columns.includes(oldName)) {
|
|
2991
|
+
toast(`Column "${oldName}" not found in dataset.`, 'error');
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
const newName = await customPrompt(`Enter the new name for column "${oldName}":`, '', 'e.g. Sales_Q3', 'Rename Column');
|
|
2995
|
+
if (!newName) return;
|
|
2996
|
+
|
|
2997
|
+
// Command the copilot
|
|
2998
|
+
els.chatInput.value = `Rename column \`${oldName}\` to \`${newName}\` in the dataset`;
|
|
2999
|
+
sendChat();
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
if (els.btnDeleteColQuick) {
|
|
3004
|
+
els.btnDeleteColQuick.addEventListener('click', async () => {
|
|
3005
|
+
const colName = await customPrompt('Enter the name of the column you want to delete:', '', 'e.g. Unwanted_Col', 'Delete Column');
|
|
3006
|
+
if (!colName) return;
|
|
3007
|
+
if (!state.columns.includes(colName)) {
|
|
3008
|
+
toast(`Column "${colName}" not found in dataset.`, 'error');
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
const confirmed = await customConfirm(`Are you sure you want to permanently delete column "${colName}"?`, 'Delete Column');
|
|
3012
|
+
if (!confirmed) return;
|
|
3013
|
+
|
|
3014
|
+
// Command the copilot
|
|
3015
|
+
els.chatInput.value = `Delete column \`${colName}\` from the dataset`;
|
|
3016
|
+
sendChat();
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// Wire agentic pipeline launch button
|
|
3021
|
+
if (els.btnRunAgenticPipeline) {
|
|
3022
|
+
els.btnRunAgenticPipeline.addEventListener('click', () => {
|
|
3023
|
+
state.uploadedSession = state.activeProject?.id;
|
|
3024
|
+
state.uploadedFile = { name: state.activeProject?.filename || 'dataset.csv' };
|
|
3025
|
+
if (!checkApiKeySet()) return;
|
|
3026
|
+
openConfigModal();
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// Wire run in background and dismiss notification buttons
|
|
3031
|
+
const btnBg = $('btnRunInBackground');
|
|
3032
|
+
if (btnBg) {
|
|
3033
|
+
btnBg.addEventListener('click', () => {
|
|
3034
|
+
const p = state.activeProject;
|
|
3035
|
+
if (p) {
|
|
3036
|
+
goToWorkspaceHub();
|
|
3037
|
+
showScreen('results');
|
|
3038
|
+
toast('Running in background. Check notifications for progress.', 'info');
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
const btnDismiss = $('dismissNotif');
|
|
3044
|
+
if (btnDismiss) {
|
|
3045
|
+
btnDismiss.addEventListener('click', (e) => {
|
|
3046
|
+
e.stopPropagation();
|
|
3047
|
+
const notif = $('analysisNotification');
|
|
3048
|
+
if (notif) notif.classList.add('hidden');
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
// Performance Metrics logic
|
|
3053
|
+
async function loadMetrics() {
|
|
3054
|
+
try {
|
|
3055
|
+
const res = await fetch('/api/metrics');
|
|
3056
|
+
if (!res.ok) throw new Error('Failed to fetch metrics');
|
|
3057
|
+
const data = await res.json();
|
|
3058
|
+
|
|
3059
|
+
const totalRuns = data.length;
|
|
3060
|
+
let totalTime = 0;
|
|
3061
|
+
let totalTokens = 0;
|
|
3062
|
+
let totalCost = 0;
|
|
3063
|
+
|
|
3064
|
+
data.forEach(run => {
|
|
3065
|
+
totalTime += run.total_time || 0;
|
|
3066
|
+
totalTokens += run.token_usage || 0;
|
|
3067
|
+
totalCost += run.estimated_cost || 0;
|
|
3068
|
+
});
|
|
3069
|
+
|
|
3070
|
+
const avgTime = totalRuns > 0 ? (totalTime / totalRuns).toFixed(1) : 0;
|
|
3071
|
+
|
|
3072
|
+
const runsEl = $('metricsTotalRuns');
|
|
3073
|
+
const timeEl = $('metricsAvgTime');
|
|
3074
|
+
const tokensEl = $('metricsTotalTokens');
|
|
3075
|
+
const costEl = $('metricsTotalCost');
|
|
3076
|
+
|
|
3077
|
+
if (runsEl) runsEl.textContent = totalRuns;
|
|
3078
|
+
if (timeEl) timeEl.textContent = `${avgTime}s`;
|
|
3079
|
+
if (tokensEl) tokensEl.textContent = totalTokens.toLocaleString();
|
|
3080
|
+
if (costEl) costEl.textContent = `$${totalCost.toFixed(3)}`;
|
|
3081
|
+
|
|
3082
|
+
const tbody = $('metricsTableBody');
|
|
3083
|
+
if (tbody) {
|
|
3084
|
+
tbody.innerHTML = '';
|
|
3085
|
+
|
|
3086
|
+
if (data.length === 0) {
|
|
3087
|
+
tbody.innerHTML = `<tr><td colspan="7" style="text-align: center; padding: 20px; color: var(--text-secondary);">No run history recorded yet.</td></tr>`;
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
const sorted = [...data].reverse();
|
|
3092
|
+
sorted.forEach(run => {
|
|
3093
|
+
const tr = document.createElement('tr');
|
|
3094
|
+
tr.style.borderBottom = '1px solid var(--border)';
|
|
3095
|
+
|
|
3096
|
+
const ts = new Date(run.timestamp).toLocaleString();
|
|
3097
|
+
const statusText = run.success ? '✓ Success' : '✗ Failed';
|
|
3098
|
+
const statusStyle = run.success ? 'color: var(--emerald); font-weight: 600;' : 'color: var(--rose); font-weight: 600;';
|
|
3099
|
+
|
|
3100
|
+
tr.innerHTML = `
|
|
3101
|
+
<td style="padding: 12px 20px; font-weight: 500;">${escHtml(run.dataset_name || 'dataset.csv')}</td>
|
|
3102
|
+
<td style="padding: 12px 20px; color: var(--text-secondary);">${run.rows || 0} x ${run.columns || 0}</td>
|
|
3103
|
+
<td style="padding: 12px 20px;">${(run.total_time || 0).toFixed(1)}s</td>
|
|
3104
|
+
<td style="padding: 12px 20px;">${(run.token_usage || 0).toLocaleString()}</td>
|
|
3105
|
+
<td style="padding: 12px 20px; color: var(--amber); font-weight: 500;">$${(run.estimated_cost || 0).toFixed(3)}</td>
|
|
3106
|
+
<td style="padding: 12px 20px; color: var(--text-secondary);">${ts}</td>
|
|
3107
|
+
<td style="padding: 12px 20px; ${statusStyle}">${statusText}</td>
|
|
3108
|
+
`;
|
|
3109
|
+
tbody.appendChild(tr);
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
} catch (err) {
|
|
3113
|
+
console.error(err);
|
|
3114
|
+
toast('Failed to load performance metrics', 'error');
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
if (els.sidebarMetricsBtn) {
|
|
3119
|
+
els.sidebarMetricsBtn.addEventListener('click', () => {
|
|
3120
|
+
showScreen('metrics');
|
|
3121
|
+
loadMetrics();
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
if (els.backToDashboardBtn) {
|
|
3126
|
+
els.backToDashboardBtn.addEventListener('click', () => {
|
|
3127
|
+
if (state.activeProject) {
|
|
3128
|
+
showScreen('results');
|
|
3129
|
+
} else {
|
|
3130
|
+
showScreen('landing');
|
|
3131
|
+
}
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// Check if a running session exists on page load
|
|
3136
|
+
// (handles F5 refresh during analysis)
|
|
3137
|
+
setTimeout(async () => {
|
|
3138
|
+
const projects = state.projects;
|
|
3139
|
+
const running = projects.find(p => p.status === 'running');
|
|
3140
|
+
if (running) switchToProject(running);
|
|
3141
|
+
}, 500);
|
|
3142
|
+
})();
|