Raykarr commited on
Commit
fa7fc22
Β·
verified Β·
1 Parent(s): f67222e

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +792 -52
index.html CHANGED
@@ -29,6 +29,8 @@
29
  --color-assist-text: #212529;
30
  --color-danger: #d90429;
31
  --color-danger-bg: #fff0f0;
 
 
32
  }
33
 
34
  * {
@@ -87,6 +89,9 @@
87
  padding: 16px 24px;
88
  background-color: #fdfdfe;
89
  border-bottom: 1px solid var(--color-border);
 
 
 
90
  }
91
 
92
  .api-key-group {
@@ -116,6 +121,37 @@
116
  margin-top: 8px;
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  /* --- Chat Box --- */
120
  #chat-box {
121
  flex-grow: 1;
@@ -174,6 +210,12 @@
174
  border: 1px solid var(--color-danger);
175
  }
176
 
 
 
 
 
 
 
177
  /* --- Markdown Formatting for Assistant --- */
178
  .message.assistant strong { font-weight: 600; }
179
  .message.assistant h1, .message.assistant h2, .message.assistant h3 {
@@ -231,10 +273,13 @@
231
  flex-wrap: wrap;
232
  gap: 8px;
233
  margin-bottom: 12px;
 
234
  }
235
 
236
  .file-pill {
237
- display: inline-block;
 
 
238
  background-color: var(--color-assist-msg);
239
  color: var(--color-text);
240
  padding: 5px 12px;
@@ -242,6 +287,26 @@
242
  font-size: 0.8rem;
243
  font-weight: 500;
244
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  .prompt-input-group {
247
  display: flex;
@@ -332,6 +397,8 @@
332
  /* --- Debug/Refined Prompt Area --- */
333
  .debug-container {
334
  padding: 12px 24px 4px;
 
 
335
  }
336
 
337
  .debug-container summary {
@@ -339,15 +406,32 @@
339
  color: var(--color-text-light);
340
  cursor: pointer;
341
  font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  }
343
 
344
  #debug-box {
345
- display: block; /* Content is shown when <details> is open */
346
  font-size: 0.8rem;
347
- background-color: var(--color-bg);
348
  border: 1px solid var(--color-border);
349
  padding: 12px;
350
- margin-top: 8px;
351
  border-radius: 8px;
352
  font-family: monospace;
353
  line-height: 1.4;
@@ -356,6 +440,27 @@
356
  white-space: pre-wrap;
357
  word-wrap: break-word;
358
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
  </style>
361
  </head>
@@ -364,7 +469,7 @@
364
  <div class="container">
365
  <header>
366
  <h1>🧠 Carver Excel Analyst</h1>
367
- <p class="caption">This AI uses a 2-step process to refine your query and provide an expert analysis.</p>
368
  </header>
369
 
370
  <div class="config-bar">
@@ -372,14 +477,34 @@
372
  <label for="api-key-input">API Key:</label>
373
  <input type="password" id="api-key-input" placeholder="Enter your Google API Key">
374
  </div>
375
- <div class="api-key-warning">πŸ”΄ For development only. Your key is visible in this browser.</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  </div>
377
 
378
  <div id="chat-box">
379
  <div class="message assistant">
380
- Hello! Please provide your API key, then upload your files (Excel, PDF, Images) and ask a question.
381
  <br><br>
382
- <b>Note:</b> Excel files (`.xlsx`, `.xls`) will be automatically converted to CSV text for analysis.
 
 
 
 
 
 
383
  </div>
384
  </div>
385
 
@@ -402,11 +527,8 @@
402
  </button>
403
  </div>
404
 
405
- <div class="debug-container">
406
- <details id="debug-details">
407
- <summary>Show Analysis Details</summary>
408
- <div id="debug-box">No analysis details yet.</div>
409
- </details>
410
  </div>
411
  </div>
412
  </div>
@@ -424,15 +546,21 @@
424
  const sendButton = document.getElementById("send-button");
425
  const chatBox = document.getElementById("chat-box");
426
  const statusText = document.getElementById("status-text");
427
- const debugDetails = document.getElementById("debug-details");
428
  const debugBox = document.getElementById("debug-box");
 
 
 
 
429
 
430
  // --- Model Configuration ---
431
  const GENERATOR_MODEL_NAME = "gemini-2.0-flash";
432
  const ANALYST_MODEL_NAME = "gemini-2.5-flash";
433
 
434
- // --- Store uploaded files ---
435
- let uploadedFiles = [];
 
 
 
436
 
437
  // --- The Main Carver Analyst System Prompt ---
438
  const CARVER_ANALYST_SYSTEM_PROMPT = `
@@ -442,9 +570,9 @@ NOTE: Excel files (.xlsx, .xls) have been pre-converted into structured CSV text
442
  ### 🎯 Mission
443
  Understand the provided data (CSV text, PDFs, images) and provide short, factual insights β€” **without making any assumptions**.
444
  If any required input is missing, unclear, or conflicting, you **must first ask a short, specific clarifying question** before proceeding. Never guess, invent, or assume.
445
- Your goal is to make Carver’s data analysis simple, reliable, and audit-ready.
446
  ### 🧩 Default Output (when all inputs are clear)
447
- 1. **Executive Summary (3–6 lines)** β€” concise explanation answering the user’s question.
448
  2. **Supporting Table (Markdown or CSV)** β€” key rows/columns only.
449
  3. **Audit Block (3–6 lines)** β€” list of: file(s) used (or file/sheet name for CSVs), cell/range references (or row/column names), missing or ignored fields, and formulas applied.
450
  Do **not** create or export a new Excel file unless the user explicitly asks for it.
@@ -454,11 +582,11 @@ Do **not** create or export a new Excel file unless the user explicitly asks for
454
  - Always confirm mappings if confidence <80%.
455
  **B. Required Data Rules**
456
  - For cost analysis β†’ need \`Qty\` + \`UnitPrice\` or \`Total\`.
457
- - For β€œvs budget” β†’ need a \`Baseline\`, \`Budget\`, or explicit reference vendor.
458
  - If missing, ask user directly.
459
  **C. No-Assumptions Policy**
460
  - If anything is missing or unclear: Ask before proceeding.
461
- - Example: β€œNo β€˜Budget’ column found. Should I use the lowest vendor total as baseline?”
462
  **D. Validations**
463
  - Detect and report missing values, currency mismatches, etc., in the Audit Block.
464
  **E. Computation Rules**
@@ -469,7 +597,9 @@ Do **not** create or export a new Excel file unless the user explicitly asks for
469
  **H. Tone**
470
  - Professional, precise, procurement-focused.
471
  ### 🧭 Objective
472
- Act like Carver’s in-house **Excel procurement analyst** β€” structured, audit-traceable, and transparent. Never make assumptions. Always verify.
 
 
473
  `;
474
 
475
  // --- Event Listeners ---
@@ -486,17 +616,25 @@ Act like Carver’s in-house **Excel procurement analyst** β€” structured, audit
486
  fileInput.click();
487
  });
