|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Carver Excel Analyst</title> |
|
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> |
|
|
|
|
|
<script type="module" src="https://esm.run/@google/generative-ai"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> |
|
|
|
|
|
<style> |
|
|
|
|
|
:root { |
|
|
--font-main: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
|
|
--color-bg: #f8f9fa; |
|
|
--color-card: #ffffff; |
|
|
--color-border: #e9ecef; |
|
|
--color-text: #212529; |
|
|
--color-text-light: #6c757d; |
|
|
--color-accent: #0d6efd; |
|
|
--color-user-msg: #0d6efd; |
|
|
--color-user-text: #ffffff; |
|
|
--color-assist-msg: #f1f3f5; |
|
|
--color-assist-text: #212529; |
|
|
--color-danger: #d90429; |
|
|
--color-danger-bg: #fff0f0; |
|
|
--color-success: #28a745; |
|
|
--color-success-bg: #f0fff4; |
|
|
} |
|
|
|
|
|
* { |
|
|
box-sizing: border-box; |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: var(--font-main); |
|
|
background-color: var(--color-bg); |
|
|
color: var(--color-text); |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: flex-start; |
|
|
min-height: 100vh; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
|
|
|
.container { |
|
|
width: 100%; |
|
|
max-width: 1500 px; |
|
|
background-color: var(--color-card); |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.05); |
|
|
overflow: hidden; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
height: calc(100vh - 40px); |
|
|
border: 1px solid var(--color-border); |
|
|
} |
|
|
|
|
|
|
|
|
header { |
|
|
padding: 20px 24px; |
|
|
border-bottom: 1px solid var(--color-border); |
|
|
background-color: var(--color-card); |
|
|
} |
|
|
|
|
|
h1 { |
|
|
margin: 0; |
|
|
color: var(--color-text); |
|
|
font-size: 1.4rem; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
p.caption { |
|
|
margin: 4px 0 0; |
|
|
font-size: 0.9rem; |
|
|
color: var(--color-text-light); |
|
|
} |
|
|
|
|
|
|
|
|
.config-bar { |
|
|
padding: 16px 24px; |
|
|
background-color: #fdfdfe; |
|
|
border-bottom: 1px solid var(--color-border); |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.api-key-group { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.api-key-group label { |
|
|
font-weight: 500; |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
|
|
|
.api-key-group input { |
|
|
flex-grow: 1; |
|
|
padding: 8px 12px; |
|
|
border: 1px solid var(--color-border); |
|
|
border-radius: 6px; |
|
|
font-family: var(--font-main); |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
|
|
|
.api-key-warning { |
|
|
font-size: 0.8rem; |
|
|
color: #e74c3c; |
|
|
font-weight: 500; |
|
|
margin-top: 8px; |
|
|
} |
|
|
|
|
|
|
|
|
.header-actions { |
|
|
display: flex; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.action-button { |
|
|
padding: 6px 12px; |
|
|
border: 1px solid var(--color-border); |
|
|
background-color: var(--color-card); |
|
|
border-radius: 6px; |
|
|
font-size: 0.85rem; |
|
|
cursor: pointer; |
|
|
font-family: var(--font-main); |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.action-button:hover { |
|
|
background-color: var(--color-assist-msg); |
|
|
border-color: var(--color-accent); |
|
|
} |
|
|
|
|
|
.action-button.danger { |
|
|
color: var(--color-danger); |
|
|
border-color: var(--color-danger); |
|
|
} |
|
|
|
|
|
.action-button.danger:hover { |
|
|
background-color: var(--color-danger-bg); |
|
|
} |
|
|
|
|
|
|
|
|
#chat-box { |
|
|
flex-grow: 1; |
|
|
padding: 24px; |
|
|
overflow-y: auto; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 16px; |
|
|
|
|
|
background-image: radial-gradient(var(--color-border) 1px, transparent 1px); |
|
|
background-size: 10px 10px; |
|
|
background-color: #ffffff; |
|
|
} |
|
|
|
|
|
|
|
|
#chat-box::-webkit-scrollbar { |
|
|
width: 6px; |
|
|
} |
|
|
#chat-box::-webkit-scrollbar-track { |
|
|
background: transparent; |
|
|
} |
|
|
#chat-box::-webkit-scrollbar-thumb { |
|
|
background: var(--color-border); |
|
|
border-radius: 3px; |
|
|
} |
|
|
#chat-box::-webkit-scrollbar-thumb:hover { |
|
|
background: #d0d5db; |
|
|
} |
|
|
|
|
|
.message { |
|
|
padding: 14px 20px; |
|
|
border-radius: 18px; |
|
|
max-width: 85%; |
|
|
line-height: 1.6; |
|
|
word-wrap: break-word; |
|
|
font-size: 0.95rem; |
|
|
} |
|
|
|
|
|
.message.user { |
|
|
background-color: var(--color-user-msg); |
|
|
color: var(--color-user-text); |
|
|
border-bottom-right-radius: 6px; |
|
|
align-self: flex-end; |
|
|
} |
|
|
|
|
|
.message.assistant { |
|
|
background-color: var(--color-assist-msg); |
|
|
color: var(--color-assist-text); |
|
|
border-bottom-left-radius: 6px; |
|
|
align-self: flex-start; |
|
|
} |
|
|
|
|
|
.message.error { |
|
|
background-color: var(--color-danger-bg); |
|
|
color: var(--color-danger); |
|
|
border: 1px solid var(--color-danger); |
|
|
} |
|
|
|
|
|
.message.success { |
|
|
background-color: var(--color-success-bg); |
|
|
color: var(--color-success); |
|
|
border: 1px solid var(--color-success); |
|
|
} |
|
|
|
|
|
|
|
|
.message.assistant strong { font-weight: 600; } |
|
|
.message.assistant h1, .message.assistant h2, .message.assistant h3 { |
|
|
margin-top: 1.2em; |
|
|
margin-bottom: 0.6em; |
|
|
border-bottom: 1px solid var(--color-border); |
|
|
padding-bottom: 5px; |
|
|
font-weight: 600; |
|
|
} |
|
|
.message.assistant code { |
|
|
font-family: monospace; |
|
|
background-color: #dfe7ed; |
|
|
padding: 2px 6px; |
|
|
border-radius: 4px; |
|
|
font-size: 0.9em; |
|
|
} |
|
|
.message.assistant pre { |
|
|
background-color: #2c3e50; |
|
|
color: #f4f7f6; |
|
|
padding: 16px; |
|
|
border-radius: 8px; |
|
|
overflow-x: auto; |
|
|
margin: 1em 0; |
|
|
} |
|
|
.message.assistant pre code { |
|
|
background-color: transparent; |
|
|
padding: 0; |
|
|
font-size: 0.85rem; |
|
|
} |
|
|
.message.assistant table { |
|
|
border-collapse: collapse; |
|
|
width: 100%; |
|
|
margin: 1em 0; |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
.message.assistant th, .message.assistant td { |
|
|
border: 1px solid var(--color-border); |
|
|
padding: 10px; |
|
|
text-align: left; |
|
|
} |
|
|
.message.assistant th { |
|
|
background-color: var(--color-assist-msg); |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
|
|
|
.input-area { |
|
|
border-top: 1px solid var(--color-border); |
|
|
padding: 16px 24px; |
|
|
background-color: var(--color-card); |
|
|
} |
|
|
|
|
|
#file-list { |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 8px; |
|
|
margin-bottom: 12px; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.file-pill { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
background-color: var(--color-assist-msg); |
|
|
color: var(--color-text); |
|
|
padding: 5px 12px; |
|
|
border-radius: 15px; |
|
|
font-size: 0.8rem; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.file-remove { |
|
|
background: none; |
|
|
border: none; |
|
|
color: var(--color-danger); |
|
|
cursor: pointer; |
|
|
font-size: 0.7rem; |
|
|
padding: 0; |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.file-remove:hover { |
|
|
background-color: var(--color-danger); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.prompt-input-group { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
border: 1px solid var(--color-border); |
|
|
border-radius: 25px; |
|
|
padding: 4px 4px 4px 12px; |
|
|
} |
|
|
.prompt-input-group:focus-within { |
|
|
border-color: var(--color-accent); |
|
|
box-shadow: 0 0 0 3px rgba(13,110,253,0.1); |
|
|
} |
|
|
|
|
|
#prompt-input { |
|
|
flex-grow: 1; |
|
|
border: none; |
|
|
outline: none; |
|
|
background: transparent; |
|
|
font-family: var(--font-main); |
|
|
font-size: 1rem; |
|
|
padding: 8px; |
|
|
} |
|
|
|
|
|
#file-input { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.icon-button { |
|
|
background-color: transparent; |
|
|
border: none; |
|
|
cursor: pointer; |
|
|
padding: 8px; |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
transition: background-color 0.2s; |
|
|
} |
|
|
.icon-button svg { |
|
|
width: 20px; |
|
|
height: 20px; |
|
|
fill: var(--color-text-light); |
|
|
} |
|
|
.icon-button:hover { |
|
|
background-color: var(--color-assist-msg); |
|
|
} |
|
|
|
|
|
#send-button { |
|
|
background-color: var(--color-accent); |
|
|
} |
|
|
#send-button svg { |
|
|
fill: var(--color-user-text); |
|
|
} |
|
|
#send-button:hover { |
|
|
background-color: #0b5ed7; |
|
|
} |
|
|
#send-button:disabled { |
|
|
background-color: #a0c7e4; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
|
|
|
#status-text { |
|
|
display: block; |
|
|
text-align: center; |
|
|
font-size: 0.85rem; |
|
|
color: var(--color-text-light); |
|
|
margin-bottom: 12px; |
|
|
height: 1.2em; |
|
|
} |
|
|
|
|
|
.spinner { |
|
|
display: inline-block; |
|
|
width: 1em; |
|
|
height: 1em; |
|
|
border: 2px solid currentColor; |
|
|
border-right-color: transparent; |
|
|
border-radius: 50%; |
|
|
animation: spin 0.6s linear infinite; |
|
|
margin-bottom: -3px; |
|
|
margin-right: 5px; |
|
|
} |
|
|
@keyframes spin { |
|
|
to { transform: rotate(360deg); } |
|
|
} |
|
|
|
|
|
|
|
|
.debug-container { |
|
|
padding: 12px 24px 4px; |
|
|
border-top: 1px solid var(--color-border); |
|
|
background-color: #f8f9fa; |
|
|
} |
|
|
|
|
|
.debug-container summary { |
|
|
font-size: 0.8rem; |
|
|
color: var(--color-text-light); |
|
|
cursor: pointer; |
|
|
font-weight: 500; |
|
|
padding: 8px 0; |
|
|
list-style: none; |
|
|
} |
|
|
|
|
|
.debug-container summary::-webkit-details-marker { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.debug-container summary::before { |
|
|
content: 'βΆ'; |
|
|
display: inline-block; |
|
|
margin-right: 8px; |
|
|
transition: transform 0.2s; |
|
|
} |
|
|
|
|
|
.debug-container[open] summary::before { |
|
|
transform: rotate(90deg); |
|
|
} |
|
|
|
|
|
#debug-box { |
|
|
display: none; |
|
|
font-size: 0.8rem; |
|
|
background-color: var(--color-card); |
|
|
border: 1px solid var(--color-border); |
|
|
padding: 12px; |
|
|
margin: 8px 0 0 0; |
|
|
border-radius: 8px; |
|
|
font-family: monospace; |
|
|
line-height: 1.4; |
|
|
max-height: 200px; |
|
|
overflow-y: auto; |
|
|
white-space: pre-wrap; |
|
|
word-wrap: break-word; |
|
|
} |
|
|
|
|
|
|
|
|
.debug-container[open] #debug-box, |
|
|
.debug-box-visible { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
|
|
|
.debug-container summary { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
|
|
|
.conversation-info { |
|
|
text-align: center; |
|
|
padding: 8px 16px; |
|
|
font-size: 0.85rem; |
|
|
color: var(--color-text-light); |
|
|
background-color: var(--color-success-bg); |
|
|
border-bottom: 1px solid var(--color-border); |
|
|
} |
|
|
|
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
|
|
|
<div class="container"> |
|
|
<header> |
|
|
<h1>π§ Carver Excel Analyst</h1> |
|
|
<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> |
|
|
</header> |
|
|
|
|
|
<div class="config-bar"> |
|
|
<div class="api-key-group"> |
|
|
<label for="api-key-input">API Key:</label> |
|
|
<input type="password" id="api-key-input" placeholder="Enter your Google API Key"> |
|
|
</div> |
|
|
<div class="header-actions"> |
|
|
<button class="action-button success" id="export-pdf-btn" title="Export conversation to PDF"> |
|
|
π Export PDF |
|
|
</button> |
|
|
<button class="action-button" id="toggle-details-btn" title="Show/Hide analysis details"> |
|
|
ποΈ Show Analysis Details |
|
|
</button> |
|
|
<button class="action-button" id="new-conversation-btn" title="Start a new conversation"> |
|
|
π New Conversation |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="conversation-info" id="conversation-info" style="display: none;"> |
|
|
πΎ Files remain available for this conversation. Ask follow-up questions anytime! |
|
|
</div> |
|
|
|
|
|
<div id="chat-box"> |
|
|
<div class="message assistant"> |
|
|
Welcome to Carver Excel Analyst! This is a <b>conversational chatbot</b> that remembers your files and conversation. |
|
|
<br><br> |
|
|
<b>How it works:</b> |
|
|
<br>β’ Upload your Excel, PDF, or image files once |
|
|
<br>β’ Ask questions about the data |
|
|
<br>β’ Files stay available for follow-up questions |
|
|
<br>β’ Start a new conversation anytime to clear history |
|
|
<br><br> |
|
|
<b>Please provide your API key and upload your files to begin!</b> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="input-area"> |
|
|
<div id="status-text"></div> |
|
|
|
|
|
<div id="file-list"> |
|
|
</div> |
|
|
|
|
|
<div class="prompt-input-group"> |
|
|
<input type="file" id="file-input" multiple> |
|
|
<button class="icon-button" id="attach-button" title="Upload files"> |
|
|
<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> |
|
|
</button> |
|
|
|
|
|
<input type="text" id="prompt-input" placeholder="Ask a question about your files..."> |
|
|
|
|
|
<button class="icon-button" id="send-button" title="Send message"> |
|
|
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2 .01 7z"></path></svg> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div class="debug-container" id="debug-container"> |
|
|
<div id="debug-box">No analysis details yet.</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script type="module"> |
|
|
// --- Import Generative AI SDK --- |
|
|
const { GoogleGenerativeAI } = await import("https://esm.run/@google/generative-ai"); |
|
|
|
|
|
// --- DOM Element References --- |
|
|
const apiKeyInput = document.getElementById("api-key-input"); |
|
|
const fileInput = document.getElementById("file-input"); |
|
|
const attachButton = document.getElementById("attach-button"); |
|
|
const fileListDisplay = document.getElementById("file-list"); |
|
|
const promptInput = document.getElementById("prompt-input"); |
|
|
const sendButton = document.getElementById("send-button"); |
|
|
const chatBox = document.getElementById("chat-box"); |
|
|
const statusText = document.getElementById("status-text"); |
|
|
const debugBox = document.getElementById("debug-box"); |
|
|
const newConversationBtn = document.getElementById("new-conversation-btn"); |
|
|
const conversationInfo = document.getElementById("conversation-info"); |
|
|
const exportPdfBtn = document.getElementById("export-pdf-btn"); |
|
|
const toggleDetailsBtn = document.getElementById("toggle-details-btn"); |
|
|
|
|
|
// --- Model Configuration --- |
|
|
const GENERATOR_MODEL_NAME = "gemini-2.0-flash"; |
|
|
const ANALYST_MODEL_NAME = "gemini-2.5-flash"; |
|
|
|
|
|
// --- Global State --- |
|
|
let uploadedFiles = []; // Persistent file storage |
|
|
let conversationHistory = []; // Store conversation messages |
|
|
let conversationStartTime = Date.now(); |
|
|
let analysisDetailsVisible = false; |
|
|
|
|
|
// --- The Main Carver Analyst System Prompt --- |
|
|
const CARVER_ANALYST_SYSTEM_PROMPT = ` |
|
|
You are **Carver Excel Analyst**, an internal AI specialist for Carver Procurement Consultancy. |
|
|
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. |
|
|
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. |
|
|
### π― Mission |
|
|
Understand the provided data (CSV text, PDFs, images) and provide short, factual insights β **without making any assumptions**. |
|
|
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. |
|
|
Your goal is to make Carver's data analysis simple, reliable, and audit-ready. |
|
|
### π§© Default Output (when all inputs are clear) |
|
|
1. **Executive Summary (3β6 lines)** β concise explanation answering the user's question. |
|
|
2. **Supporting Table (Markdown or CSV)** β key rows/columns only. |
|
|
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. |
|
|
Do **not** create or export a new Excel file unless the user explicitly asks for it. |
|
|
### π§± Working Logic |
|
|
**A. File Understanding** |
|
|
- 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. |
|
|
- Always confirm mappings if confidence <80%. |
|
|
**B. Required Data Rules** |
|
|
- For cost analysis β need \`Qty\` + \`UnitPrice\` or \`Total\`. |
|
|
- For "vs budget" β need a \`Baseline\`, \`Budget\`, or explicit reference vendor. |
|
|
- If missing, ask user directly. |
|
|
**C. No-Assumptions Policy** |
|
|
- If anything is missing or unclear: Ask before proceeding. |
|
|
- Example: "No 'Budget' column found. Should I use the lowest vendor total as baseline?" |
|
|
**D. Validations** |
|
|
- Detect and report missing values, currency mismatches, etc., in the Audit Block. |
|
|
**E. Computation Rules** |
|
|
- \`Saving = Baseline β Selected\` |
|
|
- \`Saving% = (Baseline β Selected) / Baseline Γ 100\` |
|
|
**F. Output Formatting** |
|
|
- Keep summary short. Use markdown tables. End every result with an **Audit Block**. |
|
|
**H. Tone** |
|
|
- Professional, precise, procurement-focused. |
|
|
### π§ Objective |
|
|
Act like Carver's in-house **Excel procurement analyst** β structured, audit-traceable, and transparent. Never make assumptions. Always verify. |
|
|
|
|
|
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. |
|
|
`; |
|
|
|
|
|
// --- Event Listeners --- |
|
|
sendButton.addEventListener("click", handleSend); |
|
|
promptInput.addEventListener("keydown", (e) => { |
|
|
if (e.key === "Enter" && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
handleSend(); |
|
|
} |
|
|
}); |
|
|
|
|
|
// Trigger hidden file input |
|
|
attachButton.addEventListener("click", () => { |
|
|
fileInput.click(); |
|
|
}); |
|
|
|
|
|
// Handle new conversation |
|
|
newConversationBtn.addEventListener("click", startNewConversation); |
|
|
|
|
|
// Handle PDF export |
|
|
exportPdfBtn.addEventListener("click", exportToPDF); |
|
|
|
|
|
// Handle analysis details toggle |
|
|
toggleDetailsBtn.addEventListener("click", toggleAnalysisDetails); |
|
|
|
|
|
// Update file pills when files are selected |
|
|
fileInput.addEventListener("change", (e) => { |
|
|
const newFiles = Array.from(e.target.files); |
|
|
uploadedFiles = [...uploadedFiles, ...newFiles]; // Append, don't replace |
|
|
updateFileDisplay(); |
|
|
fileInput.value = ""; // Reset input |
|
|
|
|
|
if (uploadedFiles.length > 0) { |
|
|
showSuccess(`π Added ${newFiles.length} file(s). Total files: ${uploadedFiles.length}`); |
|
|
conversationInfo.style.display = "block"; |
|
|
} |
|
|
}); |
|
|
|
|
|
// --- Main Function: handleSend --- |
|
|
async function handleSend() { |
|
|
const apiKey = apiKeyInput.value.trim(); |
|
|
const userPrompt = promptInput.value.trim(); |
|
|
|
|
|
// --- 1. Validations --- |
|
|
if (!apiKey) { |
|
|
displayError("Please enter your Google API Key."); |
|
|
return; |
|
|
} |
|
|
if (!userPrompt) { |
|
|
displayError("Please enter a prompt."); |
|
|
return; |
|
|
} |
|
|
if (uploadedFiles.length === 0) { |
|
|
displayError("Please upload at least one file first."); |
|
|
return; |
|
|
} |
|
|
|
|
|
// --- 2. Setup UI for Loading --- |
|
|
setLoadingState(true, "Processing your question..."); |
|
|
displayMessage(userPrompt, "user"); |
|
|
debugBox.textContent = "Generating refined prompt..."; // Clear old debug info |
|
|
|
|
|
const assistantMessageEl = displayMessage("", "assistant"); // Create empty bubble |
|
|
|
|
|
try { |
|
|
// --- 3. Process Files into Generative Parts --- |
|
|
const partsArrays = await Promise.all( |
|
|
uploadedFiles.map(fileToGenerativeParts) |
|
|
); |
|
|
const fileParts = partsArrays.flat(); |
|
|
|
|
|
// --- 4. Build conversation context --- |
|
|
let conversationContext = ""; |
|
|
if (conversationHistory.length > 0) { |
|
|
conversationContext = ` |
|
|
Previous conversation context: |
|
|
${conversationHistory.slice(-6).map(msg => |
|
|
`${msg.role.toUpperCase()}: ${msg.content.substring(0, 200)}${msg.content.length > 200 ? '...' : ''}` |
|
|
).join('\n')} |
|
|
|
|
|
`; |
|
|
} |
|
|
|
|
|
// --- 5. Initialize AI Client --- |
|
|
const genAI = new GoogleGenerativeAI(apiKey); |
|
|
|
|
|
// --- 6. STEP 1: Generate the "Better Prompt" (Normal Model) --- |
|
|
setLoadingState(true, `Step 1/2: Thinking about your question... (${GENERATOR_MODEL_NAME})`); |
|
|
|
|
|
const generatorModel = genAI.getGenerativeModel({ model: GENERATOR_MODEL_NAME }); |
|
|
|
|
|
const step1SystemPrompt = ` |
|
|
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'. |
|
|
NOTE: The attached files may include images, PDFs, or TEXT parts that are CSV conversions of Excel sheets. The analyst is aware of this. |
|
|
|
|
|
The user's question is: "${userPrompt}" |
|
|
|
|
|
${conversationContext}Based on the user's question and the file(s), generate a single, clear, and actionable instruction for the expert analyst. |
|
|
- Be specific. |
|
|
- Ask for the final output (summary, table, audit block) as defined by the analyst's role. |
|
|
- Consider previous conversation context when generating the prompt. |
|
|
- For example, if the user says "how much did I save?", you should generate a prompt like: |
|
|
"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." |
|
|
`; |
|
|
|
|
|
const step1Contents = [ |
|
|
...fileParts, |
|
|
{ text: step1SystemPrompt } |
|
|
]; |
|
|
|
|
|
const resultStep1 = await generatorModel.generateContent({ |
|
|
contents: [{ parts: step1Contents }] |
|
|
}); |
|
|
const generatedPrompt = resultStep1.response.text(); |
|
|
|
|
|
// Display the refined prompt in the debug box |
|
|
debugBox.textContent = generatedPrompt; |
|
|
analysisDetailsVisible = true; |
|
|
updateDetailsVisibility(); |
|
|
|
|
|
// --- 7. STEP 2: Get Final Answer (Reasoning Model) --- |
|
|
setLoadingState(true, `Step 2/2: The Carver Analyst is working... (${ANALYST_MODEL_NAME})`); |
|
|
|
|
|
const analystModel = genAI.getGenerativeModel({ |
|
|
model: ANALYST_MODEL_NAME, |
|
|
systemInstruction: CARVER_ANALYST_SYSTEM_PROMPT |
|
|
}); |
|
|
|
|
|
const step2Contents = [ |
|
|
...fileParts, |
|
|
{ text: generatedPrompt } |
|
|
]; |
|
|
|
|
|
const result = await analystModel.generateContentStream({ |
|
|
contents: [{ parts: step2Contents }] |
|
|
}); |
|
|
|
|
|
// --- 8. Stream Response to Chat --- |
|
|
let fullResponse = ""; |
|
|
for await (const chunk of result.stream) { |
|
|
if (typeof chunk.text === 'function') { |
|
|
const chunkText = chunk.text(); |
|
|
fullResponse += chunkText; |
|
|
assistantMessageEl.innerHTML = marked.parse(fullResponse); |
|
|
scrollToBottom(); |
|
|
} |
|
|
} |
|
|
|
|
|
// --- 9. Save to conversation history --- |
|
|
conversationHistory.push( |
|
|
{ role: "user", content: userPrompt }, |
|
|
{ role: "assistant", content: fullResponse } |
|
|
); |
|
|
|
|
|
// Keep only last 20 messages to prevent context overflow |
|
|
if (conversationHistory.length > 20) { |
|
|
conversationHistory = conversationHistory.slice(-20); |
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
console.error("Error:", error); |
|
|
displayError(`An error occurred: ${error.message}`); |
|
|
assistantMessageEl.remove(); // Remove the empty bubble |
|
|
analysisDetailsVisible = false; |
|
|
updateDetailsVisibility(); |
|
|
} finally { |
|
|
// --- 10. Cleanup UI --- |
|
|
setLoadingState(false); |
|
|
promptInput.value = ""; |
|
|
} |
|
|
} |
|
|
|
|
|
// --- Helper Functions --- |
|
|
|
|
|
/** |
|
|
* Start a new conversation - clears history and files |
|
|
*/ |
|
|
function startNewConversation() { |
|
|
if (confirm("Start a new conversation? This will clear all uploaded files and conversation history.")) { |
|
|
uploadedFiles = []; |
|
|
conversationHistory = []; |
|
|
conversationStartTime = Date.now(); |
|
|
|
|
|
// Clear UI |
|
|
chatBox.innerHTML = ` |
|
|
<div class="message assistant"> |
|
|
π <b>New conversation started!</b> Upload your files and ask questions about them. |
|
|
</div> |
|
|
`; |
|
|
|
|
|
updateFileDisplay(); |
|
|
conversationInfo.style.display = "none"; |
|
|
promptInput.value = ""; |
|
|
|
|
|
showSuccess("Started new conversation!"); |
|
|
} |
|
|
} |
|
|
|
|
|
/** |
|
|
* Update file display pills |
|
|
*/ |
|
|
function updateFileDisplay() { |
|
|
fileListDisplay.innerHTML = ""; |
|
|
|
|
|
if (uploadedFiles.length > 0) { |
|
|
uploadedFiles.forEach((file, index) => { |
|
|
const pill = document.createElement("span"); |
|
|
pill.className = "file-pill"; |
|
|
pill.innerHTML = ` |
|
|
${file.name} |
|
|
<button class="file-remove" onclick="removeFile(${index})" title="Remove file">Γ</button> |
|
|
`; |
|
|
fileListDisplay.appendChild(pill); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
/** |
|
|
* Remove a specific file |
|
|
*/ |
|
|
window.removeFile = function(index) { |
|
|
if (confirm(`Remove "${uploadedFiles[index].name}"?`)) { |
|
|
uploadedFiles.splice(index, 1); |
|
|
updateFileDisplay(); |
|
|
|
|
|
if (uploadedFiles.length === 0) { |
|
|
conversationInfo.style.display = "none"; |
|
|
showSuccess("All files removed"); |
|
|
} else { |
|
|
showSuccess(`Removed file. ${uploadedFiles.length} file(s) remaining`); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
/** |
|
|
* Converts a File object to an array of GoogleGenerativeAI.Part objects. |
|
|
*/ |
|
|
async function fileToGenerativeParts(file) { |
|
|
const excelMimeTypes = [ |
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx |
|
|
'application/vnd.ms-excel' // .xls |
|
|
]; |
|
|
|
|
|
if (excelMimeTypes.includes(file.type) || file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { |
|
|
try { |
|
|
const buffer = await file.arrayBuffer(); |
|
|
const workbook = XLSX.read(buffer, { type: 'buffer' }); |
|
|
const textParts = []; |
|
|
|
|
|
workbook.SheetNames.forEach(sheetName => { |
|
|
const worksheet = workbook.Sheets[sheetName]; |
|
|
const csvData = XLSX.utils.sheet_to_csv(worksheet); |
|
|
const partHeader = `[File: ${file.name} | Sheet: ${sheetName}]\n\n`; |
|
|
textParts.push({ text: partHeader + csvData }); |
|
|
}); |
|
|
|
|
|
return textParts; |
|
|
} catch (error) { |
|
|
console.error("Error reading Excel file:", error); |
|
|
return [{ text: `[Error processing Excel file ${file.name}: ${error.message}]` }]; |
|
|
} |
|
|
} |
|
|
else { // For all other files (PDF, images, etc.) |
|
|
return new Promise((resolve, reject) => { |
|
|
const reader = new FileReader(); |
|
|
reader.readAsDataURL(file); |
|
|
reader.onload = () => { |
|
|
const dataUrl = reader.result; |
|
|
const base64Data = dataUrl.split(',')[1]; |
|
|
const mimeType = dataUrl.split(';')[0].split(':')[1]; |
|
|
|
|
|
resolve([{ |
|
|
inlineData: { |
|
|
data: base64Data, |
|
|
mimeType: mimeType |
|
|
} |
|
|
}]); |
|
|
}; |
|
|
reader.onerror = (error) => reject(error); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
/** |
|
|
* Sets the loading state of the UI |
|
|
*/ |
|
|
function setLoadingState(isLoading, message = "") { |
|
|
if (isLoading) { |
|
|
statusText.innerHTML = `<span class="spinner"></span> ${message}`; |
|
|
sendButton.disabled = true; |
|
|
promptInput.disabled = true; |
|
|
attachButton.disabled = true; |
|
|
} else { |
|
|
statusText.innerHTML = ""; |
|
|
sendButton.disabled = false; |
|
|
promptInput.disabled = false; |
|
|
attachButton.disabled = false; |
|
|
promptInput.focus(); |
|
|
} |
|
|
} |
|
|
|
|
|
/** |
|
|
* Displays a message in the chat box |
|
|
*/ |
|
|
function displayMessage(text, role) { |
|
|
const messageEl = document.createElement("div"); |
|
|
messageEl.classList.add("message", role); |
|
|
|
|
|
if (role === 'error') { |
|
|
messageEl.textContent = text; |
|
|
} else if (role === 'assistant') { |
|
|
messageEl.innerHTML = marked.parse(text || "..."); // Show loading dots |
|
|
} else { |
|
|
messageEl.textContent = text; // Plain text for user |
|
|
} |
|
|
|
|
|
chatBox.appendChild(messageEl); |
|
|
scrollToBottom(); |
|
|
return messageEl; |
|
|
} |
|
|
|
|
|
/** |
|
|
* Displays a success message in the chat |
|
|
*/ |
|
|
function showSuccess(text) { |
|
|
const messageEl = document.createElement("div"); |
|
|
messageEl.className = "message success"; |
|
|
messageEl.textContent = text; |
|
|
|
|
|
chatBox.appendChild(messageEl); |
|
|
scrollToBottom(); |
|
|
|
|
|
// Auto-remove success message after 3 seconds |
|
|
setTimeout(() => { |
|
|
if (messageEl.parentNode) { |
|
|
messageEl.remove(); |
|
|
} |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
/** |
|
|
* Displays an error message in the chat |
|
|
*/ |
|
|
function displayError(text) { |
|
|
displayMessage(text, "error"); |
|
|
setLoadingState(false); |
|
|
} |
|
|
|
|
|
/** |
|
|
* Scrolls the chat box to the bottom |
|
|
*/ |
|
|
function scrollToBottom() { |
|
|
chatBox.scrollTop = chatBox.scrollHeight; |
|
|
} |
|
|
|
|
|
/** |
|
|
* Toggle analysis details visibility |
|
|
*/ |
|
|
function toggleAnalysisDetails() { |
|
|
analysisDetailsVisible = !analysisDetailsVisible; |
|
|
updateDetailsVisibility(); |
|
|
} |
|
|
|
|
|
/** |
|
|
* Update analysis details visibility |
|
|
*/ |
|
|
function updateDetailsVisibility() { |
|
|
if (analysisDetailsVisible) { |
|
|
debugBox.style.display = 'block'; |
|
|
toggleDetailsBtn.innerHTML = 'ποΈ Hide Analysis Details'; |
|
|
} else { |
|
|
debugBox.style.display = 'none'; |
|
|
toggleDetailsBtn.innerHTML = 'ποΈ Show Analysis Details'; |
|
|
} |
|
|
} |
|
|
|
|
|
/** |
|
|
* Export conversation to PDF |
|
|
*/ |
|
|
function exportToPDF() { |
|
|
if (conversationHistory.length === 0) { |
|
|
displayError("No conversation to export."); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
// Create HTML content for PDF |
|
|
const currentDate = new Date().toLocaleDateString(); |
|
|
const timestamp = new Date().toLocaleTimeString(); |
|
|
let htmlContent = ` |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<title>Carver Excel Analyst - Export</title> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
|
|
|
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; |
|
|
line-height: 1.7; |
|
|
color: #1a202c; |
|
|
background-color: #f7fafc; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.page { |
|
|
background: white; |
|
|
max-width: 900px; |
|
|
margin: 20px auto; |
|
|
padding: 60px; |
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); |
|
|
border-radius: 8px; |
|
|
min-height: 800px; |
|
|
} |
|
|
|
|
|
|
|
|
.header { |
|
|
text-align: center; |
|
|
margin-bottom: 50px; |
|
|
padding-bottom: 30px; |
|
|
border-bottom: 3px solid #e2e8f0; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.header::after { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
bottom: -3px; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
width: 80px; |
|
|
height: 3px; |
|
|
background: linear-gradient(135deg, #3182ce, #63b3ed); |
|
|
} |
|
|
|
|
|
.header h1 { |
|
|
color: #2d3748; |
|
|
font-size: 2.5rem; |
|
|
font-weight: 700; |
|
|
margin-bottom: 10px; |
|
|
letter-spacing: -0.025em; |
|
|
} |
|
|
|
|
|
.header .subtitle { |
|
|
color: #718096; |
|
|
font-size: 1.1rem; |
|
|
font-weight: 400; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
|
|
|
.header .metadata { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
gap: 30px; |
|
|
margin-top: 20px; |
|
|
font-size: 0.9rem; |
|
|
color: #a0aec0; |
|
|
} |
|
|
|
|
|
.metadata-item { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 5px; |
|
|
} |
|
|
|
|
|
|
|
|
.files-section { |
|
|
background: linear-gradient(135deg, #f7fafc, #edf2f7); |
|
|
border: 2px solid #e2e8f0; |
|
|
border-radius: 12px; |
|
|
padding: 25px; |
|
|
margin-bottom: 40px; |
|
|
} |
|
|
|
|
|
.files-section h2 { |
|
|
color: #2d3748; |
|
|
font-size: 1.4rem; |
|
|
font-weight: 600; |
|
|
margin-bottom: 20px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.files-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
|
|
gap: 15px; |
|
|
margin-top: 15px; |
|
|
} |
|
|
|
|
|
.file-card { |
|
|
background: white; |
|
|
padding: 15px 20px; |
|
|
border-radius: 8px; |
|
|
border: 1px solid #e2e8f0; |
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.file-card:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
.file-name { |
|
|
font-weight: 600; |
|
|
color: #2d3748; |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
|
|
|
.file-size { |
|
|
color: #718096; |
|
|
font-size: 0.85rem; |
|
|
} |
|
|
|
|
|
|
|
|
.conversation-section { |
|
|
margin-bottom: 40px; |
|
|
} |
|
|
|
|
|
.conversation-entry { |
|
|
background: white; |
|
|
border-radius: 12px; |
|
|
margin-bottom: 30px; |
|
|
overflow: hidden; |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); |
|
|
border: 1px solid #e2e8f0; |
|
|
} |
|
|
|
|
|
.user-entry { |
|
|
border-left: 4px solid #3182ce; |
|
|
} |
|
|
|
|
|
.assistant-entry { |
|
|
border-left: 4px solid #38a169; |
|
|
} |
|
|
|
|
|
.entry-header { |
|
|
padding: 20px 25px 15px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 12px; |
|
|
background: linear-gradient(135deg, #f7fafc, #edf2f7); |
|
|
} |
|
|
|
|
|
.user-entry .entry-header { |
|
|
background: linear-gradient(135deg, #ebf8ff, #bee3f8); |
|
|
} |
|
|
|
|
|
.assistant-entry .entry-header { |
|
|
background: linear-gradient(135deg, #f0fff4, #c6f6d5); |
|
|
} |
|
|
|
|
|
.role-icon { |
|
|
width: 32px; |
|
|
height: 32px; |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-weight: 600; |
|
|
font-size: 0.8rem; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.user-entry .role-icon { |
|
|
background: #3182ce; |
|
|
} |
|
|
|
|
|
.assistant-entry .role-icon { |
|
|
background: #38a169; |
|
|
} |
|
|
|
|
|
.role-text { |
|
|
font-weight: 600; |
|
|
font-size: 1.1rem; |
|
|
color: #2d3748; |
|
|
} |
|
|
|
|
|
.timestamp { |
|
|
margin-left: auto; |
|
|
color: #718096; |
|
|
font-size: 0.85rem; |
|
|
} |
|
|
|
|
|
.entry-content { |
|
|
padding: 25px; |
|
|
background: white; |
|
|
} |
|
|
|
|
|
|
|
|
.message-content { |
|
|
font-size: 0.95rem; |
|
|
line-height: 1.8; |
|
|
} |
|
|
|
|
|
.user-entry .message-content { |
|
|
color: #2d3748; |
|
|
} |
|
|
|
|
|
.assistant-entry .message-content { |
|
|
color: #2d3748; |
|
|
} |
|
|
|
|
|
|
|
|
.message-content h1, .message-content h2, .message-content h3 { |
|
|
color: #2d3748; |
|
|
margin-top: 1.5em; |
|
|
margin-bottom: 0.8em; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.message-content h1 { |
|
|
font-size: 1.4rem; |
|
|
border-bottom: 2px solid #e2e8f0; |
|
|
padding-bottom: 8px; |
|
|
} |
|
|
|
|
|
.message-content h2 { |
|
|
font-size: 1.25rem; |
|
|
color: #3182ce; |
|
|
} |
|
|
|
|
|
.message-content h3 { |
|
|
font-size: 1.1rem; |
|
|
color: #38a169; |
|
|
} |
|
|
|
|
|
.message-content strong { |
|
|
font-weight: 600; |
|
|
color: #2d3748; |
|
|
} |
|
|
|
|
|
.message-content table { |
|
|
width: 100%; |
|
|
border-collapse: collapse; |
|
|
margin: 20px 0; |
|
|
font-size: 0.9rem; |
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|
|
border-radius: 8px; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.message-content th { |
|
|
background: linear-gradient(135deg, #3182ce, #63b3ed); |
|
|
color: white; |
|
|
padding: 15px 12px; |
|
|
text-align: left; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.message-content td { |
|
|
padding: 12px; |
|
|
border-bottom: 1px solid #e2e8f0; |
|
|
background: white; |
|
|
} |
|
|
|
|
|
.message-content tr:nth-child(even) td { |
|
|
background: #f7fafc; |
|
|
} |
|
|
|
|
|
.message-content tr:hover td { |
|
|
background: #edf2f7; |
|
|
} |
|
|
|
|
|
.message-content code { |
|
|
background: #f7fafc; |
|
|
padding: 3px 8px; |
|
|
border-radius: 4px; |
|
|
font-family: 'Monaco', 'Consolas', 'Courier New', monospace; |
|
|
font-size: 0.85rem; |
|
|
color: #d53f8c; |
|
|
border: 1px solid #e2e8f0; |
|
|
} |
|
|
|
|
|
.message-content pre { |
|
|
background: #1a202c; |
|
|
color: #f7fafc; |
|
|
padding: 20px; |
|
|
border-radius: 8px; |
|
|
overflow-x: auto; |
|
|
margin: 20px 0; |
|
|
border: 1px solid #2d3748; |
|
|
} |
|
|
|
|
|
.message-content pre code { |
|
|
background: transparent; |
|
|
padding: 0; |
|
|
color: #f7fafc; |
|
|
border: none; |
|
|
font-size: 0.85rem; |
|
|
} |
|
|
|
|
|
.message-content blockquote { |
|
|
border-left: 4px solid #3182ce; |
|
|
padding-left: 20px; |
|
|
margin: 20px 0; |
|
|
background: #f7fafc; |
|
|
padding: 15px 20px; |
|
|
border-radius: 0 8px 8px 0; |
|
|
color: #4a5568; |
|
|
} |
|
|
|
|
|
|
|
|
.audit-block { |
|
|
background: linear-gradient(135deg, #fed7d7, #feb2b2); |
|
|
border: 2px solid #fc8181; |
|
|
border-radius: 8px; |
|
|
padding: 20px; |
|
|
margin-top: 25px; |
|
|
} |
|
|
|
|
|
.audit-block h3 { |
|
|
color: #c53030; |
|
|
margin-top: 0; |
|
|
margin-bottom: 15px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.audit-block ul { |
|
|
margin-left: 20px; |
|
|
} |
|
|
|
|
|
.audit-block li { |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
|
|
|
|
|
|
@media print { |
|
|
body { |
|
|
background: white; |
|
|
} |
|
|
|
|
|
.page { |
|
|
margin: 0; |
|
|
padding: 40px; |
|
|
box-shadow: none; |
|
|
border-radius: 0; |
|
|
} |
|
|
|
|
|
.conversation-entry { |
|
|
break-inside: avoid; |
|
|
page-break-inside: avoid; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.page-break { |
|
|
page-break-before: always; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="page"> |
|
|
<div class="header"> |
|
|
<h1>π§ Carver Excel Analyst</h1> |
|
|
<div class="subtitle">Professional Data Analysis Report</div> |
|
|
<div class="metadata"> |
|
|
<div class="metadata-item"> |
|
|
<span>π
</span> |
|
|
<span>${currentDate}</span> |
|
|
</div> |
|
|
<div class="metadata-item"> |
|
|
<span>π</span> |
|
|
<span>${timestamp}</span> |
|
|
</div> |
|
|
<div class="metadata-item"> |
|
|
<span>π¬</span> |
|
|
<span>${conversationHistory.length} messages</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="files-section"> |
|
|
<h2>π Analyzed Files</h2> |
|
|
<div class="files-grid"> |
|
|
`; |
|
|
|
|
|
uploadedFiles.forEach(file => { |
|
|
htmlContent += ` |
|
|
<div class="file-card"> |
|
|
<div class="file-name">${file.name}</div> |
|
|
<div class="file-size">${(file.size / 1024).toFixed(1)} KB</div> |
|
|
</div> |
|
|
`; |
|
|
}); |
|
|
|
|
|
htmlContent += ` |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="conversation-section"> |
|
|
<h2>π¬ Conversation Analysis</h2> |
|
|
`; |
|
|
|
|
|
conversationHistory.forEach((message, index) => { |
|
|
const roleClass = message.role === 'user' ? 'user-entry' : 'assistant-entry'; |
|
|
const roleIcon = message.role === 'user' ? 'π€' : 'π€'; |
|
|
const roleText = message.role === 'user' ? 'User' : 'Carver Analyst'; |
|
|
const messageTime = new Date(Date.now() - (conversationHistory.length - index) * 1000).toLocaleTimeString(); |
|
|
|
|
|
htmlContent += ` |
|
|
<div class="conversation-entry ${roleClass}"> |
|
|
<div class="entry-header"> |
|
|
<div class="role-icon">${roleIcon}</div> |
|
|
<div class="role-text">${roleText}</div> |
|
|
<div class="timestamp">${messageTime}</div> |
|
|
</div> |
|
|
<div class="entry-content"> |
|
|
<div class="message-content"> |
|
|
${marked.parse(message.content)} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
}); |
|
|
|
|
|
htmlContent += ` |
|
|
</div> |
|
|
</div> |
|
|
</body> |
|
|
</html> |
|
|
`; |
|
|
|
|
|
// Create and trigger download |
|
|
const blob = new Blob([htmlContent], { type: 'text/html' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = `carver-analyst-report-${new Date().toISOString().split('T')[0]}.html`; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
URL.revokeObjectURL(url); |
|
|
|
|
|
showSuccess("β
Professional report exported! Open the HTML file and print as PDF."); |
|
|
|
|
|
// Show instructions for PDF conversion |
|
|
setTimeout(() => { |
|
|
showSuccess("π‘ To create PDF: Open the file β Print (Ctrl/Cmd+P) β Save as PDF"); |
|
|
}, 2000); |
|
|
|
|
|
} catch (error) { |
|
|
console.error("Export error:", error); |
|
|
displayError(`Export failed: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
</script> |
|
|
</body> |
|
|
</html> |