1072 lines
57 KiB
HTML
1072 lines
57 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Document Intelligence — Nexus One AI</title>
|
||
<link rel="stylesheet" href="style.css?v=4">
|
||
<style>
|
||
/* ── Upload zone ── */
|
||
.di-upload-zone {
|
||
border: 2px dashed var(--bdr);
|
||
border-radius: 16px;
|
||
background:var(--navy2);
|
||
padding: 40px 32px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: .2s;
|
||
position: relative;
|
||
}
|
||
.di-upload-zone:hover, .di-upload-zone.drag-over {
|
||
border-color: var(--purple);
|
||
background:rgba(124,58,237,.04);
|
||
}
|
||
.di-upload-zone input[type="file"] {
|
||
position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%;
|
||
}
|
||
.di-upload-icon { font-size: 36px; margin-bottom: 10px; }
|
||
.di-upload-title { font-size: 16px; font-weight: 700; color: var(--ink); margin-bottom: 4px; }
|
||
.di-upload-sub { font-size: 12px; color: var(--lt); }
|
||
.di-file-types { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 12px; }
|
||
.di-type-chip { background: var(--bg); border: 1px solid var(--bdr); border-radius: 20px; padding: 3px 10px; font-size: 11px; color: var(--med); font-weight: 600; }
|
||
|
||
/* ── Comparison zone ── */
|
||
.di-compare-zone { display: none; margin-top: 14px; }
|
||
.di-compare-zone.show { display: block; }
|
||
.di-compare-label { font-size: 12px; font-weight: 700; color: var(--lt); text-transform: uppercase; letter-spacing: .4px; margin-bottom: 8px; }
|
||
|
||
/* ── Selected file bar ── */
|
||
.di-file-bar {
|
||
display: none;
|
||
background:var(--navy2);
|
||
border: 1.5px solid var(--purple);
|
||
border-radius: 12px;
|
||
padding: 12px 16px;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-top: 10px;
|
||
}
|
||
.di-file-bar.show { display: flex; }
|
||
.di-file-icon { font-size: 22px; }
|
||
.di-file-name { font-weight: 600; font-size: 13px; color: var(--ink); flex: 1; }
|
||
.di-file-size { font-size: 12px; color: var(--lt); }
|
||
.di-file-clear { background: none; border: none; cursor: pointer; font-size: 16px; color: var(--lt); padding: 0; }
|
||
.di-file-clear:hover { color: var(--ink); }
|
||
|
||
/* ── Controls ── */
|
||
.di-controls {
|
||
background:var(--navy2);
|
||
border: 1px solid var(--bdr);
|
||
border-radius: 14px;
|
||
padding: 22px 24px;
|
||
margin-top: 18px;
|
||
}
|
||
.di-controls h3 { font-size: 13px; font-weight: 700; color: var(--lt); text-transform: uppercase; letter-spacing: .5px; margin: 0 0 14px; }
|
||
.di-mode-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; margin-bottom: 18px; }
|
||
.di-mode-btn {
|
||
padding: 12px 10px; border-radius: 10px; border: 1.5px solid var(--bdr);
|
||
background:var(--navy2); cursor: pointer; font-family: inherit; font-size: 12px;
|
||
font-weight: 600; color: var(--med); text-align: center; transition: .15s;
|
||
line-height: 1.3;
|
||
}
|
||
.di-mode-btn .di-mode-icon { font-size: 20px; display: block; margin-bottom: 6px; }
|
||
.di-mode-btn.active { border-color: var(--purple); background:rgba(124,58,237,.06); color: var(--purple); }
|
||
.di-mode-btn:hover:not(.active) { border-color: var(--med); }
|
||
|
||
.di-mode-section { font-size: 10px; font-weight: 700; color: var(--xlt); text-transform: uppercase; letter-spacing: .6px; margin: 14px 0 8px; grid-column: 1 / -1; }
|
||
|
||
.di-custom-prompt { display: none; margin-bottom: 18px; }
|
||
.di-custom-prompt textarea {
|
||
width: 100%; box-sizing: border-box; border: 1.5px solid var(--bdr); border-radius: 10px;
|
||
padding: 12px 14px; font-family: inherit; font-size: 13px; resize: vertical;
|
||
min-height: 80px; color: var(--ink); background: var(--navy2);
|
||
}
|
||
.di-custom-prompt textarea:focus { outline: none; border-color: var(--purple); }
|
||
|
||
.di-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
|
||
.di-row .form-group { flex: 1; min-width: 160px; margin: 0; }
|
||
.di-row label { display: block; font-size: 11px; font-weight: 700; color: var(--lt); margin-bottom: 6px; text-transform: uppercase; letter-spacing: .4px; }
|
||
.di-row select {
|
||
width: 100%; padding: 9px 12px; border: 1.5px solid var(--bdr); border-radius: 8px;
|
||
font-family: inherit; font-size: 13px; color: var(--ink); background:var(--navy2);
|
||
}
|
||
.di-row select:focus { outline: none; border-color: var(--purple); }
|
||
|
||
/* ── Process button ── */
|
||
#process-btn {
|
||
margin-top: 18px; width: 100%; padding: 13px;
|
||
font-size: 14px; font-weight: 700; border-radius: 10px;
|
||
background: linear-gradient(135deg,var(--purple),var(--pink));
|
||
color: white; border: none; cursor: pointer;
|
||
font-family: inherit; transition: .15s;
|
||
}
|
||
#process-btn:hover { filter: brightness(1.08); }
|
||
#process-btn:disabled { opacity: .5; cursor: not-allowed; }
|
||
|
||
/* ── Progress ── */
|
||
.di-progress {
|
||
display: none;
|
||
background:var(--navy2);
|
||
border: 1px solid var(--bdr);
|
||
border-radius: 14px;
|
||
padding: 28px 24px;
|
||
margin-top: 18px;
|
||
text-align: center;
|
||
}
|
||
.di-progress.show { display: block; }
|
||
.di-spinner { font-size: 30px; animation: spin 1.2s linear infinite; display: inline-block; margin-bottom: 10px; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.di-progress-label { font-size: 14px; color: var(--med); font-weight: 600; }
|
||
.di-progress-sub { font-size: 12px; color: var(--lt); margin-top: 4px; }
|
||
|
||
/* ── Result card ── */
|
||
.di-result {
|
||
display: none;
|
||
background:var(--navy2);
|
||
border: 1px solid var(--bdr);
|
||
border-radius: 14px;
|
||
margin-top: 18px;
|
||
}
|
||
.di-result.show { display: block; }
|
||
.di-result-header {
|
||
display: flex; align-items: center; gap: 12px; padding: 14px 20px;
|
||
border-bottom: 1px solid var(--bdr);
|
||
}
|
||
.di-result-title { font-size: 14px; font-weight: 700; color: var(--ink); flex: 1; }
|
||
.di-result-meta { font-size: 12px; color: var(--lt); }
|
||
.di-result-body {
|
||
padding: 20px 24px;
|
||
font-size: 14px; color: var(--ink); line-height: 1.7;
|
||
white-space: pre-wrap; word-break: break-word;
|
||
max-height: 700px; overflow-y: auto;
|
||
}
|
||
.di-result-body.structured { white-space: normal; }
|
||
.di-result-actions { padding: 12px 20px; border-top: 1px solid var(--bdr); display: flex; gap: 10px; flex-wrap: wrap; }
|
||
|
||
/* ── Clause cards ── */
|
||
.di-clauses { display: flex; flex-direction: column; gap: 12px; }
|
||
.di-clause {
|
||
border: 1.5px solid var(--bdr); border-radius: 12px; overflow: hidden;
|
||
}
|
||
.di-clause.risk-high { border-color: rgba(220,38,38,.3); }
|
||
.di-clause.risk-medium { border-color: rgba(217,119,6,.3); }
|
||
.di-clause.risk-low { border-color: rgba(22,163,74,.3); }
|
||
.di-clause-header {
|
||
display: flex; align-items: center; gap: 10px; padding: 12px 16px;
|
||
background: var(--bg); cursor: pointer;
|
||
}
|
||
.di-clause.risk-high .di-clause-header { background: rgba(220,38,38,.04); }
|
||
.di-clause.risk-medium .di-clause-header { background: rgba(217,119,6,.04); }
|
||
.di-clause.risk-low .di-clause-header { background: rgba(22,163,74,.04); }
|
||
.di-clause-type { font-size: 13px; font-weight: 700; color: var(--ink); flex: 1; }
|
||
.di-clause-risk {
|
||
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px;
|
||
padding: 2px 9px; border-radius: 20px;
|
||
}
|
||
.risk-high .di-clause-risk { background: rgba(220,38,38,.1); color: #DC2626; }
|
||
.risk-medium .di-clause-risk { background: rgba(217,119,6,.1); color: #D97706; }
|
||
.risk-low .di-clause-risk { background: rgba(22,163,74,.1); color: #16A34A; }
|
||
.di-clause-pg { font-size: 11px; color: var(--lt); margin-left: 8px; }
|
||
.di-clause-body { padding: 14px 16px; font-size: 13px; color: var(--med); line-height: 1.6; display: none; }
|
||
.di-clause.open .di-clause-body { display: block; }
|
||
.di-clause-excerpt { font-style: italic; color: var(--lt); font-size: 12px; margin-top: 8px; border-left: 3px solid var(--bdr); padding-left: 10px; }
|
||
.di-clause-chevron { color: var(--lt); font-size: 11px; transition: .2s; }
|
||
.di-clause.open .di-clause-chevron { transform: rotate(90deg); }
|
||
|
||
/* ── Clause summary bar ── */
|
||
.di-clause-summary { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
|
||
.di-clause-stat { display: flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 20px; font-size: 12px; font-weight: 700; }
|
||
.di-clause-stat.red { background: rgba(220,38,38,.08); color: #DC2626; border: 1px solid rgba(220,38,38,.2); }
|
||
.di-clause-stat.amber { background: rgba(217,119,6,.08); color: #D97706; border: 1px solid rgba(217,119,6,.2); }
|
||
.di-clause-stat.green { background: rgba(22,163,74,.08); color: #16A34A; border: 1px solid rgba(22,163,74,.2); }
|
||
|
||
/* ── Tender checklist ── */
|
||
.di-checklist { display: flex; flex-direction: column; gap: 8px; }
|
||
.di-check-item {
|
||
display: flex; align-items: flex-start; gap: 12px; padding: 13px 16px;
|
||
border-radius: 10px; border: 1px solid var(--bdr); background: var(--bg);
|
||
}
|
||
.di-check-item.pass { border-color: rgba(22,163,74,.25); background: rgba(22,163,74,.04); }
|
||
.di-check-item.fail { border-color: rgba(220,38,38,.25); background: rgba(220,38,38,.04); }
|
||
.di-check-item.missing { border-color: rgba(217,119,6,.25); background: rgba(217,119,6,.04); }
|
||
.di-check-icon { font-size: 18px; flex-shrink: 0; margin-top: 1px; }
|
||
.di-check-body { flex: 1; }
|
||
.di-check-title { font-size: 13px; font-weight: 600; color: var(--ink); margin-bottom: 2px; }
|
||
.di-check-note { font-size: 12px; color: var(--lt); }
|
||
.di-check-status { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; padding: 2px 9px; border-radius: 20px; flex-shrink: 0; margin-top: 2px; }
|
||
.pass .di-check-status { background: rgba(22,163,74,.1); color: #16A34A; }
|
||
.fail .di-check-status { background: rgba(220,38,38,.1); color: #DC2626; }
|
||
.missing .di-check-status { background: rgba(217,119,6,.1); color: #D97706; }
|
||
|
||
.di-checklist-summary { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
|
||
.di-check-prog { height: 6px; border-radius: 20px; background: rgba(124,58,237,.1); margin-bottom: 16px; overflow: hidden; }
|
||
.di-check-prog-bar { height: 100%; border-radius: 20px; background: linear-gradient(90deg,#16A34A,#22C55E); transition: width .8s ease; }
|
||
|
||
/* ── Executive brief ── */
|
||
.di-brief { font-size: 14px; color: var(--ink); line-height: 1.8; }
|
||
.di-brief-meta { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--bdr); }
|
||
.di-brief-meta-item { display: flex; flex-direction: column; gap: 2px; }
|
||
.di-brief-meta-label { font-size: 10px; font-weight: 700; color: var(--lt); text-transform: uppercase; letter-spacing: .5px; }
|
||
.di-brief-meta-val { font-size: 13px; font-weight: 600; color: var(--ink); }
|
||
.di-brief-section { margin-bottom: 18px; }
|
||
.di-brief-section-title { font-size: 12px; font-weight: 800; color: var(--purple); text-transform: uppercase; letter-spacing: .6px; margin-bottom: 8px; }
|
||
.di-brief-kv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||
@media(max-width:600px){ .di-brief-kv-grid { grid-template-columns: 1fr; } }
|
||
.di-brief-kv { background: var(--bg); border: 1px solid var(--bdr); border-radius: 8px; padding: 10px 14px; }
|
||
.di-brief-kv-label { font-size: 10px; font-weight: 700; color: var(--lt); text-transform: uppercase; }
|
||
.di-brief-kv-val { font-size: 13px; color: var(--ink); font-weight: 600; margin-top: 2px; }
|
||
.di-brief-risk-item { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 8px; }
|
||
.di-brief-risk-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; margin-top: 5px; }
|
||
.di-brief-risk-dot.high { background: #DC2626; }
|
||
.di-brief-risk-dot.medium { background: #D97706; }
|
||
.di-brief-risk-dot.low { background: #16A34A; }
|
||
|
||
/* ── Compare view ── */
|
||
.di-compare-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||
@media(max-width:700px){ .di-compare-grid { grid-template-columns: 1fr; } }
|
||
.di-compare-col { border: 1px solid var(--bdr); border-radius: 10px; overflow: hidden; }
|
||
.di-compare-col-header { padding: 10px 14px; background: var(--bg); border-bottom: 1px solid var(--bdr); font-size: 12px; font-weight: 700; color: var(--ink); }
|
||
.di-compare-col-body { padding: 14px; font-size: 13px; color: var(--med); line-height: 1.7; max-height: 400px; overflow-y: auto; }
|
||
.di-compare-diff-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.di-compare-diff-table th { text-align: left; font-size: 11px; font-weight: 700; color: var(--lt); text-transform: uppercase; padding: 0 0 10px; border-bottom: 1px solid var(--bdr); }
|
||
.di-compare-diff-table td { padding: 12px 8px; border-bottom: 1px solid var(--bdr); vertical-align: top; }
|
||
.di-compare-diff-table tr:last-child td { border-bottom: none; }
|
||
.diff-added { background: rgba(22,163,74,.06); }
|
||
.diff-removed { background: rgba(220,38,38,.06); }
|
||
.diff-changed { background: rgba(217,119,6,.06); }
|
||
.diff-badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 20px; }
|
||
.diff-added .diff-badge { background: rgba(22,163,74,.1); color: #16A34A; }
|
||
.diff-removed .diff-badge { background: rgba(220,38,38,.1); color: #DC2626; }
|
||
.diff-changed .diff-badge { background: rgba(217,119,6,.1); color: #D97706; }
|
||
|
||
/* ── History ── */
|
||
.di-history { margin-top: 40px; }
|
||
.di-history h2 { font-size: 15px; font-weight: 700; color: var(--ink); margin-bottom: 14px; }
|
||
.di-hist-table-wrap { border: 1px solid var(--bdr); border-radius: 10px; overflow: hidden; }
|
||
table.di-hist-table { width: 100%; border-collapse: collapse; }
|
||
.di-hist-table th { background: var(--bg); padding: 9px 14px; text-align: left; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px; color: var(--lt); border-bottom: 1px solid var(--bdr); }
|
||
.di-hist-table td { padding: 10px 14px; border-bottom: 1px solid var(--bdr); font-size: 13px; color: var(--ink); }
|
||
.di-hist-table tr:last-child td { border-bottom: none; }
|
||
.di-hist-table tr:hover td { background: rgba(124,58,237,.03); cursor: pointer; }
|
||
.di-status-badge { display: inline-block; padding: 2px 9px; border-radius: 20px; font-size: 11px; font-weight: 700; }
|
||
.di-status-badge.done { background: rgba(22,163,74,.1); color: #16A34A; }
|
||
.di-status-badge.pending { background: rgba(217,119,6,.1); color: #D97706; }
|
||
.di-status-badge.processing { background: rgba(37,99,235,.1); color: #2563EB; }
|
||
.di-status-badge.error { background: rgba(220,38,38,.1); color: #DC2626; }
|
||
.di-empty { text-align: center; padding: 32px 0; color: var(--lt); font-size: 13px; }
|
||
|
||
/* ── Export toolbar ── */
|
||
.di-export-btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: 8px; border: 1px solid var(--bdr); background: var(--navy2); font-size: 12px; font-weight: 600; color: var(--med); cursor: pointer; font-family: inherit; transition: .15s; }
|
||
.di-export-btn:hover { border-color: var(--purple); color: var(--purple); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="topnav">
|
||
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
|
||
<nav>
|
||
<a href="index.html">Home</a>
|
||
<a href="quickstart.html">Quick Start</a>
|
||
<a href="prompts.html">Prompt Library</a>
|
||
<a href="usecases.html">Use Cases</a>
|
||
<span class="nav-sep"></span>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn">Help ▾</button>
|
||
<div class="nav-drop-menu">
|
||
<span class="nav-drop-cat">LEARN /</span>
|
||
<a href="quickstart.html">Quick Start</a>
|
||
<a href="models.html">Models</a>
|
||
<span class="nav-drop-cat">SUPPORT /</span>
|
||
<a href="troubleshooting.html">Troubleshoot</a>
|
||
<a href="faq.html">FAQ</a>
|
||
<span class="nav-drop-cat">MORE /</span>
|
||
<a href="glossary.html">Glossary</a>
|
||
<a href="whats-new.html">What's New</a>
|
||
</div>
|
||
</div>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn">Admin ▾</button>
|
||
<div class="nav-drop-menu nav-drop-menu-wide">
|
||
<span class="nav-drop-cat">DOCS /</span>
|
||
<a href="security.html">Security & Privacy</a>
|
||
<a href="admin.html">Admin Guide</a>
|
||
<span class="nav-drop-cat">MONITOR /</span>
|
||
<a href="dashboard.html">Dashboard</a>
|
||
<a href="analytics.html">Usage Analytics</a>
|
||
<a href="audit.html">Audit Log</a>
|
||
<a href="feedback.html">Feedback & Ratings</a>
|
||
<span class="nav-drop-cat">MANAGE /</span>
|
||
<a href="users.html">Users</a>
|
||
<a href="teams.html">Teams</a>
|
||
<a href="models-admin.html">Model Manager</a>
|
||
<a href="training.html">Training</a>
|
||
<a href="knowledge.html">Knowledge Base</a>
|
||
<span class="nav-drop-cat">TOOLS /</span>
|
||
<a href="apikeys.html">API Keys</a>
|
||
<a href="benchmark.html">Benchmarking</a>
|
||
<a href="model-compare.html">Model Compare</a>
|
||
<a href="api-playground.html">API Playground</a>
|
||
<a href="guardrails.html">Guardrails</a>
|
||
<a href="rag-quality.html">RAG Quality</a>
|
||
<a href="router.html">Model Router</a>
|
||
<a href="connectors.html">Connectors</a>
|
||
<span class="nav-drop-cat">SYSTEM /</span>
|
||
<a href="console.html">Console</a>
|
||
<a href="settings.html">Settings</a>
|
||
</div>
|
||
</div>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn active">AI Tools ▾</button>
|
||
<div class="nav-drop-menu">
|
||
<span class="nav-drop-cat">INTELLIGENCE /</span>
|
||
<a href="documents.html">Document Intelligence</a>
|
||
<a href="chat-multi.html">Multimodal Chat</a>
|
||
<a href="prompt-studio.html">Prompt Studio</a>
|
||
<a href="meeting.html">Meeting Assistant</a>
|
||
<span class="nav-drop-cat">AUTOMATION /</span>
|
||
<a href="agents.html">Agent Builder</a>
|
||
<a href="schedules.html">Scheduled Jobs</a>
|
||
<a href="workflows.html">Workflow Automation</a>
|
||
<span class="nav-drop-cat">QUALITY /</span>
|
||
<a href="evals.html">AI Eval Suite</a>
|
||
<a href="chatrooms.html">Chat Rooms</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
<a href="notifications.html" style="position:relative">🔔</a>
|
||
<span class="badge" data-brand="tier">Basic Tier</span>
|
||
<div id="nav-org-logo" class="nav-org-logo"></div>
|
||
</header>
|
||
|
||
<div class="page-hero">
|
||
<div class="label">AI Tools</div>
|
||
<h1>Document Intelligence Workbench</h1>
|
||
<p>Contract analysis, tender checklists, executive briefs, document comparison — your entire document workflow, on-premises.</p>
|
||
</div>
|
||
|
||
<div class="content">
|
||
|
||
<!-- Upload zone -->
|
||
<div class="di-upload-zone" id="upload-zone">
|
||
<input type="file" id="file-input"
|
||
accept=".pdf,.docx,.doc,.txt,.md,.png,.jpg,.jpeg,.webp,.bmp">
|
||
<div class="di-upload-icon">📄</div>
|
||
<div class="di-upload-title">Drop your file here or click to browse</div>
|
||
<div class="di-upload-sub">Supports PDF, Word, images, and plain text — up to 20 MB</div>
|
||
<div class="di-file-types">
|
||
<span class="di-type-chip">PDF</span>
|
||
<span class="di-type-chip">DOCX</span>
|
||
<span class="di-type-chip">TXT / MD</span>
|
||
<span class="di-type-chip">PNG / JPG</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Selected file bar -->
|
||
<div class="di-file-bar" id="file-bar">
|
||
<span class="di-file-icon" id="file-icon">📄</span>
|
||
<span class="di-file-name" id="file-name">—</span>
|
||
<span class="di-file-size" id="file-size"></span>
|
||
<button class="di-file-clear" onclick="clearFile()" title="Remove file">✕</button>
|
||
</div>
|
||
|
||
<!-- Second file (comparison mode) -->
|
||
<div class="di-compare-zone" id="compare-zone">
|
||
<div class="di-compare-label">📎 Second Document (to compare against)</div>
|
||
<div class="di-upload-zone" id="upload-zone-b" style="padding:24px 20px">
|
||
<input type="file" id="file-input-b"
|
||
accept=".pdf,.docx,.doc,.txt,.md,.png,.jpg,.jpeg,.webp,.bmp">
|
||
<div class="di-upload-icon" style="font-size:28px">📄</div>
|
||
<div class="di-upload-title" style="font-size:14px">Drop second document here</div>
|
||
</div>
|
||
<div class="di-file-bar" id="file-bar-b">
|
||
<span class="di-file-icon" id="file-icon-b">📄</span>
|
||
<span class="di-file-name" id="file-name-b">—</span>
|
||
<span class="di-file-size" id="file-size-b"></span>
|
||
<button class="di-file-clear" onclick="clearFileB()" title="Remove file">✕</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Controls -->
|
||
<div class="di-controls">
|
||
<h3>What should the AI do?</h3>
|
||
<div class="di-mode-grid" id="mode-grid">
|
||
<div class="di-mode-section">GENERAL</div>
|
||
<button class="di-mode-btn active" data-mode="summarise" onclick="setMode('summarise',this)">
|
||
<span class="di-mode-icon">📝</span>Summarise
|
||
</button>
|
||
<button class="di-mode-btn" data-mode="keypoints" onclick="setMode('keypoints',this)">
|
||
<span class="di-mode-icon">🔑</span>Key Points
|
||
</button>
|
||
<button class="di-mode-btn" data-mode="tables" onclick="setMode('tables',this)">
|
||
<span class="di-mode-icon">📊</span>Extract Tables
|
||
</button>
|
||
<button class="di-mode-btn" data-mode="questions" onclick="setMode('questions',this)">
|
||
<span class="di-mode-icon">❓</span>Q&A
|
||
</button>
|
||
<button class="di-mode-btn" data-mode="translate" onclick="setMode('translate',this)">
|
||
<span class="di-mode-icon">🌐</span>Translate
|
||
</button>
|
||
<div class="di-mode-section">LEGAL & COMPLIANCE</div>
|
||
<button class="di-mode-btn" data-mode="clauses" onclick="setMode('clauses',this)">
|
||
<span class="di-mode-icon">📋</span>Contract Clauses
|
||
</button>
|
||
<button class="di-mode-btn" data-mode="tender" onclick="setMode('tender',this)">
|
||
<span class="di-mode-icon">✅</span>Tender Checklist
|
||
</button>
|
||
<div class="di-mode-section">INSIGHTS</div>
|
||
<button class="di-mode-btn" data-mode="brief" onclick="setMode('brief',this)">
|
||
<span class="di-mode-icon">🗒️</span>Executive Brief
|
||
</button>
|
||
<button class="di-mode-btn" data-mode="compare" onclick="setMode('compare',this)">
|
||
<span class="di-mode-icon">🔄</span>Compare Docs
|
||
</button>
|
||
<button class="di-mode-btn" data-mode="custom" onclick="setMode('custom',this)">
|
||
<span class="di-mode-icon">✏️</span>Custom Prompt
|
||
</button>
|
||
</div>
|
||
|
||
<div class="di-custom-prompt" id="custom-prompt-wrap">
|
||
<textarea id="custom-prompt" placeholder="Enter your custom instruction, e.g. 'List all equipment mentioned with their specifications'"></textarea>
|
||
</div>
|
||
|
||
<div class="di-row">
|
||
<div class="form-group">
|
||
<label>Model</label>
|
||
<select id="model-select">
|
||
<option value="llama3">llama3 (default)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<button id="process-btn" onclick="processDoc()" disabled>
|
||
⚡ Process Document
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Progress -->
|
||
<div class="di-progress" id="progress-card">
|
||
<div class="di-spinner">⚙️</div>
|
||
<div class="di-progress-label" id="progress-label">Extracting text…</div>
|
||
<div class="di-progress-sub">This may take 30–120 seconds depending on file size and model</div>
|
||
</div>
|
||
|
||
<!-- Result -->
|
||
<div class="di-result" id="result-card">
|
||
<div class="di-result-header">
|
||
<span style="font-size:20px" id="result-icon">📝</span>
|
||
<div class="di-result-title" id="result-title">Result</div>
|
||
<div class="di-result-meta" id="result-meta"></div>
|
||
</div>
|
||
<div class="di-result-body" id="result-body"></div>
|
||
<div class="di-result-actions">
|
||
<button class="di-export-btn" onclick="copyResult()">📋 Copy</button>
|
||
<button class="di-export-btn" onclick="downloadMd()">⬇️ Download .md</button>
|
||
<button class="di-export-btn" onclick="printPdf()">🖨️ Print / PDF</button>
|
||
<button class="btn btn-ghost" style="margin-left:auto" onclick="newJob()">+ New Document</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- History -->
|
||
<div class="di-history">
|
||
<h2>Recent Documents</h2>
|
||
<div id="history-wrap">
|
||
<div class="di-empty">No documents processed yet</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<footer>
|
||
<p>Nexus One AI · Powered by Cezen · <span data-brand="tier">Basic Tier</span></p>
|
||
<p>Questions? <a href="mailto:support@cezentech.com">support@cezentech.com</a> · <a href="https://cezentech.com" target="_blank">cezentech.com</a></p>
|
||
</footer>
|
||
|
||
<script>
|
||
const _API = '/api';
|
||
let selectedFile = null;
|
||
let selectedFileB = null;
|
||
let currentMode = 'summarise';
|
||
let currentJobId = null;
|
||
let pollTimer = null;
|
||
let currentResultText = '';
|
||
|
||
const MODE_ICONS = {
|
||
summarise:'📝', keypoints:'🔑', tables:'📊', questions:'❓',
|
||
translate:'🌐', custom:'✏️', clauses:'📋', tender:'✅',
|
||
brief:'🗒️', compare:'🔄'
|
||
};
|
||
const MODE_LABELS = {
|
||
summarise:'Summary', keypoints:'Key Points', tables:'Extracted Tables',
|
||
questions:'Q&A', translate:'Translation', custom:'Result',
|
||
clauses:'Contract Clause Analysis', tender:'Tender Compliance Checklist',
|
||
brief:'Executive Brief', compare:'Document Comparison'
|
||
};
|
||
|
||
// ── File handling ────────────────────────────────────────────────────────────
|
||
const fileInput = document.getElementById('file-input');
|
||
const fileInputB = document.getElementById('file-input-b');
|
||
const uploadZone = document.getElementById('upload-zone');
|
||
const uploadZoneB = document.getElementById('upload-zone-b');
|
||
|
||
fileInput.addEventListener('change', () => { if (fileInput.files[0]) selectFile(fileInput.files[0]); });
|
||
fileInputB.addEventListener('change', () => { if (fileInputB.files[0]) selectFileB(fileInputB.files[0]); });
|
||
|
||
['dragover','dragleave','drop'].forEach(ev => {
|
||
uploadZone.addEventListener(ev, e => {
|
||
e.preventDefault();
|
||
if (ev === 'dragover') uploadZone.classList.add('drag-over');
|
||
else if (ev === 'dragleave') uploadZone.classList.remove('drag-over');
|
||
else { uploadZone.classList.remove('drag-over'); if (e.dataTransfer.files[0]) selectFile(e.dataTransfer.files[0]); }
|
||
});
|
||
uploadZoneB.addEventListener(ev, e => {
|
||
e.preventDefault();
|
||
if (ev === 'dragover') uploadZoneB.classList.add('drag-over');
|
||
else if (ev === 'dragleave') uploadZoneB.classList.remove('drag-over');
|
||
else { uploadZoneB.classList.remove('drag-over'); if (e.dataTransfer.files[0]) selectFileB(e.dataTransfer.files[0]); }
|
||
});
|
||
});
|
||
|
||
function selectFile(file) {
|
||
selectedFile = file;
|
||
const ext = file.name.split('.').pop().toLowerCase();
|
||
const icons = { pdf:'📕', docx:'📘', doc:'📘', txt:'📄', md:'📄',
|
||
png:'🖼️', jpg:'🖼️', jpeg:'🖼️', webp:'🖼️', bmp:'🖼️' };
|
||
document.getElementById('file-icon').textContent = icons[ext] || '📄';
|
||
document.getElementById('file-name').textContent = file.name;
|
||
document.getElementById('file-size').textContent = fmtSize(file.size);
|
||
document.getElementById('file-bar').classList.add('show');
|
||
updateProcessBtn();
|
||
hideResult();
|
||
}
|
||
|
||
function selectFileB(file) {
|
||
selectedFileB = file;
|
||
const ext = file.name.split('.').pop().toLowerCase();
|
||
const icons = { pdf:'📕', docx:'📘', doc:'📘', txt:'📄', md:'📄',
|
||
png:'🖼️', jpg:'🖼️', jpeg:'🖼️', webp:'🖼️', bmp:'🖼️' };
|
||
document.getElementById('file-icon-b').textContent = icons[ext] || '📄';
|
||
document.getElementById('file-name-b').textContent = file.name;
|
||
document.getElementById('file-size-b').textContent = fmtSize(file.size);
|
||
document.getElementById('file-bar-b').classList.add('show');
|
||
updateProcessBtn();
|
||
}
|
||
|
||
function clearFile() {
|
||
selectedFile = null; fileInput.value = '';
|
||
document.getElementById('file-bar').classList.remove('show');
|
||
updateProcessBtn(); hideResult();
|
||
}
|
||
|
||
function clearFileB() {
|
||
selectedFileB = null; fileInputB.value = '';
|
||
document.getElementById('file-bar-b').classList.remove('show');
|
||
updateProcessBtn();
|
||
}
|
||
|
||
function updateProcessBtn() {
|
||
const needsB = (currentMode === 'compare');
|
||
const ready = selectedFile && (!needsB || selectedFileB);
|
||
document.getElementById('process-btn').disabled = !ready;
|
||
}
|
||
|
||
function fmtSize(bytes) {
|
||
if (bytes > 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
|
||
return Math.round(bytes / 1024) + ' KB';
|
||
}
|
||
|
||
// ── Mode ─────────────────────────────────────────────────────────────────────
|
||
function setMode(mode, btn) {
|
||
currentMode = mode;
|
||
document.querySelectorAll('.di-mode-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.getElementById('custom-prompt-wrap').style.display = mode === 'custom' ? 'block' : 'none';
|
||
const compareZone = document.getElementById('compare-zone');
|
||
if (mode === 'compare') compareZone.classList.add('show');
|
||
else { compareZone.classList.remove('show'); }
|
||
updateProcessBtn();
|
||
}
|
||
|
||
// ── Process ──────────────────────────────────────────────────────────────────
|
||
async function processDoc() {
|
||
if (!selectedFile) return;
|
||
if (currentMode === 'custom' && !document.getElementById('custom-prompt').value.trim()) {
|
||
alert('Please enter a custom instruction first.'); return;
|
||
}
|
||
if (currentMode === 'compare' && !selectedFileB) {
|
||
alert('Please upload a second document to compare against.'); return;
|
||
}
|
||
|
||
const model = document.getElementById('model-select').value;
|
||
const fd = new FormData();
|
||
fd.append('file', selectedFile);
|
||
fd.append('mode', currentMode);
|
||
fd.append('model', model);
|
||
if (currentMode === 'custom') fd.append('custom_prompt', document.getElementById('custom-prompt').value.trim());
|
||
if (selectedFileB) fd.append('file_b', selectedFileB);
|
||
|
||
document.getElementById('process-btn').disabled = true;
|
||
document.getElementById('progress-label').textContent = 'Uploading and extracting text…';
|
||
document.getElementById('progress-card').classList.add('show');
|
||
hideResult();
|
||
|
||
// Try real API first, fall back to mock
|
||
try {
|
||
const res = await fetch(`${_API}/docjobs`, { method: 'POST', credentials: 'include', body: fd });
|
||
if (!res.ok) throw new Error('API unavailable');
|
||
const job = await res.json();
|
||
currentJobId = job.id;
|
||
pollJob(job.id);
|
||
} catch(e) {
|
||
// API unavailable — run mock demo
|
||
runMockProcess();
|
||
}
|
||
}
|
||
|
||
function runMockProcess() {
|
||
const steps = [
|
||
'Extracting text from document…',
|
||
'Sending to AI model…',
|
||
'Model is analysing…',
|
||
'Formatting results…'
|
||
];
|
||
let i = 0;
|
||
const lbl = document.getElementById('progress-label');
|
||
const iv = setInterval(() => {
|
||
lbl.textContent = steps[Math.min(i++, steps.length-1)];
|
||
if (i >= steps.length + 1) {
|
||
clearInterval(iv);
|
||
const mockResult = generateMockResult(currentMode, selectedFile ? selectedFile.name : 'document.pdf');
|
||
document.getElementById('progress-card').classList.remove('show');
|
||
document.getElementById('process-btn').disabled = false;
|
||
showStructuredResult(currentMode, mockResult, selectedFile ? selectedFile.name : 'document.pdf');
|
||
saveLocalHistory(selectedFile ? selectedFile.name : 'document.pdf', currentMode);
|
||
renderLocalHistory();
|
||
}
|
||
}, 900);
|
||
}
|
||
|
||
function pollJob(id) {
|
||
if (pollTimer) clearInterval(pollTimer);
|
||
const labels = ['Extracting text from document…','Sending to AI model…','Model is thinking…','Almost done…'];
|
||
let tick = 0;
|
||
pollTimer = setInterval(async () => {
|
||
try {
|
||
document.getElementById('progress-label').textContent = labels[Math.min(tick++, labels.length-1)];
|
||
const res = await fetch(`${_API}/docjobs/${id}`, { credentials: 'include' });
|
||
const job = await res.json();
|
||
if (job.status === 'done') {
|
||
clearInterval(pollTimer);
|
||
showStructuredResult(job.mode, job.result, job.orig_name);
|
||
loadHistory();
|
||
document.getElementById('process-btn').disabled = false;
|
||
document.getElementById('progress-card').classList.remove('show');
|
||
} else if (job.status === 'error') {
|
||
clearInterval(pollTimer);
|
||
document.getElementById('progress-card').classList.remove('show');
|
||
document.getElementById('process-btn').disabled = false;
|
||
alert('Processing failed: ' + (job.error_msg || 'Unknown error'));
|
||
loadHistory();
|
||
}
|
||
} catch(e) { /* keep polling */ }
|
||
}, 2500);
|
||
}
|
||
|
||
// ── Result rendering ─────────────────────────────────────────────────────────
|
||
function showStructuredResult(mode, data, filename) {
|
||
document.getElementById('result-icon').textContent = MODE_ICONS[mode] || '📝';
|
||
document.getElementById('result-title').textContent = `${MODE_LABELS[mode] || 'Result'} — ${filename}`;
|
||
document.getElementById('result-meta').textContent = new Date().toLocaleTimeString();
|
||
const body = document.getElementById('result-body');
|
||
|
||
if (mode === 'clauses' && typeof data === 'object' && data.clauses) {
|
||
body.classList.add('structured');
|
||
body.innerHTML = renderClauses(data);
|
||
currentResultText = exportClausesText(data);
|
||
} else if (mode === 'tender' && typeof data === 'object' && data.items) {
|
||
body.classList.add('structured');
|
||
body.innerHTML = renderTenderChecklist(data);
|
||
currentResultText = exportTenderText(data);
|
||
} else if (mode === 'brief' && typeof data === 'object' && data.summary) {
|
||
body.classList.add('structured');
|
||
body.innerHTML = renderBrief(data);
|
||
currentResultText = exportBriefText(data);
|
||
} else if (mode === 'compare' && typeof data === 'object' && data.diffs) {
|
||
body.classList.add('structured');
|
||
body.innerHTML = renderCompare(data);
|
||
currentResultText = exportCompareText(data);
|
||
} else {
|
||
body.classList.remove('structured');
|
||
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||
body.textContent = text;
|
||
currentResultText = text;
|
||
}
|
||
|
||
document.getElementById('result-card').classList.add('show');
|
||
}
|
||
|
||
// ── Clause renderer ───────────────────────────────────────────────────────────
|
||
function renderClauses(data) {
|
||
const counts = {high:0, medium:0, low:0};
|
||
data.clauses.forEach(c => counts[c.risk] = (counts[c.risk]||0)+1);
|
||
let html = `<div class="di-clause-summary">
|
||
<span class="di-clause-stat red">⚠️ ${counts.high||0} High Risk</span>
|
||
<span class="di-clause-stat amber">⚡ ${counts.medium||0} Medium Risk</span>
|
||
<span class="di-clause-stat green">✓ ${counts.low||0} Low Risk</span>
|
||
</div><div class="di-clauses">`;
|
||
data.clauses.forEach((c, i) => {
|
||
html += `<div class="di-clause risk-${c.risk}" id="clause-${i}">
|
||
<div class="di-clause-header" onclick="toggleClause(${i})">
|
||
<span class="di-clause-type">${c.type}</span>
|
||
<span class="di-clause-risk">${c.risk} risk</span>
|
||
${c.page ? `<span class="di-clause-pg">p.${c.page}</span>` : ''}
|
||
<span class="di-clause-chevron">▶</span>
|
||
</div>
|
||
<div class="di-clause-body">
|
||
<div>${c.analysis}</div>
|
||
${c.excerpt ? `<div class="di-clause-excerpt">"${c.excerpt}"</div>` : ''}
|
||
</div>
|
||
</div>`;
|
||
});
|
||
return html + '</div>';
|
||
}
|
||
function toggleClause(i) {
|
||
document.getElementById('clause-' + i).classList.toggle('open');
|
||
}
|
||
|
||
// ── Tender checklist renderer ─────────────────────────────────────────────────
|
||
function renderTenderChecklist(data) {
|
||
const pass = data.items.filter(i=>i.status==='pass').length;
|
||
const total = data.items.length;
|
||
const pct = Math.round(pass/total*100);
|
||
let html = `<div class="di-checklist-summary">
|
||
<span class="di-clause-stat green">✓ ${pass} Met</span>
|
||
<span class="di-clause-stat red">✗ ${data.items.filter(i=>i.status==='fail').length} Not Met</span>
|
||
<span class="di-clause-stat amber">? ${data.items.filter(i=>i.status==='missing').length} Missing</span>
|
||
</div>
|
||
<div class="di-check-prog"><div class="di-check-prog-bar" style="width:${pct}%"></div></div>
|
||
<div class="di-checklist">`;
|
||
const statusIcon = {pass:'✅', fail:'❌', missing:'⚠️'};
|
||
const statusLabel = {pass:'Met', fail:'Not Met', missing:'Missing'};
|
||
data.items.forEach(item => {
|
||
html += `<div class="di-check-item ${item.status}">
|
||
<span class="di-check-icon">${statusIcon[item.status]||'❓'}</span>
|
||
<div class="di-check-body">
|
||
<div class="di-check-title">${item.requirement}</div>
|
||
${item.note ? `<div class="di-check-note">${item.note}</div>` : ''}
|
||
</div>
|
||
<span class="di-check-status">${statusLabel[item.status]||item.status}</span>
|
||
</div>`;
|
||
});
|
||
return html + '</div>';
|
||
}
|
||
|
||
// ── Executive brief renderer ──────────────────────────────────────────────────
|
||
function renderBrief(data) {
|
||
let html = `<div class="di-brief">
|
||
<div class="di-brief-meta">`;
|
||
if (data.doc_type) html += `<div class="di-brief-meta-item"><div class="di-brief-meta-label">Document Type</div><div class="di-brief-meta-val">${data.doc_type}</div></div>`;
|
||
if (data.date) html += `<div class="di-brief-meta-item"><div class="di-brief-meta-label">Date</div><div class="di-brief-meta-val">${data.date}</div></div>`;
|
||
if (data.parties) html += `<div class="di-brief-meta-item"><div class="di-brief-meta-label">Parties</div><div class="di-brief-meta-val">${data.parties}</div></div>`;
|
||
if (data.value) html += `<div class="di-brief-meta-item"><div class="di-brief-meta-label">Value / Amount</div><div class="di-brief-meta-val">${data.value}</div></div>`;
|
||
html += `</div>
|
||
<div class="di-brief-section">
|
||
<div class="di-brief-section-title">Executive Summary</div>
|
||
<p>${data.summary}</p>
|
||
</div>`;
|
||
if (data.key_facts && data.key_facts.length) {
|
||
html += `<div class="di-brief-section">
|
||
<div class="di-brief-section-title">Key Facts</div>
|
||
<div class="di-brief-kv-grid">`;
|
||
data.key_facts.forEach(f => {
|
||
html += `<div class="di-brief-kv"><div class="di-brief-kv-label">${f.label}</div><div class="di-brief-kv-val">${f.value}</div></div>`;
|
||
});
|
||
html += `</div></div>`;
|
||
}
|
||
if (data.risks && data.risks.length) {
|
||
html += `<div class="di-brief-section">
|
||
<div class="di-brief-section-title">Key Risks & Watch Points</div>`;
|
||
data.risks.forEach(r => {
|
||
html += `<div class="di-brief-risk-item">
|
||
<div class="di-brief-risk-dot ${r.level}"></div>
|
||
<div style="font-size:13px;color:var(--ink)">${r.text}</div>
|
||
</div>`;
|
||
});
|
||
html += `</div>`;
|
||
}
|
||
if (data.recommendations && data.recommendations.length) {
|
||
html += `<div class="di-brief-section">
|
||
<div class="di-brief-section-title">Recommendations</div>
|
||
<ol style="margin:0;padding-left:18px;font-size:13px;color:var(--med);line-height:1.8">`;
|
||
data.recommendations.forEach(r => html += `<li>${r}</li>`);
|
||
html += `</ol></div>`;
|
||
}
|
||
return html + '</div>';
|
||
}
|
||
|
||
// ── Compare renderer ──────────────────────────────────────────────────────────
|
||
function renderCompare(data) {
|
||
let html = `<div class="di-compare-grid">
|
||
<div class="di-compare-col">
|
||
<div class="di-compare-col-header">📄 ${data.file_a || 'Document A'}</div>
|
||
<div class="di-compare-col-body">${escHtml(data.summary_a || '')}</div>
|
||
</div>
|
||
<div class="di-compare-col">
|
||
<div class="di-compare-col-header">📄 ${data.file_b || 'Document B'}</div>
|
||
<div class="di-compare-col-body">${escHtml(data.summary_b || '')}</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:20px">
|
||
<div style="font-size:12px;font-weight:700;color:var(--lt);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px">Key Differences</div>
|
||
<table class="di-compare-diff-table"><tr>
|
||
<th style="width:140px">Area</th><th>Document A</th><th>Document B</th><th style="width:90px">Change</th>
|
||
</tr>`;
|
||
data.diffs.forEach(d => {
|
||
html += `<tr class="diff-${d.type}">
|
||
<td style="font-weight:600">${d.area}</td>
|
||
<td>${escHtml(d.a)}</td>
|
||
<td>${escHtml(d.b)}</td>
|
||
<td><span class="diff-badge">${d.type}</span></td>
|
||
</tr>`;
|
||
});
|
||
return html + '</table></div>';
|
||
}
|
||
|
||
// ── Export helpers ────────────────────────────────────────────────────────────
|
||
function exportClausesText(data) {
|
||
return data.clauses.map(c =>
|
||
`## ${c.type} [${c.risk} risk]\n${c.analysis}\n${c.excerpt ? `> "${c.excerpt}"` : ''}`
|
||
).join('\n\n');
|
||
}
|
||
function exportTenderText(data) {
|
||
const icons = {pass:'✅', fail:'❌', missing:'⚠️'};
|
||
return data.items.map(i => `${icons[i.status]} ${i.requirement}${i.note ? '\n ' + i.note : ''}`).join('\n');
|
||
}
|
||
function exportBriefText(data) {
|
||
let t = `# Executive Brief\n\n## Summary\n${data.summary}\n\n`;
|
||
if (data.risks) t += `## Risks\n` + data.risks.map(r => `- [${r.level}] ${r.text}`).join('\n') + '\n\n';
|
||
if (data.recommendations) t += `## Recommendations\n` + data.recommendations.map((r,i) => `${i+1}. ${r}`).join('\n');
|
||
return t;
|
||
}
|
||
function exportCompareText(data) {
|
||
let t = `# Document Comparison\n\n`;
|
||
t += `## ${data.file_a || 'Document A'}\n${data.summary_a || ''}\n\n`;
|
||
t += `## ${data.file_b || 'Document B'}\n${data.summary_b || ''}\n\n`;
|
||
t += `## Key Differences\n`;
|
||
data.diffs.forEach(d => t += `| ${d.area} | ${d.a} | ${d.b} | ${d.type} |\n`);
|
||
return t;
|
||
}
|
||
|
||
function copyResult() {
|
||
navigator.clipboard.writeText(currentResultText).then(() => {
|
||
const btn = event.target;
|
||
const orig = btn.textContent;
|
||
btn.textContent = '✓ Copied!';
|
||
setTimeout(() => btn.textContent = orig, 1500);
|
||
});
|
||
}
|
||
|
||
function downloadMd() {
|
||
const fname = (document.getElementById('result-title').textContent || 'result').replace(/[^a-z0-9]/gi,'_').toLowerCase() + '.md';
|
||
const blob = new Blob([currentResultText], {type:'text/markdown'});
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = fname;
|
||
a.click();
|
||
}
|
||
|
||
function printPdf() {
|
||
window.print();
|
||
}
|
||
|
||
function hideResult() {
|
||
document.getElementById('result-card').classList.remove('show');
|
||
document.getElementById('progress-card').classList.remove('show');
|
||
}
|
||
|
||
function newJob() {
|
||
clearFile(); clearFileB(); hideResult();
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
// ── Mock data generators ──────────────────────────────────────────────────────
|
||
function generateMockResult(mode, filename) {
|
||
if (mode === 'clauses') return {
|
||
clauses: [
|
||
{ type:'Limitation of Liability', risk:'high', page:8, analysis:'Vendor liability is capped at 3 months of fees paid. This is unusually low for a contract of this size and provides minimal recourse for significant losses.', excerpt:"In no event shall Vendor's total cumulative liability exceed the fees paid in the three (3) months preceding the claim." },
|
||
{ type:'Payment Terms', risk:'medium', page:4, analysis:'Payment is due within 15 days of invoice. Late payments attract 2% monthly interest. Ensure your AP process can meet this timeline.', excerpt:'Invoices are payable within fifteen (15) calendar days. Overdue amounts accrue interest at 2% per month.' },
|
||
{ type:'Termination for Convenience', risk:'medium', page:12, analysis:'Either party may terminate with 90 days notice. However, early termination by the Client incurs a 25% penalty on remaining contract value.', excerpt:'Client may terminate for convenience upon 90 days written notice, subject to an early termination fee of 25% of the remaining contract value.' },
|
||
{ type:'Intellectual Property', risk:'low', page:6, analysis:'Work product created specifically for the Client is assigned to the Client upon full payment. Pre-existing IP remains with the Vendor.', excerpt:'Upon receipt of full payment, Vendor assigns all rights in Work Product created specifically for Client under this agreement.' },
|
||
{ type:'Confidentiality', risk:'low', page:9, analysis:'Standard mutual NDA covering a 3-year post-contract period. Definition of confidential information is broad and appropriate.', excerpt:'Both parties agree to maintain confidentiality of Confidential Information for a period of three (3) years after termination.' },
|
||
{ type:'Governing Law', risk:'low', page:15, analysis:'Agreement is governed by the laws of England and Wales with disputes subject to London arbitration. Standard and enforceable.', excerpt:'This Agreement shall be governed by and construed in accordance with the laws of England and Wales.' }
|
||
]
|
||
};
|
||
if (mode === 'tender') return {
|
||
items: [
|
||
{ requirement:'Company registration certificate', status:'pass', note:'Certificate No. CIN U72900MH2018PTC123456 provided' },
|
||
{ requirement:'Audited financial statements (last 3 years)', status:'pass', note:'FY2022, FY2023, FY2024 statements included' },
|
||
{ requirement:'Minimum annual turnover ₹5 Crore', status:'pass', note:'FY2024 turnover: ₹8.2 Crore — compliant' },
|
||
{ requirement:'ISO 27001 certification', status:'missing', note:'Certificate not included. Must be submitted before bid evaluation' },
|
||
{ requirement:'Technical team CVs (minimum 5 qualified engineers)', status:'pass', note:'7 CVs submitted with relevant qualifications' },
|
||
{ requirement:'Experience letter from 2 similar projects', status:'pass', note:'Letters from Reliance Industries and TCS attached' },
|
||
{ requirement:'EMD (Earnest Money Deposit) — ₹2,50,000', status:'pass', note:'Demand draft No. 482910 from SBI, dated 15 Jun 2026' },
|
||
{ requirement:'Signed declaration of no conflict of interest', status:'fail', note:'Declaration form is unsigned — requires authorised signatory' },
|
||
{ requirement:'Technical proposal (max 40 pages)', status:'pass', note:'38-page proposal submitted in prescribed format' },
|
||
{ requirement:'Commercial bid in sealed envelope', status:'missing', note:'Commercial bid envelope not found in submission package' }
|
||
]
|
||
};
|
||
if (mode === 'brief') return {
|
||
doc_type: 'Master Services Agreement',
|
||
date: '12 June 2026',
|
||
parties: 'Cezen Technologies Pvt. Ltd. & Infra Solutions Ltd.',
|
||
value: '₹1.4 Crore / year',
|
||
summary: 'This Master Services Agreement establishes a three-year engagement for AI platform deployment and managed services. Cezen Technologies will deliver an on-premises AI infrastructure including model hosting, knowledge base management, and 24×7 L2/L3 support. The contract favours the vendor on liability caps and early termination penalties. The client has limited audit rights and no SLA credits above ₹5 lakh per year.',
|
||
key_facts: [
|
||
{ label:'Contract Duration', value:'36 months (Jun 2026 – May 2029)' },
|
||
{ label:'Total Contract Value', value:'₹4.2 Crore' },
|
||
{ label:'Payment Schedule', value:'Quarterly in advance' },
|
||
{ label:'SLA Uptime Commitment', value:'99.5% monthly' },
|
||
{ label:'Support Response SLA', value:'P1: 2 hrs, P2: 8 hrs' },
|
||
{ label:'Auto-Renewal', value:'Yes — 30-day opt-out window' }
|
||
],
|
||
risks: [
|
||
{ level:'high', text:'Liability cap (3 months fees) is disproportionately low for a ₹4.2 Cr contract. Negotiate to at least 12 months.' },
|
||
{ level:'high', text:'25% early termination penalty locks in commitment. Seek a mutual termination clause without penalty after Year 1.' },
|
||
{ level:'medium', text:'Auto-renewal with only 30-day opt-out window. Flag for calendar reminder 6 months before contract end.' },
|
||
{ level:'medium', text:'SLA credits capped at ₹5 lakh/year regardless of actual impact. Insufficient for business-critical deployments.' },
|
||
{ level:'low', text:'Governing law is England & Wales. Consider adding an Indian arbitration clause for practical dispute resolution.' }
|
||
],
|
||
recommendations: [
|
||
'Negotiate liability cap to minimum 12 months of fees (₹1.4 Cr) before signing.',
|
||
'Insert a mutual termination clause with no penalty after the first year of service.',
|
||
'Add SLA credit mechanics tied to actual business impact, uncapped up to 20% of annual fees.',
|
||
'Request quarterly business reviews and formal audit rights with 10-day notice.',
|
||
'Set a calendar reminder for contract renewal opt-out — deadline is 30 April 2029.'
|
||
]
|
||
};
|
||
if (mode === 'compare') return {
|
||
file_a: selectedFile ? selectedFile.name : 'Document A',
|
||
file_b: selectedFileB ? selectedFileB.name : 'Document B',
|
||
summary_a: 'Version 2.1 (March 2025) — Original contract with standard vendor-favourable terms. Payment net-30, liability capped at 3 months fees, 90-day termination notice with 25% early exit penalty. Governing law: England & Wales.',
|
||
summary_b: 'Version 2.3 (June 2026) — Revised contract following negotiation. Payment net-15 (tightened), liability cap increased to 6 months, termination notice reduced to 60 days, early exit penalty removed after Year 1. SLA credits added.',
|
||
diffs: [
|
||
{ area:'Payment Terms', a:'Net 30 days', b:'Net 15 days', type:'changed' },
|
||
{ area:'Liability Cap', a:'3 months fees', b:'6 months fees', type:'changed' },
|
||
{ area:'Termination Notice', a:'90 days', b:'60 days', type:'changed' },
|
||
{ area:'Early Exit Penalty', a:'25% of remaining value', b:'Waived after Year 1', type:'changed' },
|
||
{ area:'SLA Credits', a:'Not included', b:'Up to 20% annual fees', type:'added' },
|
||
{ area:'Audit Rights', a:'Not included', b:'Annual audit with notice', type:'added' },
|
||
{ area:'Auto-Renewal', a:'30-day opt-out window', b:'90-day opt-out window', type:'changed' },
|
||
{ area:'Governing Law', a:'England & Wales', b:'England & Wales (unchanged)', type:'changed' }
|
||
]
|
||
};
|
||
// Default text modes
|
||
const textMock = {
|
||
summarise: `This document outlines a comprehensive services agreement between two parties for AI platform deployment. The key provisions cover a 36-month engagement, quarterly payment terms, and specific deliverables around model hosting and knowledge management. The vendor provides 99.5% uptime SLA with defined support response times. Notable clauses include an auto-renewal provision and a liability cap set at three months of fees paid.`,
|
||
keypoints: `• Contract Duration: 36 months starting June 2026\n• Total Value: ₹4.2 Crore payable quarterly in advance\n• SLA Commitment: 99.5% monthly uptime\n• Support: P1 response within 2 hours, P2 within 8 hours\n• Liability Cap: 3 months of fees paid (~₹35 lakhs)\n• Auto-renewal with 30-day opt-out window\n• Governing Law: England and Wales`,
|
||
tables: `Table 1: Service Level Definitions\n────────────────────────────────────\nPriority | Description | Response | Resolution\nP1 | Total service outage | 2 hrs | 4 hrs\nP2 | Major degradation | 8 hrs | 24 hrs\nP3 | Minor issues | 24 hrs | 72 hrs\n\nTable 2: Payment Schedule\n──────────────────────────\nQuarter | Amount (₹) | Due Date\nQ1 | 35,00,000 | 1 Jul 2026\nQ2 | 35,00,000 | 1 Oct 2026\nQ3 | 35,00,000 | 1 Jan 2027\nQ4 | 35,00,000 | 1 Apr 2027`,
|
||
questions: `Q: What is the contract duration?\nA: 36 months, from June 2026 to May 2029.\n\nQ: What is the liability cap?\nA: Vendor liability is capped at 3 months of fees paid, approximately ₹35 lakhs.\n\nQ: What happens if we want to exit early?\nA: A 25% penalty on remaining contract value applies if the client terminates for convenience.\n\nQ: Is there an SLA?\nA: Yes — 99.5% monthly uptime with P1 response within 2 hours and P2 within 8 hours.`,
|
||
translate: `[Translated from Hindi/Regional Language]\n\nThis agreement is entered into between Cezen Technologies Private Limited (hereinafter "Service Provider") and Infra Solutions Limited (hereinafter "Client") on the twelfth day of June, Two Thousand and Twenty-Six.\n\nThe Service Provider agrees to provide AI platform services including model hosting, knowledge base management, user access management, and technical support as described in Schedule A attached hereto...`,
|
||
custom: `Analysis complete. The document has been processed according to your custom instructions. Key findings have been extracted and formatted below.\n\n[Mock output — connect to a running Ollama or OpenAI-compatible API to get real AI-powered results for your specific prompt.]`
|
||
};
|
||
return textMock[mode] || textMock.summarise;
|
||
}
|
||
|
||
// ── History ───────────────────────────────────────────────────────────────────
|
||
const HIST_KEY = 'cezen_docjobs';
|
||
|
||
function saveLocalHistory(filename, mode) {
|
||
const hist = JSON.parse(localStorage.getItem(HIST_KEY) || '[]');
|
||
hist.unshift({ id: Date.now(), orig_name: filename, mode, status:'done', created_at: new Date().toISOString() });
|
||
localStorage.setItem(HIST_KEY, JSON.stringify(hist.slice(0, 20)));
|
||
}
|
||
|
||
function renderLocalHistory() {
|
||
const hist = JSON.parse(localStorage.getItem(HIST_KEY) || '[]');
|
||
const wrap = document.getElementById('history-wrap');
|
||
if (!hist.length) { wrap.innerHTML = '<div class="di-empty">No documents processed yet</div>'; return; }
|
||
let html = `<div class="di-hist-table-wrap"><table class="di-hist-table">
|
||
<tr><th>File</th><th>Mode</th><th>Status</th><th>When</th></tr>`;
|
||
hist.forEach(j => {
|
||
const icon = MODE_ICONS[j.mode] || '📄';
|
||
const label = MODE_LABELS[j.mode] || j.mode;
|
||
const when = j.created_at ? new Date(j.created_at).toLocaleString() : '—';
|
||
html += `<tr>
|
||
<td><span style="margin-right:6px">${icon}</span>${escHtml(j.orig_name)}</td>
|
||
<td>${label}</td>
|
||
<td><span class="di-status-badge done">done</span></td>
|
||
<td style="color:var(--lt);font-size:12px">${when}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</table></div>';
|
||
wrap.innerHTML = html;
|
||
}
|
||
|
||
async function loadHistory() {
|
||
try {
|
||
const res = await fetch(`${_API}/docjobs`, { credentials: 'include' });
|
||
const jobs = await res.json();
|
||
renderApiHistory(jobs);
|
||
} catch(e) { renderLocalHistory(); }
|
||
}
|
||
|
||
function renderApiHistory(jobs) {
|
||
const wrap = document.getElementById('history-wrap');
|
||
if (!jobs.length) { wrap.innerHTML = '<div class="di-empty">No documents processed yet</div>'; return; }
|
||
let html = `<div class="di-hist-table-wrap"><table class="di-hist-table">
|
||
<tr><th>File</th><th>Mode</th><th>Model</th><th>Status</th><th>When</th><th></th></tr>`;
|
||
jobs.forEach(j => {
|
||
const statusClass = j.status === 'done' ? 'done' : j.status === 'error' ? 'error'
|
||
: j.status === 'processing' ? 'processing' : 'pending';
|
||
const icon = MODE_ICONS[j.mode] || '📄';
|
||
const label = MODE_LABELS[j.mode] || j.mode;
|
||
const when = j.created_at ? new Date(j.created_at).toLocaleString() : '—';
|
||
html += `<tr>
|
||
<td><span style="margin-right:6px">${icon}</span>${escHtml(j.orig_name)}</td>
|
||
<td>${label}</td>
|
||
<td style="color:var(--lt)">${escHtml(j.model || '—')}</td>
|
||
<td><span class="di-status-badge ${statusClass}">${j.status}</span></td>
|
||
<td style="color:var(--lt);font-size:12px">${when}</td>
|
||
<td><button class="btn btn-ghost" style="padding:4px 10px;font-size:12px"
|
||
onclick="event.stopPropagation();deleteJob(${j.id})">✕</button></td>
|
||
</tr>`;
|
||
});
|
||
html += '</table></div>';
|
||
wrap.innerHTML = html;
|
||
}
|
||
|
||
async function deleteJob(id) {
|
||
if (!confirm('Delete this document job?')) return;
|
||
await fetch(`${_API}/docjobs/${id}`, { method: 'DELETE', credentials: 'include' });
|
||
loadHistory();
|
||
}
|
||
|
||
function escHtml(s) {
|
||
return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
// ── Load models ───────────────────────────────────────────────────────────────
|
||
async function loadModels() {
|
||
try {
|
||
const res = await fetch(`${_API}/models/list`, { credentials: 'include' });
|
||
const data = await res.json();
|
||
const sel = document.getElementById('model-select');
|
||
const models = data.models || [];
|
||
if (!models.length) throw new Error();
|
||
sel.innerHTML = models.map(m =>
|
||
`<option value="${escHtml(m.name)}">${escHtml(m.name)}</option>`
|
||
).join('');
|
||
} catch(e) {
|
||
document.getElementById('model-select').innerHTML =
|
||
'<option value="llama3">llama3 (default)</option>';
|
||
}
|
||
}
|
||
|
||
loadModels();
|
||
loadHistory();
|
||
</script>
|
||
|
||
<script src="auth.js"></script>
|
||
<script src="branding.js"></script>
|
||
</body>
|
||
</html>
|