488
 
489
- // Update file pills
 
 
 
 
 
 
 
 
 
490
  fileInput.addEventListener("change", (e) => {
491
- uploadedFiles = Array.from(e.target.files);
492
- fileListDisplay.innerHTML = ""; // Clear existing pills
 
 
 
493
  if (uploadedFiles.length > 0) {
494
- uploadedFiles.forEach(file => {
495
- const pill = document.createElement("span");
496
- pill.className = "file-pill";
497
- pill.textContent = file.name;
498
- fileListDisplay.appendChild(pill);
499
- });
500
  }
501
  });
502
 
@@ -515,15 +653,14 @@ Act like Carver’s in-house **Excel procurement analyst** β€” structured, audit
515
  return;
516
  }
517
  if (uploadedFiles.length === 0) {
518
- displayError("Please upload at least one file.");
519
  return;
520
  }
521
 
522
  // --- 2. Setup UI for Loading ---
523
- setLoadingState(true, "Processing files... (Excel files will be converted to text)");
524
  displayMessage(userPrompt, "user");
525
  debugBox.textContent = "Generating refined prompt..."; // Clear old debug info
526
- debugDetails.open = false; // Close the details box
527
 
528
  const assistantMessageEl = displayMessage("", "assistant"); // Create empty bubble
529
 
@@ -534,26 +671,39 @@ Act like Carver’s in-house **Excel procurement analyst** β€” structured, audit
534
  );
535
  const fileParts = partsArrays.flat();
536
 
537
- // --- 4. Initialize AI Client ---
 
 
 
 
 
 
 
 
 
 
 
 
538
  const genAI = new GoogleGenerativeAI(apiKey);
539
 
540
- // --- 5. STEP 1: Generate the "Better Prompt" (Normal Model) ---
541
  setLoadingState(true, `Step 1/2: Thinking about your question... (${GENERATOR_MODEL_NAME})`);
542
 
543
  const generatorModel = genAI.getGenerativeModel({ model: GENERATOR_MODEL_NAME });
544
 
545
  const step1SystemPrompt = `
546
- You are a prompt engineering assistant. Your job is to take a user's simple question and the attached file(s) and generate a new, detailed, and specific prompt. This new prompt will be given to a second AI, which is an expert 'Carver Excel Analyst'.
547
- NOTE: The attached files may include images, PDFs, or TEXT parts that are CSV conversions of Excel sheets. The analyst is aware of this.
548
 
549
- The user's question is: "${userPrompt}"
550
 
551
- Based on the user's question and the file(s), generate a single, clear, and actionable instruction for the expert analyst.
552
- - Be specific.
553
- - Ask for the final output (summary, table, audit block) as defined by the analyst's role.
554
- - For example, if the user says "how much did I save?", you should generate a prompt like:
555
- "Please analyze the attached data (CSV text, PDFs, images). Identify the baseline or budget, compare all vendor totals against it, and calculate the saving amount and percentage for the lowest-cost compliant vendor. Present this in an executive summary, a comparison table, and an audit block."
556
- `;
 
557
 
558
  const step1Contents = [
559
  ...fileParts,
@@ -567,9 +717,10 @@ Act like Carver’s in-house **Excel procurement analyst** β€” structured, audit
567
 
568
  // Display the refined prompt in the debug box
569
  debugBox.textContent = generatedPrompt;
570
- debugDetails.open = true; // Open the details box to show the new prompt
 
571
 
572
- // --- 6. STEP 2: Get Final Answer (Reasoning Model) ---
573
  setLoadingState(true, `Step 2/2: The Carver Analyst is working... (${ANALYST_MODEL_NAME})`);
574
 
575
  const analystModel = genAI.getGenerativeModel({
@@ -586,7 +737,7 @@ Act like Carver’s in-house **Excel procurement analyst** β€” structured, audit
586
  contents: [{ parts: step2Contents }]
587
  });
588
 
589
- // --- 7. Stream Response to Chat ---
590
  let fullResponse = "";
591
  for await (const chunk of result.stream) {
592
  if (typeof chunk.text === 'function') {
@@ -597,23 +748,92 @@ Act like Carver’s in-house **Excel procurement analyst** β€” structured, audit
597
  }
598
  }
599
 
 
 
 
 
 
 
 
 
 
 
 
600
  } catch (error) {
601
  console.error("Error:", error);
602
  displayError(`An error occurred: ${error.message}`);
603
  assistantMessageEl.remove(); // Remove the empty bubble
 
 
604
  } finally {
605
- // --- 8. Cleanup UI ---
606
  setLoadingState(false);
607
  promptInput.value = "";
608
- // Clear files after processing
609
- uploadedFiles = [];
610
- fileInput.value = ""; // Reset the file input
611
- fileListDisplay.innerHTML = "";
612
  }
613
  }
614
 
