Raykarr commited on
Commit
f67222e
·
verified ·
1 Parent(s): 5f88637

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +721 -19
index.html CHANGED
@@ -1,19 +1,721 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Carver Excel Analyst</title>
7
+
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
11
+
12
+ <script type="module" src="https://esm.run/@google/generative-ai"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
14
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
15
+
16
+ <style>
17
+ /* --- CSS Reset & Base --- */
18
+ :root {
19
+ --font-main: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
20
+ --color-bg: #f8f9fa;
21
+ --color-card: #ffffff;
22
+ --color-border: #e9ecef;
23
+ --color-text: #212529;
24
+ --color-text-light: #6c757d;
25
+ --color-accent: #0d6efd;
26
+ --color-user-msg: #0d6efd;
27
+ --color-user-text: #ffffff;
28
+ --color-assist-msg: #f1f3f5;
29
+ --color-assist-text: #212529;
30
+ --color-danger: #d90429;
31
+ --color-danger-bg: #fff0f0;
32
+ }
33
+
34
+ * {
35
+ box-sizing: border-box;
36
+ margin: 0;
37
+ padding: 0;
38
+ }
39
+
40
+ body {
41
+ font-family: var(--font-main);
42
+ background-color: var(--color-bg);
43
+ color: var(--color-text);
44
+ display: flex;
45
+ justify-content: center;
46
+ align-items: flex-start;
47
+ min-height: 100vh;
48
+ padding: 20px;
49
+ }
50
+
51
+ /* --- Main App Container --- */
52
+ .container {
53
+ width: 100%;
54
+ max-width: 900px;
55
+ background-color: var(--color-card);
56
+ border-radius: 12px;
57
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.05);
58
+ overflow: hidden;
59
+ display: flex;
60
+ flex-direction: column;
61
+ height: calc(100vh - 40px);
62
+ border: 1px solid var(--color-border);
63
+ }
64
+
65
+ /* --- Header --- */
66
+ header {
67
+ padding: 20px 24px;
68
+ border-bottom: 1px solid var(--color-border);
69
+ background-color: var(--color-card);
70
+ }
71
+
72
+ h1 {
73
+ margin: 0;
74
+ color: var(--color-text);
75
+ font-size: 1.4rem;
76
+ font-weight: 600;
77
+ }
78
+
79
+ p.caption {
80
+ margin: 4px 0 0;
81
+ font-size: 0.9rem;
82
+ color: var(--color-text-light);
83
+ }
84
+
85
+ /* --- Config Section (API Key) --- */
86
+ .config-bar {
87
+ padding: 16px 24px;
88
+ background-color: #fdfdfe;
89
+ border-bottom: 1px solid var(--color-border);
90
+ }
91
+
92
+ .api-key-group {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 10px;
96
+ }
97
+
98
+ .api-key-group label {
99
+ font-weight: 500;
100
+ font-size: 0.9rem;
101
+ }
102
+
103
+ .api-key-group input {
104
+ flex-grow: 1;
105
+ padding: 8px 12px;
106
+ border: 1px solid var(--color-border);
107
+ border-radius: 6px;
108
+ font-family: var(--font-main);
109
+ font-size: 0.9rem;
110
+ }
111
+
112
+ .api-key-warning {
113
+ font-size: 0.8rem;
114
+ color: #e74c3c;
115
+ font-weight: 500;
116
+ margin-top: 8px;
117
+ }
118
+
119
+ /* --- Chat Box --- */
120
+ #chat-box {
121
+ flex-grow: 1;
122
+ padding: 24px;
123
+ overflow-y: auto;
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: 16px;
127
+ /* Subtle dot background */
128
+ background-image: radial-gradient(var(--color-border) 1px, transparent 1px);
129
+ background-size: 10px 10px;
130
+ background-color: #ffffff;
131
+ }
132
+
133
+ /* Minimal Scrollbar */
134
+ #chat-box::-webkit-scrollbar {
135
+ width: 6px;
136
+ }
137
+ #chat-box::-webkit-scrollbar-track {
138
+ background: transparent;
139
+ }
140
+ #chat-box::-webkit-scrollbar-thumb {
141
+ background: var(--color-border);
142
+ border-radius: 3px;
143
+ }
144
+ #chat-box::-webkit-scrollbar-thumb:hover {
145
+ background: #d0d5db;
146
+ }
147
+
148
+ .message {
149
+ padding: 14px 20px;
150
+ border-radius: 18px;
151
+ max-width: 85%;
152
+ line-height: 1.6;
153
+ word-wrap: break-word;
154
+ font-size: 0.95rem;
155
+ }
156
+
157
+ .message.user {
158
+ background-color: var(--color-user-msg);
159
+ color: var(--color-user-text);
160
+ border-bottom-right-radius: 6px;
161
+ align-self: flex-end;
162
+ }
163
+
164
+ .message.assistant {
165
+ background-color: var(--color-assist-msg);
166
+ color: var(--color-assist-text);
167
+ border-bottom-left-radius: 6px;
168
+ align-self: flex-start;
169
+ }
170
+
171
+ .message.error {
172
+ background-color: var(--color-danger-bg);
173
+ color: var(--color-danger);
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 {
180
+ margin-top: 1.2em;
181
+ margin-bottom: 0.6em;
182
+ border-bottom: 1px solid var(--color-border);
183
+ padding-bottom: 5px;
184
+ font-weight: 600;
185
+ }
186
+ .message.assistant code {
187
+ font-family: monospace;
188
+ background-color: #dfe7ed;
189
+ padding: 2px 6px;
190
+ border-radius: 4px;
191
+ font-size: 0.9em;
192
+ }
193
+ .message.assistant pre {
194
+ background-color: #2c3e50;
195
+ color: #f4f7f6;
196
+ padding: 16px;
197
+ border-radius: 8px;
198
+ overflow-x: auto;
199
+ margin: 1em 0;
200
+ }
201
+ .message.assistant pre code {
202
+ background-color: transparent;
203
+ padding: 0;
204
+ font-size: 0.85rem;
205
+ }
206
+ .message.assistant table {
207
+ border-collapse: collapse;
208
+ width: 100%;
209
+ margin: 1em 0;
210
+ font-size: 0.9rem;
211
+ }
212
+ .message.assistant th, .message.assistant td {
213
+ border: 1px solid var(--color-border);
214
+ padding: 10px;
215
+ text-align: left;
216
+ }
217
+ .message.assistant th {
218
+ background-color: var(--color-assist-msg);
219
+ font-weight: 600;
220
+ }
221
+
222
+ /* --- Input Area --- */
223
+ .input-area {
224
+ border-top: 1px solid var(--color-border);
225
+ padding: 16px 24px;
226
+ background-color: var(--color-card);
227
+ }
228
+
229
+ #file-list {
230
+ display: flex;
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;
241
+ border-radius: 15px;
242
+ font-size: 0.8rem;
243
+ font-weight: 500;
244
+ }
245
+
246
+ .prompt-input-group {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 10px;
250
+ border: 1px solid var(--color-border);
251
+ border-radius: 25px;
252
+ padding: 4px 4px 4px 12px;
253
+ }
254
+ .prompt-input-group:focus-within {
255
+ border-color: var(--color-accent);
256
+ box-shadow: 0 0 0 3px rgba(13,110,253,0.1);
257
+ }
258
+
259
+ #prompt-input {
260
+ flex-grow: 1;
261
+ border: none;
262
+ outline: none;
263
+ background: transparent;
264
+ font-family: var(--font-main);
265
+ font-size: 1rem;
266
+ padding: 8px;
267
+ }
268
+
269
+ #file-input {
270
+ display: none;
271
+ }
272
+
273
+ .icon-button {
274
+ background-color: transparent;
275
+ border: none;
276
+ cursor: pointer;
277
+ padding: 8px;
278
+ border-radius: 50%;
279
+ display: flex;
280
+ align-items: center;
281
+ justify-content: center;
282
+ transition: background-color 0.2s;
283
+ }
284
+ .icon-button svg {
285
+ width: 20px;
286
+ height: 20px;
287
+ fill: var(--color-text-light);
288
+ }
289
+ .icon-button:hover {
290
+ background-color: var(--color-assist-msg);
291
+ }
292
+
293
+ #send-button {
294
+ background-color: var(--color-accent);
295
+ }
296
+ #send-button svg {
297
+ fill: var(--color-user-text);
298
+ }
299
+ #send-button:hover {
300
+ background-color: #0b5ed7;
301
+ }
302
+ #send-button:disabled {
303
+ background-color: #a0c7e4;
304
+ cursor: not-allowed;
305
+ }
306
+
307
+ /* --- Status & Loading --- */
308
+ #status-text {
309
+ display: block;
310
+ text-align: center;
311
+ font-size: 0.85rem;
312
+ color: var(--color-text-light);
313
+ margin-bottom: 12px;
314
+ height: 1.2em;
315
+ }
316
+
317
+ .spinner {
318
+ display: inline-block;
319
+ width: 1em;
320
+ height: 1em;
321
+ border: 2px solid currentColor;
322
+ border-right-color: transparent;
323
+ border-radius: 50%;
324
+ animation: spin 0.6s linear infinite;
325
+ margin-bottom: -3px;
326
+ margin-right: 5px;
327
+ }
328
+ @keyframes spin {
329
+ to { transform: rotate(360deg); }
330
+ }
331
+
332
+ /* --- Debug/Refined Prompt Area --- */
333
+ .debug-container {
334
+ padding: 12px 24px 4px;
335
+ }
336
+
337
+ .debug-container summary {
338
+ font-size: 0.8rem;
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;
354
+ max-height: 200px;
355
+ overflow-y: auto;
356
+ white-space: pre-wrap;
357
+ word-wrap: break-word;
358
+ }
359
+
360
+ </style>
361
+ </head>
362
+ <body>
363
+
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">
371
+ <div class="api-key-group">
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
+
386
+ <div class="input-area">
387
+ <div id="status-text"></div>
388
+
389
+ <div id="file-list">
390
+ </div>
391
+
392
+ <div class="prompt-input-group">
393
+ <input type="file" id="file-input" multiple>
394
+ <button class="icon-button" id="attach-button" title="Upload files">
395
+ <svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v11.5c0 2.76 2.24 5 5 5s5-2.24 5-5V6h-1.5z"></path></svg>
396
+ </button>
397
+
398
+ <input type="text" id="prompt-input" placeholder="Ask a question about your files...">
399
+
400
+ <button class="icon-button" id="send-button" title="Send message">
401
+ <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2 .01 7z"></path></svg>
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>
413
+
414
+ <script type="module">
415
+ // --- Import Generative AI SDK ---
416
+ const { GoogleGenerativeAI } = await import("https://esm.run/@google/generative-ai");
417
+
418
+ // --- DOM Element References ---
419
+ const apiKeyInput = document.getElementById("api-key-input");
420
+ const fileInput = document.getElementById("file-input");
421
+ const attachButton = document.getElementById("attach-button");
422
+ const fileListDisplay = document.getElementById("file-list");
423
+ const promptInput = document.getElementById("prompt-input");
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 = `
439
+ You are **Carver Excel Analyst**, an internal AI specialist for Carver Procurement Consultancy.
440
+ Your role: Help Carver team members analyze Excel sheets (e.g., BOQs, comparative sheets, quotations, price lists, tender data) and produce clear summaries and structured tables.
441
+ NOTE: Excel files (.xlsx, .xls) have been pre-converted into structured CSV text, with each sheet as a separate text block, prefixed with [File: filename.xlsx | Sheet: SheetName]. Use this text data for your analysis.
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.
451
+ ### 🧱 Working Logic
452
+ **A. File Understanding**
453
+ - Detect columns and datatypes from the CSV text or tables in PDFs/images. Map columns like *Vendor*, *Item*, *Qty*, *Rate/Unit Price*, *Total*, *Lead Time*, *Remarks*, etc.
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**
465
+ - \`Saving = Baseline – Selected\`
466
+ - \`Saving% = (Baseline – Selected) / Baseline × 100\`
467
+ **F. Output Formatting**
468
+ - Keep summary short. Use markdown tables. End every result with an **Audit Block**.
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 ---
476
+ sendButton.addEventListener("click", handleSend);
477
+ promptInput.addEventListener("keydown", (e) => {
478
+ if (e.key === "Enter" && !e.shiftKey) {
479
+ e.preventDefault();
480
+ handleSend();
481
+ }
482
+ });
483
+
484
+ // Trigger hidden file input
485
+ attachButton.addEventListener("click", () => {
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
+
503
+ // --- Main Function: handleSend ---
504
+ async function handleSend() {
505
+ const apiKey = apiKeyInput.value.trim();
506
+ const userPrompt = promptInput.value.trim();
507
+
508
+ // --- 1. Validations ---
509
+ if (!apiKey) {
510
+ displayError("Please enter your Google API Key.");
511
+ return;
512
+ }
513
+ if (!userPrompt) {
514
+ displayError("Please enter a prompt.");
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
+
530
+ try {
531
+ // --- 3. Process Files into Generative Parts ---
532
+ const partsArrays = await Promise.all(
533
+ uploadedFiles.map(fileToGenerativeParts)
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,
560
+ { text: step1SystemPrompt }
561
+ ];
562
+
563
+ const resultStep1 = await generatorModel.generateContent({
564
+ contents: [{ parts: step1Contents }]
565
+ });
566
+ const generatedPrompt = resultStep1.response.text();
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({
576
+ model: ANALYST_MODEL_NAME,
577
+ systemInstruction: CARVER_ANALYST_SYSTEM_PROMPT
578
+ });
579
+
580
+ const step2Contents = [
581
+ ...fileParts,
582
+ { text: generatedPrompt }
583
+ ];
584
+
585
+ const result = await analystModel.generateContentStream({
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') {
593
+ const chunkText = chunk.text();
594
+ fullResponse += chunkText;
595
+ assistantMessageEl.innerHTML = marked.parse(fullResponse);
596
+ scrollToBottom();
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
+ */
620
+ async function fileToGenerativeParts(file) {
621
+ const excelMimeTypes = [
622
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
623
+ 'application/vnd.ms-excel' // .xls
624
+ ];
625
+
626
+ if (excelMimeTypes.includes(file.type) || file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
627
+ try {
628
+ const buffer = await file.arrayBuffer();
629
+ const workbook = XLSX.read(buffer, { type: 'buffer' });
630
+ const textParts = [];
631
+
632
+ workbook.SheetNames.forEach(sheetName => {
633
+ const worksheet = workbook.Sheets[sheetName];
634
+ const csvData = XLSX.utils.sheet_to_csv(worksheet);
635
+ const partHeader = `[File: ${file.name} | Sheet: ${sheetName}]\n\n`;
636
+ textParts.push({ text: partHeader + csvData });
637
+ });
638
+
639
+ return textParts;
640
+ } catch (error) {
641
+ console.error("Error reading Excel file:", error);
642
+ return [{ text: `[Error processing Excel file ${file.name}: ${error.message}]` }];
643
+ }
644
+ }
645
+ else { // For all other files (PDF, images, etc.)
646
+ return new Promise((resolve, reject) => {
647
+ const reader = new FileReader();
648
+ reader.readAsDataURL(file);
649
+ reader.onload = () => {
650
+ const dataUrl = reader.result;
651
+ const base64Data = dataUrl.split(',')[1];
652
+ const mimeType = dataUrl.split(';')[0].split(':')[1];
653
+
654
+ resolve([{
655
+ inlineData: {
656
+ data: base64Data,
657
+ mimeType: mimeType
658
+ }
659
+ }]);
660
+ };
661
+ reader.onerror = (error) => reject(error);
662
+ });
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Sets the loading state of the UI
668
+ */
669
+ function setLoadingState(isLoading, message = "") {
670
+ if (isLoading) {
671
+ statusText.innerHTML = `<span class="spinner"></span> ${message}`;
672
+ sendButton.disabled = true;
673
+ promptInput.disabled = true;
674
+ attachButton.disabled = true;
675
+ } else {
676
+ statusText.innerHTML = "";
677
+ sendButton.disabled = false;
678
+ promptInput.disabled = false;
679
+ attachButton.disabled = false;
680
+ promptInput.focus();
681
+ }
682
+ }
683
+
684
+ /**
685
+ * Displays a message in the chat box
686
+ */
687
+ function displayMessage(text, role) {
688
+ const messageEl = document.createElement("div");
689
+ messageEl.classList.add("message", role);
690
+
691
+ if (role === 'error') {
692
+ messageEl.textContent = text;
693
+ } else if (role === 'assistant') {
694
+ messageEl.innerHTML = marked.parse(text || "..."); // Show loading dots
695
+ } else {
696
+ messageEl.textContent = text; // Plain text for user
697
+ }
698
+
699
+ chatBox.appendChild(messageEl);
700
+ scrollToBottom();
701
+ return messageEl;
702
+ }
703
+
704
+ /**
705
+ * Displays an error message in the chat
706
+ */
707
+ function displayError(text) {
708
+ displayMessage(text, "error");
709
+ setLoadingState(false);
710
+ }
711
+
712
+ /**
713
+ * Scrolls the chat box to the bottom
714
+ */
715
+ function scrollToBottom() {
716
+ chatBox.scrollTop = chatBox.scrollHeight;
717
+ }
718
+
719
+ </script>
720
+ </body>
721
+ </html>