615
  // --- Helper Functions ---
616
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  /**
618
  * Converts a File object to an array of GoogleGenerativeAI.Part objects.
619
  */
@@ -701,6 +921,25 @@ Act like Carver’s in-house **Excel procurement analyst** β€” structured, audit
701
  return messageEl;
702
  }
703
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  /**
705
  * Displays an error message in the chat
706
  */
@@ -716,6 +955,507 @@ Act like Carver’s in-house **Excel procurement analyst** β€” structured, audit
716
  chatBox.scrollTop = chatBox.scrollHeight;
717
  }
718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
  </script>
720
  </body>
721
  </html>
 
29
  --color-assist-text: #212529;
30
  --color-danger: #d90429;
31
  --color-danger-bg: #fff0f0;
32
+ --color-success: #28a745;
33
+ --color-success-bg: #f0fff4;
34
  }
35
 
36
  * {
 
89
  padding: 16px 24px;
90
  background-color: #fdfdfe;
91
  border-bottom: 1px solid var(--color-border);
92
+ display: flex;
93
+ justify-content: space-between;
94
+ align-items: center;
95
  }
96
 
97
  .api-key-group {
 
121
  margin-top: 8px;
122
  }
123
 
124
+ /* --- Header Actions --- */
125
+ .header-actions {
126
+ display: flex;
127
+ gap: 8px;
128
+ }
129
+
130
+ .action-button {
131
+ padding: 6px 12px;
132
+ border: 1px solid var(--color-border);
133
+ background-color: var(--color-card);
134
+ border-radius: 6px;
135
+ font-size: 0.85rem;
136
+ cursor: pointer;
137
+ font-family: var(--font-main);
138
+ transition: all 0.2s;
139
+ }
140
+
141
+ .action-button:hover {
142
+ background-color: var(--color-assist-msg);
143
+ border-color: var(--color-accent);
144
+ }
145
+
146
+ .action-button.danger {
147
+ color: var(--color-danger);
148
+ border-color: var(--color-danger);
149
+ }
150
+
151
+ .action-button.danger:hover {
152
+ background-color: var(--color-danger-bg);
153
+ }
154
+
155
  /* --- Chat Box --- */
156
  #chat-box {
157
  flex-grow: 1;
 
210
  border: 1px solid var(--color-danger);
211
  }
212
 
213
+ .message.success {
214
+ background-color: var(--color-success-bg);
215
+ color: var(--color-success);
216
+ border: 1px solid var(--color-success);
217
+ }
218
+
219
  /* --- Markdown Formatting for Assistant --- */
220
  .message.assistant strong { font-weight: 600; }
221
  .message.assistant h1, .message.assistant h2, .message.assistant h3 {
 
273
  flex-wrap: wrap;
274
  gap: 8px;
275
  margin-bottom: 12px;
276
+ align-items: center;
277
  }
278
 
279
  .file-pill {
280
+ display: inline-flex;
281
+ align-items: center;
282
+ gap: 6px;
283
  background-color: var(--color-assist-msg);
284
  color: var(--color-text);
285
  padding: 5px 12px;
 
287
  font-size: 0.8rem;
288
  font-weight: 500;
289
  }
290
+
291
+ .file-remove {
292
+ background: none;
293
+ border: none;
294
+ color: var(--color-danger);
295
+ cursor: pointer;
296
+ font-size: 0.7rem;
297
+ padding: 0;
298
+ width: 16px;
299
+ height: 16px;
300
+ border-radius: 50%;
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ }
305
+
306
+ .file-remove:hover {
307
+ background-color: var(--color-danger);
308
+ color: white;
309
+ }
310
 
311
  .prompt-input-group {
312
  display: flex;
 
397
  /* --- Debug/Refined Prompt Area --- */
398
  .debug-container {
399
  padding: 12px 24px 4px;
400
+ border-top: 1px solid var(--color-border);
401
+ background-color: #f8f9fa;
402
  }
403
 
404
  .debug-container summary {
 
406
  color: var(--color-text-light);
407
  cursor: pointer;
408
  font-weight: 500;
409
+ padding: 8px 0;
410
+ list-style: none;
411
+ }
412
+
413
+ .debug-container summary::-webkit-details-marker {
414
+ display: none;
415
+ }
416
+
417
+ .debug-container summary::before {
418
+ content: 'β–Ά';
419
+ display: inline-block;
420
+ margin-right: 8px;
421
+ transition: transform 0.2s;
422
+ }
423
+
424
+ .debug-container[open] summary::before {
425
+ transform: rotate(90deg);
426
  }
427
 
428
  #debug-box {
429
+ display: none; /* Hidden by default */
430
  font-size: 0.8rem;
431
+ background-color: var(--color-card);
432
  border: 1px solid var(--color-border);
433
  padding: 12px;
434
+ margin: 8px 0 0 0;
435
  border-radius: 8px;
436
  font-family: monospace;
437
  line-height: 1.4;
 
440
  white-space: pre-wrap;
441
  word-wrap: break-word;
442
  }
443
+
444
+ /* Show debug box when details are open */
445
+ .debug-container[open] #debug-box,
446
+ .debug-box-visible {
447
+ display: block;
448
+ }
449
+
450
+ /* Hide the native details arrow */
451
+ .debug-container summary {
452
+ display: none;
453
+ }
454
+
455
+ /* --- Conversation Info --- */
456
+ .conversation-info {
457
+ text-align: center;
458
+ padding: 8px 16px;
459
+ font-size: 0.85rem;
460
+ color: var(--color-text-light);
461
+ background-color: var(--color-success-bg);
462
+ border-bottom: 1px solid var(--color-border);
463
+ }
464
 
465
  </style>
466
  </head>
 
469
  <div class="container">
470
  <header>
471
  <h1>🧠 Carver Excel Analyst</h1>
472
+ <p class="caption">This AI uses a 2-step process to refine your query and provide an expert analysis. <b>Upload files once, ask multiple questions!</b></p>
473
  </header>
474
 
475
  <div class="config-bar">
 
477
  <label for="api-key-input">API Key:</label>
478
  <input type="password" id="api-key-input" placeholder="Enter your Google API Key">
479
  </div>
480
+ <div class="header-actions">
481
+ <button class="action-button success" id="export-pdf-btn" title="Export conversation to PDF">
482
+ πŸ“„ Export PDF
483
+ </button>
484
+ <button class="action-button" id="toggle-details-btn" title="Show/Hide analysis details">
485
+ πŸ‘οΈ Show Analysis Details
486
+ </button>
487
+ <button class="action-button" id="new-conversation-btn" title="Start a new conversation">
488
+ πŸ”„ New Conversation
489
+ </button>
490
+ </div>
491
+ </div>
492
+
493
+ <div class="conversation-info" id="conversation-info" style="display: none;">
494
+ πŸ’Ύ Files remain available for this conversation. Ask follow-up questions anytime!
495
  </div>
496
 
497
  <div id="chat-box">
498
  <div class="message assistant">
499
+ Welcome to Carver Excel Analyst! This is a <b>conversational chatbot</b> that remembers your files and conversation.
500
  <br><br>
501
+ <b>How it works:</b>
502
+ <br>β€’ Upload your Excel, PDF, or image files once
503
+ <br>β€’ Ask questions about the data
504
+ <br>β€’ Files stay available for follow-up questions
505
+ <br>β€’ Start a new conversation anytime to clear history
506
+ <br><br>
507
+ <b>Please provide your API key and upload your files to begin!</b>
508
  </div>
509
  </div>
510
 
 
527
  </button>
528
  </div>
529
 
530
+ <div class="debug-container" id="debug-container">
531
+ <div id="debug-box">No analysis details yet.</div>
 
 
 
532
  </div>
533
  </div>
534
  </div>
 
546
  const sendButton = document.getElementById("send-button");
547
  const chatBox = document.getElementById("chat-box");
548
  const statusText = document.getElementById("status-text");
 
549
  const debugBox = document.getElementById("debug-box");
550
+ const newConversationBtn = document.getElementById("new-conversation-btn");
551
+ const conversationInfo = document.getElementById("conversation-info");
552
+ const exportPdfBtn = document.getElementById("export-pdf-btn");
553
+ const toggleDetailsBtn = document.getElementById("toggle-details-btn");
554
 
555
  // --- Model Configuration ---
556
  const GENERATOR_MODEL_NAME = "gemini-2.0-flash";
557
  const ANALYST_MODEL_NAME = "gemini-2.5-flash";
558
 
559
+ // --- Global State ---
560
+ let uploadedFiles = []; // Persistent file storage
561
+ let conversationHistory = []; // Store conversation messages
562
+ let conversationStartTime = Date.now();
563
+ let analysisDetailsVisible = false;
564
 
565
  // --- The Main Carver Analyst System Prompt ---
566
  const CARVER_ANALYST_SYSTEM_PROMPT = `
 
570
  ### 🎯 Mission
571
  Understand the provided data (CSV text, PDFs, images) and provide short, factual insights β€” **without making any assumptions**.
572
  If any required input is missing, unclear, or conflicting, you **must first ask a short, specific clarifying question** before proceeding. Never guess, invent, or assume.
573
+ Your goal is to make Carver's data analysis simple, reliable, and audit-ready.
574
  ### 🧩 Default Output (when all inputs are clear)
575
+ 1. **Executive Summary (3–6 lines)** β€” concise explanation answering the user's question.
576
  2. **Supporting Table (Markdown or CSV)** β€” key rows/columns only.
577
  3. **Audit Block (3–6 lines)** β€” list of: file(s) used (or file/sheet name for CSVs), cell/range references (or row/column names), missing or ignored fields, and formulas applied.
578
  Do **not** create or export a new Excel file unless the user explicitly asks for it.
 
582
  - Always confirm mappings if confidence <80%.
583
  **B. Required Data Rules**
584
  - For cost analysis β†’ need \`Qty\` + \`UnitPrice\` or \`Total\`.
585
+ - For "vs budget" β†’ need a \`Baseline\`, \`Budget\`, or explicit reference vendor.
586
  - If missing, ask user directly.
587
  **C. No-Assumptions Policy**
588
  - If anything is missing or unclear: Ask before proceeding.
589
+ - Example: "No 'Budget' column found. Should I use the lowest vendor total as baseline?"
590
  **D. Validations**
591
  - Detect and report missing values, currency mismatches, etc., in the Audit Block.
592
  **E. Computation Rules**
 
597
  **H. Tone**
598
  - Professional, precise, procurement-focused.
599
  ### 🧭 Objective
600
+ Act like Carver's in-house **Excel procurement analyst** β€” structured, audit-traceable, and transparent. Never make assumptions. Always verify.
601
+
602
+ IMPORTANT: This is a conversational chatbot. You have access to the conversation history and can refer back to previous analyses when answering follow-up questions.
603
  `;
604
 
605
  // --- Event Listeners ---
 
616
  fileInput.click();
617
  });
618
 
619
+ // Handle new conversation
620
+ newConversationBtn.addEventListener("click", startNewConversation);
621
+
622
+ // Handle PDF export
623
+ exportPdfBtn.addEventListener("click", exportToPDF);
624
+
625
+ // Handle analysis details toggle
626
+ toggleDetailsBtn.addEventListener("click", toggleAnalysisDetails);
627
+
628
+ // Update file pills when files are selected
629
  fileInput.addEventListener("change", (e) => {
630
+ const newFiles = Array.from(e.target.files);
631
+ uploadedFiles = [...uploadedFiles, ...newFiles]; // Append, don't replace
632
+ updateFileDisplay();
633
+ fileInput.value = ""; // Reset input
634
+
635
  if (uploadedFiles.length > 0) {
636
+ showSuccess(`πŸ“Ž Added ${newFiles.length} file(s). Total files: ${uploadedFiles.length}`);
637
+ conversationInfo.style.display = "block";
 
 
 
 
638
  }
639
  });
640
 
 
653
  return;
654
  }
655
  if (uploadedFiles.length === 0) {
656
+ displayError("Please upload at least one file first.");
657
  return;
658
  }
659
 
660
  // --- 2. Setup UI for Loading ---
661
+ setLoadingState(true, "Processing your question...");
662
  displayMessage(userPrompt, "user");
663
  debugBox.textContent = "Generating refined prompt..."; // Clear old debug info
 
664
 
665
  const assistantMessageEl = displayMessage("", "assistant"); // Create empty bubble
666
 
 
671
  );
672
  const fileParts = partsArrays.flat();
673
 
674
+ // --- 4. Build conversation context ---
675
+ let conversationContext = "";
676
+ if (conversationHistory.length > 0) {
677
+ conversationContext = `
678
+ Previous conversation context:
679
+ ${conversationHistory.slice(-6).map(msg =>
680
+ `${msg.role.toUpperCase()}: ${msg.content.substring(0, 200)}${msg.content.length > 200 ? '...' : ''}`
681
+ ).join('\n')}
682
+
683
+ `;
684
+ }
685
+
686
+ // --- 5. Initialize AI Client ---
687
  const genAI = new GoogleGenerativeAI(apiKey);
688
 
689
+ // --- 6. STEP 1: Generate the "Better Prompt" (Normal Model) ---
690
  setLoadingState(true, `Step 1/2: Thinking about your question... (${GENERATOR_MODEL_NAME})`);
691
 
692
  const generatorModel = genAI.getGenerativeModel({ model: GENERATOR_MODEL_NAME });
693
 
694
  const step1SystemPrompt = `
695
+ You are a prompt engineering assistant. Your job is to take a user's simple question and the attached file(s) and generate a new, detailed, and specific prompt. This new prompt will be given to a second AI, which is an expert 'Carver Excel Analyst'.
696
+ NOTE: The attached files may include images, PDFs, or TEXT parts that are CSV conversions of Excel sheets. The analyst is aware of this.
697
 
698
+ The user's question is: "${userPrompt}"
699
 
700
+ ${conversationContext}Based on the user's question and the file(s), generate a single, clear, and actionable instruction for the expert analyst.
701
+ - Be specific.
702
+ - Ask for the final output (summary, table, audit block) as defined by the analyst's role.
703
+ - Consider previous conversation context when generating the prompt.
704
+ - For example, if the user says "how much did I save?", you should generate a prompt like:
705
+ "Please analyze the attached data (CSV text, PDFs, images). Identify the baseline or budget, compare all vendor totals against it, and calculate the saving amount and percentage for the lowest-cost compliant vendor. Present this in an executive summary, a comparison table, and an audit block."
706
+ `;
707
 
708
  const step1Contents = [
709
  ...fileParts,
 
717
 
718
  // Display the refined prompt in the debug box
719
  debugBox.textContent = generatedPrompt;
720
+ analysisDetailsVisible = true;
721
+ updateDetailsVisibility();
722
 
723
+ // --- 7. STEP 2: Get Final Answer (Reasoning Model) ---
724
  setLoadingState(true, `Step 2/2: The Carver Analyst is working... (${ANALYST_MODEL_NAME})`);
725
 
726
  const analystModel = genAI.getGenerativeModel({
 
737
  contents: [{ parts: step2Contents }]
738
  });
739
 
740
+ // --- 8. Stream Response to Chat ---
741
  let fullResponse = "";
742
  for await (const chunk of result.stream) {
743
  if (typeof chunk.text === 'function') {
 
748
  }
749
  }
750
 
751
+ // --- 9. Save to conversation history ---
752
+ conversationHistory.push(
753
+ { role: "user", content: userPrompt },
754
+ { role: "assistant", content: fullResponse }
755
+ );
756
+
757
+ // Keep only last 20 messages to prevent context overflow
758
+ if (conversationHistory.length > 20) {
759
+ conversationHistory = conversationHistory.slice(-20);
760
+ }
761
+
762
  } catch (error) {
763
  console.error("Error:", error);
764
  displayError(`An error occurred: ${error.message}`);
765
  assistantMessageEl.remove(); // Remove the empty bubble
766
+ analysisDetailsVisible = false;
767
+ updateDetailsVisibility();
768
  } finally {
769
+ // --- 10. Cleanup UI ---
770
  setLoadingState(false);
771
  promptInput.value = "";
 
 
 
 
772
  }
773
  }
774
 
775
  // --- Helper Functions ---
776
 
777
+ /**
778
+ * Start a new conversation - clears history and files
779
+ */
780
+ function startNewConversation() {
781
+ if (confirm("Start a new conversation? This will clear all uploaded files and conversation history.")) {
782
+ uploadedFiles = [];
783
+ conversationHistory = [];
784
+ conversationStartTime = Date.now();
785
+
786
+ // Clear UI
787
+ chatBox.innerHTML = `
788
+ <div class="message assistant">
789
+ πŸ”„ <b>New conversation started!</b> Upload your files and ask questions about them.
790
+ </div>
791
+ `;
792
+
793
+ updateFileDisplay();
794
+ conversationInfo.style.display = "none";
795
+ promptInput.value = "";
796
+
797
+ showSuccess("Started new conversation!");
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Update file display pills
803
+ */
804
+ function updateFileDisplay() {
805
+ fileListDisplay.innerHTML = "";
806
+
807
+ if (uploadedFiles.length > 0) {
808
+ uploadedFiles.forEach((file, index) => {
809
+ const pill = document.createElement("span");
810
+ pill.className = "file-pill";
811
+ pill.innerHTML = `
812
+ ${file.name}
813
+ <button class="file-remove" onclick="removeFile(${index})" title="Remove file">Γ—</button>
814
+ `;
815
+ fileListDisplay.appendChild(pill);
816
+ });
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Remove a specific file
822
+ */
823
+ window.removeFile = function(index) {
824
+ if (confirm(`Remove "${uploadedFiles[index].name}"?`)) {
825
+ uploadedFiles.splice(index, 1);
826
+ updateFileDisplay();
827
+
828
+ if (uploadedFiles.length === 0) {
829
+ conversationInfo.style.display = "none";
830
+ showSuccess("All files removed");
831
+ } else {
832
+ showSuccess(`Removed file. ${uploadedFiles.length} file(s) remaining`);
833
+ }
834
+ }
835
+ };
836
+
837
  /**
838
  * Converts a File object to an array of GoogleGenerativeAI.Part objects.
839
  */
 
921
  return messageEl;
922
  }
923
 
924
+ /**
925
+ * Displays a success message in the chat
926
+ */
927
+ function showSuccess(text) {
928
+ const messageEl = document.createElement("div");
929
+ messageEl.className = "message success";
930
+ messageEl.textContent = text;
931
+
932
+ chatBox.appendChild(messageEl);
933
+ scrollToBottom();
934
+
935
+ // Auto-remove success message after 3 seconds
936
+ setTimeout(() => {
937
+ if (messageEl.parentNode) {
938
+ messageEl.remove();
939
+ }
940
+ }, 3000);
941
+ }
942
+
943
  /**
944
  * Displays an error message in the chat
945
  */
 
955
  chatBox.scrollTop = chatBox.scrollHeight;
956
  }
957
 
958
+ /**
959
+ * Toggle analysis details visibility
960
+ */
961
+ function toggleAnalysisDetails() {
962
+ analysisDetailsVisible = !analysisDetailsVisible;
963
+ updateDetailsVisibility();
964
+ }
965
+
966
+ /**
967
+ * Update analysis details visibility
968
+ */
969
+ function updateDetailsVisibility() {
970
+ if (analysisDetailsVisible) {
971
+ debugBox.style.display = 'block';
972
+ toggleDetailsBtn.innerHTML = 'πŸ‘οΈ Hide Analysis Details';
973
+ } else {
974
+ debugBox.style.display = 'none';
975
+ toggleDetailsBtn.innerHTML = 'πŸ‘οΈ Show Analysis Details';
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Export conversation to PDF
981
+ */
982
+ function exportToPDF() {
983
+ if (conversationHistory.length === 0) {
984
+ displayError("No conversation to export.");
985
+ return;
986
+ }
987
+
988
+ try {
989
+ // Create HTML content for PDF
990
+ const currentDate = new Date().toLocaleDateString();
991
+ const timestamp = new Date().toLocaleTimeString();
992
+ let htmlContent = `
993
+ <!DOCTYPE html>
994
+ <html>
995
+ <head>
996
+ <meta charset="UTF-8">
997
+ <title>Carver Excel Analyst - Export</title>
998
+ <style>
999
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
1000
+
1001
+ * {
1002
+ margin: 0;
1003
+ padding: 0;
1004
+ box-sizing: border-box;
1005
+ }
1006
+
1007
+ body {
1008
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
1009
+ line-height: 1.7;
1010
+ color: #1a202c;
1011
+ background-color: #f7fafc;
1012
+ font-size: 14px;
1013
+ }
1014
+
1015
+ .page {
1016
+ background: white;
1017
+ max-width: 900px;
1018
+ margin: 20px auto;
1019
+ padding: 60px;
1020
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
1021
+ border-radius: 8px;
1022
+ min-height: 800px;
1023
+ }
1024
+
1025
+ /* Header Styles */
1026
+ .header {
1027
+ text-align: center;
1028
+ margin-bottom: 50px;
1029
+ padding-bottom: 30px;
1030
+ border-bottom: 3px solid #e2e8f0;
1031
+ position: relative;
1032
+ }
1033
+
1034
+ .header::after {
1035
+ content: '';
1036
+ position: absolute;
1037
+ bottom: -3px;
1038
+ left: 50%;
1039
+ transform: translateX(-50%);
1040
+ width: 80px;
1041
+ height: 3px;
1042
+ background: linear-gradient(135deg, #3182ce, #63b3ed);
1043
+ }
1044
+
1045
+ .header h1 {
1046
+ color: #2d3748;
1047
+ font-size: 2.5rem;
1048
+ font-weight: 700;
1049
+ margin-bottom: 10px;
1050
+ letter-spacing: -0.025em;
1051
+ }
1052
+
1053
+ .header .subtitle {
1054
+ color: #718096;
1055
+ font-size: 1.1rem;
1056
+ font-weight: 400;
1057
+ margin-bottom: 15px;
1058
+ }
1059
+
1060
+ .header .metadata {
1061
+ display: flex;
1062
+ justify-content: center;
1063
+ gap: 30px;
1064
+ margin-top: 20px;
1065
+ font-size: 0.9rem;
1066
+ color: #a0aec0;
1067
+ }
1068
+
1069
+ .metadata-item {
1070
+ display: flex;
1071
+ align-items: center;
1072
+ gap: 5px;
1073
+ }
1074
+
1075
+ /* Files Section */
1076
+ .files-section {
1077
+ background: linear-gradient(135deg, #f7fafc, #edf2f7);
1078
+ border: 2px solid #e2e8f0;
1079
+ border-radius: 12px;
1080
+ padding: 25px;
1081
+ margin-bottom: 40px;
1082
+ }
1083
+
1084
+ .files-section h2 {
1085
+ color: #2d3748;
1086
+ font-size: 1.4rem;
1087
+ font-weight: 600;
1088
+ margin-bottom: 20px;
1089
+ display: flex;
1090
+ align-items: center;
1091
+ gap: 10px;
1092
+ }
1093
+
1094
+ .files-grid {
1095
+ display: grid;
1096
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
1097
+ gap: 15px;
1098
+ margin-top: 15px;
1099
+ }
1100
+
1101
+ .file-card {
1102
+ background: white;
1103
+ padding: 15px 20px;
1104
+ border-radius: 8px;
1105
+ border: 1px solid #e2e8f0;
1106
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
1107
+ transition: all 0.2s ease;
1108
+ }
1109
+
1110
+ .file-card:hover {
1111
+ transform: translateY(-2px);
1112
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
1113
+ }
1114
+
1115
+ .file-name {
1116
+ font-weight: 600;
1117
+ color: #2d3748;
1118
+ margin-bottom: 5px;
1119
+ }
1120
+
1121
+ .file-size {
1122
+ color: #718096;
1123
+ font-size: 0.85rem;
1124
+ }
1125
+
1126
+ /* Conversation Entries */
1127
+ .conversation-section {
1128
+ margin-bottom: 40px;
1129
+ }
1130
+
1131
+ .conversation-entry {
1132
+ background: white;
1133
+ border-radius: 12px;
1134
+ margin-bottom: 30px;
1135
+ overflow: hidden;
1136
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
1137
+ border: 1px solid #e2e8f0;
1138
+ }
1139
+
1140
+ .user-entry {
1141
+ border-left: 4px solid #3182ce;
1142
+ }
1143
+
1144
+ .assistant-entry {
1145
+ border-left: 4px solid #38a169;
1146
+ }
1147
+
1148
+ .entry-header {
1149
+ padding: 20px 25px 15px;
1150
+ display: flex;
1151
+ align-items: center;
1152
+ gap: 12px;
1153
+ background: linear-gradient(135deg, #f7fafc, #edf2f7);
1154
+ }
1155
+
1156
+ .user-entry .entry-header {
1157
+ background: linear-gradient(135deg, #ebf8ff, #bee3f8);
1158
+ }
1159
+
1160
+ .assistant-entry .entry-header {
1161
+ background: linear-gradient(135deg, #f0fff4, #c6f6d5);
1162
+ }
1163
+
1164
+ .role-icon {
1165
+ width: 32px;
1166
+ height: 32px;
1167
+ border-radius: 50%;
1168
+ display: flex;
1169
+ align-items: center;
1170
+ justify-content: center;
1171
+ font-weight: 600;
1172
+ font-size: 0.8rem;
1173
+ color: white;
1174
+ }
1175
+
1176
+ .user-entry .role-icon {
1177
+ background: #3182ce;
1178
+ }
1179
+
1180
+ .assistant-entry .role-icon {
1181
+ background: #38a169;
1182
+ }
1183
+
1184
+ .role-text {
1185
+ font-weight: 600;
1186
+ font-size: 1.1rem;
1187
+ color: #2d3748;
1188
+ }
1189
+
1190
+ .timestamp {
1191
+ margin-left: auto;
1192
+ color: #718096;
1193
+ font-size: 0.85rem;
1194
+ }
1195
+
1196
+ .entry-content {
1197
+ padding: 25px;
1198
+ background: white;
1199
+ }
1200
+
1201
+ /* Message Content Styling */
1202
+ .message-content {
1203
+ font-size: 0.95rem;
1204
+ line-height: 1.8;
1205
+ }
1206
+
1207
+ .user-entry .message-content {
1208
+ color: #2d3748;
1209
+ }
1210
+
1211
+ .assistant-entry .message-content {
1212
+ color: #2d3748;
1213
+ }
1214
+
1215
+ /* Markdown Styling */
1216
+ .message-content h1, .message-content h2, .message-content h3 {
1217
+ color: #2d3748;
1218
+ margin-top: 1.5em;
1219
+ margin-bottom: 0.8em;
1220
+ font-weight: 600;
1221
+ }
1222
+
1223
+ .message-content h1 {
1224
+ font-size: 1.4rem;
1225
+ border-bottom: 2px solid #e2e8f0;
1226
+ padding-bottom: 8px;
1227
+ }
1228
+
1229
+ .message-content h2 {
1230
+ font-size: 1.25rem;
1231
+ color: #3182ce;
1232
+ }
1233
+
1234
+ .message-content h3 {
1235
+ font-size: 1.1rem;
1236
+ color: #38a169;
1237
+ }
1238
+
1239
+ .message-content strong {
1240
+ font-weight: 600;
1241
+ color: #2d3748;
1242
+ }
1243
+
1244
+ .message-content table {
1245
+ width: 100%;
1246
+ border-collapse: collapse;
1247
+ margin: 20px 0;
1248
+ font-size: 0.9rem;
1249
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1250
+ border-radius: 8px;
1251
+ overflow: hidden;
1252
+ }
1253
+
1254
+ .message-content th {
1255
+ background: linear-gradient(135deg, #3182ce, #63b3ed);
1256
+ color: white;
1257
+ padding: 15px 12px;
1258
+ text-align: left;
1259
+ font-weight: 600;
1260
+ }
1261
+
1262
+ .message-content td {
1263
+ padding: 12px;
1264
+ border-bottom: 1px solid #e2e8f0;
1265
+ background: white;
1266
+ }
1267
+
1268
+ .message-content tr:nth-child(even) td {
1269
+ background: #f7fafc;
1270
+ }
1271
+
1272
+ .message-content tr:hover td {
1273
+ background: #edf2f7;
1274
+ }
1275
+
1276
+ .message-content code {
1277
+ background: #f7fafc;
1278
+ padding: 3px 8px;
1279
+ border-radius: 4px;
1280
+ font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
1281
+ font-size: 0.85rem;
1282
+ color: #d53f8c;
1283
+ border: 1px solid #e2e8f0;
1284
+ }
1285
+
1286
+ .message-content pre {
1287
+ background: #1a202c;
1288
+ color: #f7fafc;
1289
+ padding: 20px;
1290
+ border-radius: 8px;
1291
+ overflow-x: auto;
1292
+ margin: 20px 0;
1293
+ border: 1px solid #2d3748;
1294
+ }
1295
+
1296
+ .message-content pre code {
1297
+ background: transparent;
1298
+ padding: 0;
1299
+ color: #f7fafc;
1300
+ border: none;
1301
+ font-size: 0.85rem;
1302
+ }
1303
+
1304
+ .message-content blockquote {
1305
+ border-left: 4px solid #3182ce;
1306
+ padding-left: 20px;
1307
+ margin: 20px 0;
1308
+ background: #f7fafc;
1309
+ padding: 15px 20px;
1310
+ border-radius: 0 8px 8px 0;
1311
+ color: #4a5568;
1312
+ }
1313
+
1314
+ /* Audit Block Styling */
1315
+ .audit-block {
1316
+ background: linear-gradient(135deg, #fed7d7, #feb2b2);
1317
+ border: 2px solid #fc8181;
1318
+ border-radius: 8px;
1319
+ padding: 20px;
1320
+ margin-top: 25px;
1321
+ }
1322
+
1323
+ .audit-block h3 {
1324
+ color: #c53030;
1325
+ margin-top: 0;
1326
+ margin-bottom: 15px;
1327
+ font-weight: 600;
1328
+ }
1329
+
1330
+ .audit-block ul {
1331
+ margin-left: 20px;
1332
+ }
1333
+
1334
+ .audit-block li {
1335
+ margin-bottom: 5px;
1336
+ }
1337
+
1338
+ /* Print Styles */
1339
+ @media print {
1340
+ body {
1341
+ background: white;
1342
+ }
1343
+
1344
+ .page {
1345
+ margin: 0;
1346
+ padding: 40px;
1347
+ box-shadow: none;
1348
+ border-radius: 0;
1349
+ }
1350
+
1351
+ .conversation-entry {
1352
+ break-inside: avoid;
1353
+ page-break-inside: avoid;
1354
+ }
1355
+ }
1356
+
1357
+ /* Page Breaks */
1358
+ .page-break {
1359
+ page-break-before: always;
1360
+ }
1361
+ </style>
1362
+ </head>
1363
+ <body>
1364
+ <div class="page">
1365
+ <div class="header">
1366
+ <h1>🧠 Carver Excel Analyst</h1>
1367
+ <div class="subtitle">Professional Data Analysis Report</div>
1368
+ <div class="metadata">
1369
+ <div class="metadata-item">
1370
+ <span>πŸ“…</span>
1371
+ <span>${currentDate}</span>
1372
+ </div>
1373
+ <div class="metadata-item">
1374
+ <span>πŸ•’</span>
1375
+ <span>${timestamp}</span>
1376
+ </div>
1377
+ <div class="metadata-item">
1378
+ <span>πŸ’¬</span>
1379
+ <span>${conversationHistory.length} messages</span>
1380
+ </div>
1381
+ </div>
1382
+ </div>
1383
+
1384
+ <div class="files-section">
1385
+ <h2>πŸ“Ž Analyzed Files</h2>
1386
+ <div class="files-grid">
1387
+ `;
1388
+
1389
+ uploadedFiles.forEach(file => {
1390
+ htmlContent += `
1391
+ <div class="file-card">
1392
+ <div class="file-name">${file.name}</div>
1393
+ <div class="file-size">${(file.size / 1024).toFixed(1)} KB</div>
1394
+ </div>
1395
+ `;
1396
+ });
1397
+
1398
+ htmlContent += `
1399
+ </div>
1400
+ </div>
1401
+
1402
+ <div class="conversation-section">
1403
+ <h2>πŸ’¬ Conversation Analysis</h2>
1404
+ `;
1405
+
1406
+ conversationHistory.forEach((message, index) => {
1407
+ const roleClass = message.role === 'user' ? 'user-entry' : 'assistant-entry';
1408
+ const roleIcon = message.role === 'user' ? 'πŸ‘€' : 'πŸ€–';
1409
+ const roleText = message.role === 'user' ? 'User' : 'Carver Analyst';
1410
+ const messageTime = new Date(Date.now() - (conversationHistory.length - index) * 1000).toLocaleTimeString();
1411
+
1412
+ htmlContent += `
1413
+ <div class="conversation-entry ${roleClass}">
1414
+ <div class="entry-header">
1415
+ <div class="role-icon">${roleIcon}</div>
1416
+ <div class="role-text">${roleText}</div>
1417
+ <div class="timestamp">${messageTime}</div>
1418
+ </div>
1419
+ <div class="entry-content">
1420
+ <div class="message-content">
1421
+ ${marked.parse(message.content)}
1422
+ </div>
1423
+ </div>
1424
+ </div>
1425
+ `;
1426
+ });
1427
+
1428
+ htmlContent += `
1429
+ </div>
1430
+ </div>
1431
+ </body>
1432
+ </html>
1433
+ `;
1434
+
1435
+ // Create and trigger download
1436
+ const blob = new Blob([htmlContent], { type: 'text/html' });
1437
+ const url = URL.createObjectURL(blob);
1438
+ const a = document.createElement('a');
1439
+ a.href = url;
1440
+ a.download = `carver-analyst-report-${new Date().toISOString().split('T')[0]}.html`;
1441
+ document.body.appendChild(a);
1442
+ a.click();
1443
+ document.body.removeChild(a);
1444
+ URL.revokeObjectURL(url);
1445
+
1446
+ showSuccess("βœ… Professional report exported! Open the HTML file and print as PDF.");
1447
+
1448
+ // Show instructions for PDF conversion
1449
+ setTimeout(() => {
1450
+ showSuccess("πŸ’‘ To create PDF: Open the file β†’ Print (Ctrl/Cmd+P) β†’ Save as PDF");
1451
+ }, 2000);
1452
+
1453
+ } catch (error) {
1454
+ console.error("Export error:", error);
1455
+ displayError(`Export failed: ${error.message}`);
1456
+ }
1457
+ }
1458
+
1459
  </script>
1460
  </body>
1461
  </html>