aipackage/cezen-portal/meeting.html
2026-06-30 10:51:41 +05:30

947 lines
48 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meeting Assistant — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=4">
<style>
.ma-wrap { display:grid; grid-template-columns:320px 1fr; min-height:calc(100vh - 116px); }
@media(max-width:900px){ .ma-wrap { grid-template-columns:1fr; } }
/* ── Left panel ── */
.ma-left { border-right:1px solid var(--bdr); background:var(--navy2); display:flex; flex-direction:column; }
.ma-left-header { padding:16px 18px 12px; border-bottom:1px solid var(--bdr); }
.ma-left-title { font-size:13px; font-weight:700; color:var(--ink); margin-bottom:10px; }
/* ── Upload zone ── */
.ma-drop-zone { border:2px dashed var(--bdr); border-radius:12px; padding:28px 16px; text-align:center; cursor:pointer; transition:.15s; background:var(--bg); }
.ma-drop-zone:hover, .ma-drop-zone.drag-over { border-color:var(--purple); background:rgba(124,58,237,.04); }
.ma-drop-icon { font-size:32px; margin-bottom:8px; }
.ma-drop-label { font-size:13px; font-weight:600; color:var(--ink); margin-bottom:4px; }
.ma-drop-sub { font-size:11px; color:var(--lt); }
.ma-drop-input { display:none; }
.ma-file-chip { display:flex; align-items:center; gap:10px; padding:10px 14px; background:rgba(124,58,237,.06); border:1px solid rgba(124,58,237,.18); border-radius:10px; margin-top:10px; }
.ma-file-icon { font-size:20px; }
.ma-file-name { font-size:12px; font-weight:600; color:var(--ink); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ma-file-remove{ background:none; border:none; color:var(--lt); cursor:pointer; font-size:16px; padding:0; }
.ma-file-remove:hover{ color:#DC2626; }
/* ── Divider ── */
.ma-or { display:flex; align-items:center; gap:10px; margin:14px 0; color:var(--lt); font-size:11px; font-weight:700; letter-spacing:.5px; }
.ma-or::before, .ma-or::after { content:''; flex:1; height:1px; background:var(--bdr); }
/* ── Paste transcript ── */
.ma-paste { width:100%; box-sizing:border-box; padding:10px 12px; border:1.5px solid var(--bdr); border-radius:8px; font-family:inherit; font-size:12px; color:var(--ink); resize:vertical; min-height:100px; background:var(--navy2); }
.ma-paste:focus { outline:none; border-color:var(--purple); }
/* ── Meeting meta ── */
.ma-meta-section { padding:14px 18px; border-bottom:1px solid var(--bdr); }
.ma-meta-title { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin-bottom:10px; }
.ma-field { display:flex; flex-direction:column; gap:4px; margin-bottom:10px; }
.ma-field label { font-size:11px; color:var(--lt); font-weight:600; }
.ma-field input, .ma-field select { padding:8px 10px; border:1.5px solid var(--bdr); border-radius:8px; font-family:inherit; font-size:12px; color:var(--ink); background:var(--navy2); }
.ma-field input:focus, .ma-field select:focus { outline:none; border-color:var(--purple); }
/* ── Attendees ── */
.ma-att-chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
.ma-att-chip { display:inline-flex; align-items:center; gap:5px; background:rgba(124,58,237,.08); border:1px solid rgba(124,58,237,.18); border-radius:20px; padding:3px 10px; font-size:11px; color:var(--purple); font-weight:600; }
.ma-att-chip button { background:none; border:none; color:var(--lt); cursor:pointer; font-size:13px; padding:0; line-height:1; }
.ma-att-chip button:hover { color:#DC2626; }
.ma-att-input { display:flex; gap:6px; margin-top:6px; }
.ma-att-input input { flex:1; padding:6px 10px; border:1.5px solid var(--bdr); border-radius:8px; font-family:inherit; font-size:12px; color:var(--ink); background:var(--navy2); }
.ma-att-add { padding:6px 12px; background:rgba(124,58,237,.08); border:1px solid rgba(124,58,237,.18); border-radius:8px; font-family:inherit; font-size:12px; font-weight:600; color:var(--purple); cursor:pointer; transition:.15s; }
.ma-att-add:hover { background:rgba(124,58,237,.14); }
/* ── Options ── */
.ma-opts { padding:14px 18px; border-bottom:1px solid var(--bdr); }
.ma-opt-row { display:flex; align-items:center; gap:10px; margin-bottom:10px; }
.ma-opt-row:last-child { margin-bottom:0; }
.ma-opt-row input[type=checkbox] { accent-color:var(--purple); width:14px; height:14px; flex-shrink:0; }
.ma-opt-label { font-size:12px; color:var(--ink); font-weight:500; }
/* ── Run button ── */
.ma-run-wrap { padding:14px 18px; margin-top:auto; }
.ma-run-btn { display:flex; align-items:center; justify-content:center; gap:8px; width:100%; padding:12px; background:linear-gradient(135deg,var(--purple),var(--pink)); color:white; border:none; border-radius:10px; font-family:inherit; font-size:14px; font-weight:700; cursor:pointer; transition:.15s; }
.ma-run-btn:hover { filter:brightness(1.08); transform:translateY(-1px); box-shadow:0 6px 24px rgba(124,58,237,.35); }
.ma-run-btn:disabled { opacity:.5; cursor:not-allowed; transform:none; }
/* ── Right panel (output) ── */
.ma-right { padding:28px 32px; overflow-y:auto; background:var(--bg); }
@media(max-width:900px){ .ma-right { padding:20px 16px; } }
/* ── Progress ── */
.ma-progress { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:28px; margin-bottom:24px; display:none; }
.ma-progress.active { display:block; }
.ma-progress-title { font-size:14px; font-weight:700; color:var(--ink); margin-bottom:16px; }
.ma-step-list { display:flex; flex-direction:column; gap:10px; }
.ma-step-row { display:flex; align-items:center; gap:12px; }
.ma-step-dot { width:24px; height:24px; border-radius:50%; border:2px solid var(--bdr); display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; flex-shrink:0; transition:.3s; }
.ma-step-dot.done { background:var(--purple); border-color:var(--purple); color:white; }
.ma-step-dot.running { background:rgba(124,58,237,.1); border-color:var(--purple); color:var(--purple); animation:pulse 1.2s infinite; }
.ma-step-dot.pending { background:var(--bg); color:var(--lt); }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
.ma-step-label { font-size:13px; color:var(--ink); font-weight:500; }
.ma-step-label.running { color:var(--purple); }
.ma-step-label.pending { color:var(--lt); }
/* ── Output tabs ── */
.ma-tabs { display:flex; gap:4px; border-bottom:2px solid var(--bdr); margin-bottom:20px; }
.ma-tab { padding:8px 18px; border-radius:8px 8px 0 0; font-size:13px; font-weight:600; color:var(--lt); background:none; border:none; cursor:pointer; transition:.15s; font-family:inherit; border-bottom:2px solid transparent; margin-bottom:-2px; }
.ma-tab:hover { color:var(--ink); }
.ma-tab.active { color:var(--purple); border-bottom-color:var(--purple); }
.ma-tab-panel { display:none; }
.ma-tab-panel.active { display:block; }
/* ── Output cards ── */
.ma-out-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; margin-bottom:16px; overflow:hidden; }
.ma-out-hdr { padding:14px 18px; border-bottom:1px solid var(--bdr); display:flex; align-items:center; gap:10px; }
.ma-out-title{ font-size:13px; font-weight:700; color:var(--ink); flex:1; }
.ma-out-body { padding:18px; }
.ma-out-text { font-size:13px; color:var(--ink); line-height:1.75; white-space:pre-wrap; }
/* ── Transcript ── */
.ma-transcript { font-size:12.5px; color:var(--ink); line-height:1.8; white-space:pre-wrap; max-height:480px; overflow-y:auto; background:var(--bg); border-radius:8px; padding:14px; border:1px solid var(--bdr); }
.ma-transcript .speaker { font-weight:700; color:var(--purple); }
/* ── Action items table ── */
.ma-action-table { width:100%; border-collapse:collapse; }
.ma-action-table th { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.4px; padding:0 0 10px; text-align:left; border-bottom:1px solid var(--bdr); }
.ma-action-table td { padding:11px 0; border-bottom:1px solid var(--bdr); font-size:13px; vertical-align:top; }
.ma-action-table tr:last-child td { border-bottom:none; }
.ma-action-item { font-weight:500; color:var(--ink); }
.ma-action-owner{ display:inline-flex; align-items:center; gap:5px; background:rgba(124,58,237,.08); border:1px solid rgba(124,58,237,.18); border-radius:20px; padding:2px 10px; font-size:11px; color:var(--purple); font-weight:600; }
.ma-action-due { font-size:11px; color:var(--lt); }
.ma-priority { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; }
.ma-priority.high{ background:#DC2626; }
.ma-priority.med { background:#D97706; }
.ma-priority.low { background:#16A34A; }
/* ── Decisions list ── */
.ma-decision { display:flex; gap:12px; padding:11px 0; border-bottom:1px solid var(--bdr); }
.ma-decision:last-child { border-bottom:none; }
.ma-decision-num { width:22px; height:22px; border-radius:50%; background:linear-gradient(135deg,var(--purple),var(--pink)); color:white; font-size:10px; font-weight:800; display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:2px; }
.ma-decision-text { font-size:13px; color:var(--ink); line-height:1.6; }
/* ── Key topics ── */
.ma-topics { display:flex; flex-wrap:wrap; gap:8px; }
.ma-topic-chip { background:rgba(124,58,237,.07); border:1px solid rgba(124,58,237,.18); border-radius:20px; padding:5px 14px; font-size:12px; font-weight:600; color:var(--purple); }
/* ── Export bar ── */
.ma-export-bar { display:flex; gap:10px; flex-wrap:wrap; padding:16px 18px; background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; margin-bottom:20px; align-items:center; }
.ma-export-label { font-size:12px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin-right:4px; }
.ma-export-btn { display:inline-flex; align-items:center; gap:6px; padding:7px 16px; border-radius:20px; border:1px solid var(--bdr); background:var(--bg); font-family:inherit; font-size:12px; font-weight:600; color:var(--med); cursor:pointer; transition:.15s; }
.ma-export-btn:hover { border-color:var(--purple); color:var(--purple); background:rgba(124,58,237,.04); }
/* ── Empty state ── */
.ma-empty { text-align:center; padding:80px 20px; color:var(--lt); }
.ma-empty-icon { font-size:56px; margin-bottom:14px; }
.ma-empty-title { font-size:18px; font-weight:700; color:var(--ink); margin-bottom:8px; }
.ma-empty-sub { font-size:13px; max-width:380px; margin:0 auto; line-height:1.6; }
/* ── History ── */
.ma-hist-item { display:flex; align-items:center; gap:12px; padding:10px 12px; border-radius:8px; cursor:pointer; transition:.1s; border:1px solid transparent; margin-bottom:4px; }
.ma-hist-item:hover { background:rgba(124,58,237,.04); border-color:var(--bdr); }
.ma-hist-icon { font-size:20px; flex-shrink:0; }
.ma-hist-name { font-size:12px; font-weight:600; color:var(--ink); }
.ma-hist-date { font-size:11px; color:var(--lt); margin-top:2px; }
</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 &amp; 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 &amp; 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="ma-wrap">
<!-- ── Left panel ── -->
<aside class="ma-left">
<!-- Upload section -->
<div class="ma-left-header">
<div class="ma-left-title">🎙️ Audio / Transcript Input</div>
<!-- Drop zone -->
<div class="ma-drop-zone" id="drop-zone"
onclick="document.getElementById('audio-input').click()"
ondragover="onDragOver(event)" ondragleave="onDragLeave(event)" ondrop="onDrop(event)">
<div class="ma-drop-icon">🎧</div>
<div class="ma-drop-label">Drop audio file here</div>
<div class="ma-drop-sub">MP3, M4A, WAV, OGG, WEBM · up to 500MB</div>
</div>
<input type="file" id="audio-input" class="ma-drop-input" accept="audio/*,video/mp4,video/webm" onchange="onFileSelect(this)">
<div id="file-chip" style="display:none"></div>
<div class="ma-or">OR</div>
<!-- Paste transcript -->
<label style="font-size:11px;font-weight:700;color:var(--lt);text-transform:uppercase;letter-spacing:.4px;display:block;margin-bottom:5px">Paste Transcript</label>
<textarea class="ma-paste" id="paste-transcript" placeholder="Paste meeting transcript here — speaker labels like 'John: ...' will be detected automatically…" rows="6"></textarea>
</div>
<!-- Meeting metadata -->
<div class="ma-meta-section">
<div class="ma-meta-title">Meeting Details</div>
<div class="ma-field">
<label>Meeting Title</label>
<input type="text" id="meeting-title" placeholder="e.g. Q3 Budget Review">
</div>
<div class="ma-field">
<label>Date</label>
<input type="date" id="meeting-date">
</div>
<div class="ma-field">
<label>Type</label>
<select id="meeting-type">
<option value="general">General Meeting</option>
<option value="project">Project Review</option>
<option value="board">Board / Executive</option>
<option value="standup">Stand-up / Sync</option>
<option value="interview">Interview</option>
<option value="client">Client Call</option>
<option value="training">Training / Workshop</option>
</select>
</div>
<div class="ma-field">
<label>Attendees</label>
<div class="ma-att-chips" id="att-chips"></div>
<div class="ma-att-input">
<input type="text" id="att-input" placeholder="Name or email" onkeydown="if(event.key==='Enter')addAttendee()">
<button class="ma-att-add" onclick="addAttendee()"></button>
</div>
</div>
</div>
<!-- Options -->
<div class="ma-opts">
<div class="ma-meta-title">Processing Options</div>
<div class="ma-opt-row"><input type="checkbox" id="opt-transcript" checked><label class="ma-opt-label">Full transcript</label></div>
<div class="ma-opt-row"><input type="checkbox" id="opt-summary" checked><label class="ma-opt-label">Executive summary</label></div>
<div class="ma-opt-row"><input type="checkbox" id="opt-decisions" checked><label class="ma-opt-label">Decisions log</label></div>
<div class="ma-opt-row"><input type="checkbox" id="opt-actions" checked><label class="ma-opt-label">Action items with owners</label></div>
<div class="ma-opt-row"><input type="checkbox" id="opt-topics" checked><label class="ma-opt-label">Key topics / keywords</label></div>
<div class="ma-opt-row"><input type="checkbox" id="opt-sentiment"><label class="ma-opt-label">Sentiment analysis</label></div>
<div class="ma-opt-row"><input type="checkbox" id="opt-minutes" checked><label class="ma-opt-label">Generate formal minutes</label></div>
</div>
<!-- Run -->
<div class="ma-run-wrap">
<button class="ma-run-btn" id="run-btn" onclick="runMeeting()">
✨ Process Meeting
</button>
</div>
<!-- History (past meetings) -->
<div style="border-top:1px solid var(--bdr);padding:12px 18px;flex-shrink:0" id="history-section">
<div class="ma-meta-title" style="margin-bottom:8px">Recent Meetings</div>
<div id="history-list">
<div style="font-size:12px;color:var(--lt);text-align:center;padding:8px 0">No previous meetings</div>
</div>
</div>
</aside>
<!-- ── Right panel (output) ── -->
<main class="ma-right">
<!-- Progress -->
<div class="ma-progress" id="progress-card">
<div class="ma-progress-title">⚙️ Processing Meeting…</div>
<div class="ma-step-list" id="progress-steps"></div>
</div>
<!-- Empty state -->
<div class="ma-empty" id="empty-state">
<div class="ma-empty-icon">🎙️</div>
<div class="ma-empty-title">Meeting Assistant</div>
<div class="ma-empty-sub">Upload an audio recording or paste a transcript. The AI will extract a summary, decisions, action items, and generate formal minutes — ready to share.</div>
<div style="margin-top:24px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
<button class="btn-primary" onclick="loadDemo()">▶ Try Demo Transcript</button>
</div>
</div>
<!-- Output area (hidden until processed) -->
<div id="output-area" style="display:none">
<!-- Meeting header -->
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px;flex-wrap:wrap">
<div>
<div style="font-size:11px;font-weight:700;color:var(--lt);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">Meeting Minutes</div>
<div style="font-size:22px;font-weight:800;color:var(--ink)" id="out-title"></div>
<div style="font-size:13px;color:var(--lt);margin-top:4px" id="out-meta"></div>
</div>
<div class="ma-export-bar" style="margin:0;flex-shrink:0">
<span class="ma-export-label">Export</span>
<button class="ma-export-btn" onclick="exportPDF()">📄 PDF Minutes</button>
<button class="ma-export-btn" onclick="exportMarkdown()">📝 Markdown</button>
<button class="ma-export-btn" onclick="copyToClipboard()">📋 Copy</button>
</div>
</div>
<!-- Tabs -->
<div class="ma-tabs">
<button class="ma-tab active" onclick="showTab('summary')">📋 Summary</button>
<button class="ma-tab" onclick="showTab('actions')">✅ Action Items</button>
<button class="ma-tab" onclick="showTab('decisions')">🏛️ Decisions</button>
<button class="ma-tab" onclick="showTab('transcript')">📜 Transcript</button>
<button class="ma-tab" onclick="showTab('minutes')">📄 Full Minutes</button>
</div>
<!-- Summary tab -->
<div class="ma-tab-panel active" id="tab-summary">
<div class="ma-out-card">
<div class="ma-out-hdr"><span>📋</span><div class="ma-out-title">Executive Summary</div></div>
<div class="ma-out-body"><div class="ma-out-text" id="out-summary"></div></div>
</div>
<div class="ma-out-card">
<div class="ma-out-hdr"><span>🏷️</span><div class="ma-out-title">Key Topics</div></div>
<div class="ma-out-body"><div class="ma-topics" id="out-topics"></div></div>
</div>
<div class="ma-out-card" id="sentiment-card" style="display:none">
<div class="ma-out-hdr"><span>💬</span><div class="ma-out-title">Sentiment Overview</div></div>
<div class="ma-out-body" id="out-sentiment"></div>
</div>
</div>
<!-- Actions tab -->
<div class="ma-tab-panel" id="tab-actions">
<div class="ma-out-card">
<div class="ma-out-hdr">
<span></span>
<div class="ma-out-title">Action Items</div>
<span style="font-size:12px;color:var(--lt)" id="action-count"></span>
</div>
<div class="ma-out-body">
<table class="ma-action-table" id="out-actions">
<thead><tr>
<th style="width:46%">Action</th>
<th style="width:22%">Owner</th>
<th style="width:16%">Due</th>
<th style="width:16%">Priority</th>
</tr></thead>
<tbody id="actions-body"></tbody>
</table>
</div>
</div>
</div>
<!-- Decisions tab -->
<div class="ma-tab-panel" id="tab-decisions">
<div class="ma-out-card">
<div class="ma-out-hdr"><span>🏛️</span><div class="ma-out-title">Decisions Made</div></div>
<div class="ma-out-body"><div id="out-decisions"></div></div>
</div>
</div>
<!-- Transcript tab -->
<div class="ma-tab-panel" id="tab-transcript">
<div class="ma-out-card">
<div class="ma-out-hdr">
<span>📜</span>
<div class="ma-out-title">Full Transcript</div>
<span style="font-size:12px;color:var(--lt)" id="transcript-words"></span>
</div>
<div class="ma-out-body">
<div class="ma-transcript" id="out-transcript"></div>
</div>
</div>
</div>
<!-- Full Minutes tab -->
<div class="ma-tab-panel" id="tab-minutes">
<div class="ma-out-card">
<div class="ma-out-hdr"><span>📄</span><div class="ma-out-title">Formal Meeting Minutes</div></div>
<div class="ma-out-body">
<div class="ma-out-text" id="out-minutes" style="white-space:pre-wrap;font-family:Georgia,serif;line-height:2"></div>
</div>
</div>
</div>
</div><!-- /#output-area -->
</main>
</div>
<script>
const _API = '/api';
let attendees = [];
let currentResult = null;
// ── Init ───────────────────────────────────────────────────────────────────────
document.getElementById('meeting-date').value = new Date().toISOString().slice(0,10);
// ── File drag/drop ─────────────────────────────────────────────────────────────
function onDragOver(e) { e.preventDefault(); document.getElementById('drop-zone').classList.add('drag-over'); }
function onDragLeave() { document.getElementById('drop-zone').classList.remove('drag-over'); }
function onDrop(e) {
e.preventDefault();
onDragLeave();
const file = e.dataTransfer.files[0];
if (file) setFile(file);
}
function onFileSelect(input) {
if (input.files[0]) setFile(input.files[0]);
}
function setFile(file) {
const chip = document.getElementById('file-chip');
chip.style.display = 'flex';
chip.innerHTML = `
<div class="ma-file-chip">
<span class="ma-file-icon">🎵</span>
<span class="ma-file-name">${esc(file.name)}</span>
<span style="font-size:11px;color:var(--lt);white-space:nowrap">${(file.size/1024/1024).toFixed(1)} MB</span>
<button class="ma-file-remove" onclick="clearFile()" title="Remove">×</button>
</div>`;
window._selectedFile = file;
// Auto-fill title from filename
if (!document.getElementById('meeting-title').value) {
document.getElementById('meeting-title').value = file.name.replace(/\.[^.]+$/,'').replace(/[-_]/g,' ');
}
}
function clearFile() {
window._selectedFile = null;
document.getElementById('file-chip').style.display = 'none';
document.getElementById('audio-input').value = '';
}
// ── Attendees ──────────────────────────────────────────────────────────────────
function addAttendee() {
const v = document.getElementById('att-input').value.trim();
if (!v || attendees.includes(v)) return;
attendees.push(v);
renderAttendees();
document.getElementById('att-input').value = '';
}
function removeAttendee(i) {
attendees.splice(i,1);
renderAttendees();
}
function renderAttendees() {
document.getElementById('att-chips').innerHTML = attendees.map((a,i) => `
<div class="ma-att-chip">
${esc(a)}
<button onclick="removeAttendee(${i})">×</button>
</div>`).join('');
}
// ── Progress steps ─────────────────────────────────────────────────────────────
const STEPS = [
{ key:'upload', label:'Uploading audio file' },
{ key:'transcribe', label:'Transcribing with Whisper' },
{ key:'summary', label:'Generating executive summary' },
{ key:'decisions', label:'Extracting decisions' },
{ key:'actions', label:'Identifying action items with owners' },
{ key:'topics', label:'Extracting key topics' },
{ key:'minutes', label:'Writing formal minutes' },
];
function renderProgress(currentStep) {
const card = document.getElementById('progress-card');
card.classList.add('active');
document.getElementById('empty-state').style.display = 'none';
document.getElementById('output-area').style.display = 'none';
const activeIdx = STEPS.findIndex(s => s.key === currentStep);
document.getElementById('progress-steps').innerHTML = STEPS.map((s,i) => {
const cls = i < activeIdx ? 'done' : i === activeIdx ? 'running' : 'pending';
const dot = i < activeIdx ? '✓' : i === activeIdx ? '⚙' : (i+1);
return `<div class="ma-step-row">
<div class="ma-step-dot ${cls}">${dot}</div>
<div class="ma-step-label ${cls}">${s.label}${i === activeIdx ? '…' : ''}</div>
</div>`;
}).join('');
}
// ── Run ────────────────────────────────────────────────────────────────────────
async function runMeeting() {
const hasPaste = document.getElementById('paste-transcript').value.trim().length > 10;
const hasFile = !!window._selectedFile;
if (!hasPaste && !hasFile) { alert('Upload an audio file or paste a transcript first.'); return; }
const btn = document.getElementById('run-btn');
btn.disabled = true;
btn.textContent = '⚙ Processing…';
const opts = {
transcript: document.getElementById('opt-transcript').checked,
summary: document.getElementById('opt-summary').checked,
decisions: document.getElementById('opt-decisions').checked,
actions: document.getElementById('opt-actions').checked,
topics: document.getElementById('opt-topics').checked,
sentiment: document.getElementById('opt-sentiment').checked,
minutes: document.getElementById('opt-minutes').checked,
};
const meta = {
title: document.getElementById('meeting-title').value || 'Untitled Meeting',
date: document.getElementById('meeting-date').value,
type: document.getElementById('meeting-type').value,
attendees: [...attendees],
};
try {
if (hasFile) {
// Upload + transcribe via API
renderProgress('upload');
const formData = new FormData();
formData.append('file', window._selectedFile);
formData.append('meta', JSON.stringify({...meta, opts}));
renderProgress('transcribe');
const res = await fetch(`${_API}/meeting/process`, {
method:'POST', credentials:'include', body: formData
});
if (!res.ok) throw new Error((await res.json()).detail || 'Processing failed');
const data = await res.json();
renderProgress('minutes');
await new Promise(r=>setTimeout(r,400));
showResult(data, meta);
} else {
// Text-only — use LLM pipeline
await runTextPipeline(document.getElementById('paste-transcript').value, meta, opts);
}
} catch(e) {
// Fall back to demo if API unavailable
await runDemoPipeline(document.getElementById('paste-transcript').value || DEMO_TRANSCRIPT, meta, opts);
}
btn.disabled = false;
btn.textContent = '✨ Process Meeting';
}
async function runTextPipeline(text, meta, opts) {
const steps = [
{ key:'summary', label:'Generating executive summary' },
{ key:'decisions', label:'Extracting decisions' },
{ key:'actions', label:'Identifying action items' },
{ key:'topics', label:'Extracting key topics' },
{ key:'minutes', label:'Writing formal minutes' },
].filter(s => opts[s.key] !== false);
for (const s of steps) {
renderProgress(s.key);
await new Promise(r => setTimeout(r, 600 + Math.random()*400));
}
// Try real API
try {
const res = await fetch(`${_API}/meeting/analyse`, {
method:'POST', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ transcript: text, meta, opts })
});
if (res.ok) { showResult(await res.json(), meta); return; }
} catch(_) {}
// Fallback: generate locally using mock
await runDemoPipeline(text, meta, opts);
}
async function runDemoPipeline(text, meta, opts) {
const result = generateMockResult(text, meta);
renderProgress('minutes');
await new Promise(r=>setTimeout(r,500));
showResult(result, meta);
saveToHistory(meta, result);
}
// ── Show result ────────────────────────────────────────────────────────────────
function showResult(data, meta) {
currentResult = data;
document.getElementById('progress-card').classList.remove('active');
document.getElementById('empty-state').style.display = 'none';
document.getElementById('output-area').style.display = '';
document.getElementById('out-title').textContent = meta.title || 'Meeting Minutes';
document.getElementById('out-meta').textContent = [
meta.date ? new Date(meta.date).toLocaleDateString('en-GB',{day:'numeric',month:'long',year:'numeric'}) : '',
meta.attendees?.length ? meta.attendees.join(', ') : '',
].filter(Boolean).join(' · ');
// Summary
document.getElementById('out-summary').textContent = data.summary || '';
// Topics
document.getElementById('out-topics').innerHTML = (data.topics || []).map(t =>
`<div class="ma-topic-chip">${esc(t)}</div>`).join('');
// Sentiment
if (data.sentiment) {
document.getElementById('sentiment-card').style.display = '';
document.getElementById('out-sentiment').innerHTML = `
<div style="display:flex;gap:16px;flex-wrap:wrap">
${Object.entries(data.sentiment).map(([k,v])=>`
<div style="text-align:center;min-width:70px">
<div style="font-size:22px">${sentimentEmoji(k)}</div>
<div style="font-size:13px;font-weight:700;color:var(--ink)">${Math.round(v*100)}%</div>
<div style="font-size:11px;color:var(--lt);text-transform:capitalize">${k}</div>
</div>`).join('')}
</div>`;
}
// Action items
const actions = data.action_items || [];
document.getElementById('action-count').textContent = `${actions.length} item${actions.length!==1?'s':''}`;
document.getElementById('actions-body').innerHTML = actions.map(a => `<tr>
<td><div class="ma-action-item">${esc(a.action)}</div></td>
<td>${a.owner ? `<span class="ma-action-owner">👤 ${esc(a.owner)}</span>` : '<span style="color:var(--lt);font-size:12px">Unassigned</span>'}</td>
<td><div class="ma-action-due">${esc(a.due||'—')}</div></td>
<td><span class="ma-priority ${a.priority||'med'}"></span>${capitalize(a.priority||'medium')}</td>
</tr>`).join('');
// Decisions
document.getElementById('out-decisions').innerHTML = (data.decisions || []).map((d,i) =>
`<div class="ma-decision">
<div class="ma-decision-num">${i+1}</div>
<div class="ma-decision-text">${esc(d)}</div>
</div>`).join('') || '<div style="color:var(--lt);font-size:13px">No explicit decisions recorded</div>';
// Transcript
const tx = data.transcript || document.getElementById('paste-transcript').value || '';
const wordCount = tx.trim().split(/\s+/).length;
document.getElementById('transcript-words').textContent = `${wordCount.toLocaleString()} words`;
document.getElementById('out-transcript').innerHTML = formatTranscript(tx);
// Minutes
document.getElementById('out-minutes').textContent = data.minutes || generateMinutesText(data, meta);
showTab('summary');
}
// ── Demo ───────────────────────────────────────────────────────────────────────
const DEMO_TRANSCRIPT = `Priya: Good morning everyone. Let's get started with the Q3 budget review. Can someone confirm we have quorum?
Rajan: Yes, we have Priya, myself, Ananya, and Suresh on the call.
Priya: Perfect. So the main agenda today is reviewing the Q3 spend against forecast, deciding on the server upgrade budget, and looking at the training plan for the new AI tools.
Ananya: Before we start — just to flag, IT submitted a late request for additional storage capacity. Should we add that to the agenda?
Priya: Yes, let's take that after the server discussion. Suresh, can you walk us through Q3 actuals?
Suresh: Sure. Overall we came in at 94% of forecast, which is good news. The main variance was in infrastructure — we underspent by about 8% because the server upgrade was deferred. On the people side, we're exactly on track.
Rajan: The 8% infrastructure underspend — can that roll over to Q4?
Suresh: Yes, finance confirmed that. About ₹12 lakhs can carry over.
Priya: Good. Now for the server upgrade — Ananya, what are we recommending?
Ananya: We're recommending Option B: three new GPU nodes with 2TB SSD each, which comes to ₹38 lakhs including setup. Option A was cheaper at ₹24 lakhs but only one GPU node, which won't support the new AI workloads we're planning.
Rajan: I agree Option B makes more sense given where we're heading. The AI suite alone needs at least two nodes running in parallel for reliability.
Priya: Agreed. We'll go with Option B. Ananya, can you get the purchase order raised by end of month?
Ananya: Will do. I'll also loop in Suresh for the finance approvals.
Suresh: I'll prioritise the approval. Should be done within a week.
Priya: Now the storage request from IT — Ananya, what did they ask for?
Ananya: An extra 10TB NAS for the document archive. They said current storage hits 85% capacity by December.
Priya: That needs to be addressed before December. Let's add 10TB to the Q4 plan. Suresh, can you find budget for that?
Suresh: I'll check, but given the Q3 carryover we should have room. I'll confirm by Thursday.
Priya: Great. Now, training. Rajan, where are we on the AI tools training for the team?
Rajan: We have 28 staff who need onboarding on the new Nexus One AI suite. I'd like to run two workshops in October — one for the core users and one for the admin team. I need budget approval for an external trainer, approximately ₹80,000.
Priya: That's reasonable. Approved. Can you share the workshop schedule with everyone by next week?
Rajan: Yes, I'll have it out by Friday.
Priya: Good. Any other business?
Ananya: Just a reminder that the vendor review for the current cloud contracts is due in November. We should start that conversation soon given we're moving more to local AI.
Priya: Noted. Let's schedule a dedicated session for that in the first week of November. Rajan, can you set that up?
Rajan: Yes, I'll send out a calendar invite this week.
Priya: Perfect. Thank you all. Let me recap the key decisions — we're going with Option B for the server upgrade at ₹38 lakhs, adding 10TB NAS storage to Q4, approving ₹80,000 for AI training workshops, and scheduling a vendor review for November. Everyone aligned?
All: Yes.
Priya: Great. Thanks everyone. We'll reconvene in four weeks.`;
function loadDemo() {
document.getElementById('paste-transcript').value = DEMO_TRANSCRIPT;
document.getElementById('meeting-title').value = 'Q3 Budget Review';
attendees = ['Priya', 'Rajan', 'Ananya', 'Suresh'];
renderAttendees();
document.getElementById('meeting-type').value = 'project';
}
function generateMockResult(text, meta) {
const isDemo = text.includes('Q3 Budget Review') || text.includes('Priya');
return {
transcript: text,
summary: isDemo
? 'The Q3 budget review confirmed overall spend at 94% of forecast, with an ₹12 lakh infrastructure underspend carrying over to Q4. The team approved the Option B server upgrade (₹38 lakhs, 3 GPU nodes), added 10TB NAS storage to the Q4 plan, approved ₹80,000 for AI tools training workshops, and scheduled a cloud vendor review for November.'
: 'Meeting covered key agenda items. Participants discussed current status, upcoming priorities, and assigned follow-up actions. Several decisions were made regarding resource allocation and timelines.',
topics: isDemo
? ['Q3 Budget', 'Server Upgrade', 'GPU Infrastructure', 'AI Training', 'Storage Capacity', 'Vendor Review', 'Q4 Planning']
: ['Budget Review', 'Project Status', 'Team Updates', 'Next Steps'],
decisions: isDemo
? [
'Approved Option B server upgrade — 3 GPU nodes + 2TB SSD each — at ₹38 lakhs including setup.',
'Added 10TB NAS storage to Q4 budget plan to address IT capacity requirement.',
'Approved ₹80,000 budget for two AI tools training workshops in October.',
'Scheduled cloud vendor contract review for first week of November.',
'Confirmed ₹12 lakh Q3 infrastructure carryover to Q4.',
]
: ['Decision on primary agenda item approved by all attendees.', 'Follow-up review scheduled for next month.'],
action_items: isDemo
? [
{ action:'Raise purchase order for Option B server upgrade (₹38L)', owner:'Ananya', due:'End of October', priority:'high' },
{ action:'Process finance approval for server upgrade PO', owner:'Suresh', due:'Within 1 week', priority:'high' },
{ action:'Confirm Q4 budget allocation for 10TB NAS storage', owner:'Suresh', due:'Thursday', priority:'med' },
{ action:'Share AI training workshop schedule with all staff', owner:'Rajan', due:'Friday', priority:'med' },
{ action:'Send calendar invite for November cloud vendor review', owner:'Rajan', due:'This week', priority:'med' },
{ action:'Initiate cloud vendor contract review discussions', owner:'Ananya', due:'November Week 1', priority:'low' },
]
: [
{ action:'Follow up on key discussion point', owner:'Team Lead', due:'Next week', priority:'high' },
{ action:'Share updated document with team', owner:'Secretary', due:'Tomorrow', priority:'med' },
],
sentiment: { positive: 0.68, neutral: 0.26, concern: 0.06 },
minutes: null, // generated below
};
}
function generateMinutesText(data, meta) {
const date = meta.date
? new Date(meta.date).toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'})
: new Date().toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'});
const atts = meta.attendees?.length ? meta.attendees.join(', ') : 'As per attendance register';
const decisionsText = (data.decisions||[]).map((d,i)=>` ${i+1}. ${d}`).join('\n') || ' (None recorded)';
const actionsText = (data.action_items||[]).map(a=>`${a.action} — Owner: ${a.owner||'TBD'}, Due: ${a.due||'TBD'}`).join('\n') || ' (None recorded)';
return `MINUTES OF MEETING
${'═'.repeat(60)}
Meeting Title : ${meta.title || 'Meeting'}
Date : ${date}
Meeting Type : ${capitalize(meta.type||'General')}
Attendees : ${atts}
Recorded by : Nexus One AI Meeting Assistant
${'─'.repeat(60)}
1. EXECUTIVE SUMMARY
${data.summary || ''}
${'─'.repeat(60)}
2. KEY TOPICS DISCUSSED
${(data.topics||[]).map(t=>`${t}`).join('\n') || ' (None recorded)'}
${'─'.repeat(60)}
3. DECISIONS MADE
${decisionsText}
${'─'.repeat(60)}
4. ACTION ITEMS
${actionsText}
${'─'.repeat(60)}
5. NEXT STEPS
Follow up on all action items as per assigned owners and deadlines listed above.
${'═'.repeat(60)}
These minutes were generated by Nexus One AI and should be reviewed for accuracy before distribution.
Generated: ${new Date().toLocaleString()}`;
}
// ── Tabs ───────────────────────────────────────────────────────────────────────
function showTab(name) {
document.querySelectorAll('.ma-tab').forEach((t,i) => {
const panels = ['summary','actions','decisions','transcript','minutes'];
t.classList.toggle('active', panels[i] === name);
});
document.querySelectorAll('.ma-tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById(`tab-${name}`)?.classList.add('active');
}
// ── Export ─────────────────────────────────────────────────────────────────────
function exportMarkdown() {
if (!currentResult) return;
const meta = {
title: document.getElementById('out-title').textContent,
meta: document.getElementById('out-meta').textContent,
};
const md = `# ${meta.title}\n_${meta.meta}_\n\n## Summary\n${currentResult.summary}\n\n## Decisions\n${(currentResult.decisions||[]).map((d,i)=>`${i+1}. ${d}`).join('\n')}\n\n## Action Items\n${(currentResult.action_items||[]).map(a=>`- **${a.action}** — ${a.owner||'TBD'} · ${a.due||'TBD'}`).join('\n')}\n\n## Topics\n${(currentResult.topics||[]).join(', ')}`;
const blob = new Blob([md], {type:'text/markdown'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${meta.title.replace(/\s+/g,'-')}-minutes.md`;
a.click();
}
function exportPDF() {
const content = document.getElementById('out-minutes').textContent;
if (!content) return;
// Open print dialog with minutes content
const w = window.open('', '_blank');
w.document.write(`<!DOCTYPE html><html><head><title>Meeting Minutes</title>
<style>body{font-family:Georgia,serif;max-width:800px;margin:40px auto;line-height:2;color:#111;font-size:14px}
pre{white-space:pre-wrap;font-family:Georgia,serif;font-size:14px}
@media print{body{margin:20mm}}</style></head>
<body><pre>${esc(content)}</pre></body></html>`);
w.document.close();
w.print();
}
function copyToClipboard() {
const txt = document.getElementById('out-minutes').textContent;
navigator.clipboard.writeText(txt).then(() => showToast('Copied to clipboard'));
}
// ── History ────────────────────────────────────────────────────────────────────
function saveToHistory(meta, result) {
const hist = JSON.parse(localStorage.getItem('cezen_meetings')||'[]');
hist.unshift({ title:meta.title, date:meta.date, ts:new Date().toISOString(), result, meta });
localStorage.setItem('cezen_meetings', JSON.stringify(hist.slice(0,10)));
renderHistory();
}
function renderHistory() {
const hist = JSON.parse(localStorage.getItem('cezen_meetings')||'[]');
const el = document.getElementById('history-list');
if (!hist.length) { el.innerHTML = '<div style="font-size:12px;color:var(--lt);text-align:center;padding:8px 0">No previous meetings</div>'; return; }
el.innerHTML = hist.map((h,i) => `
<div class="ma-hist-item" onclick="loadHistory(${i})">
<div class="ma-hist-icon">📋</div>
<div>
<div class="ma-hist-name">${esc(h.title||'Meeting')}</div>
<div class="ma-hist-date">${h.date ? new Date(h.date).toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'}) : 'Unknown date'}</div>
</div>
</div>`).join('');
}
function loadHistory(i) {
const hist = JSON.parse(localStorage.getItem('cezen_meetings')||'[]');
if (!hist[i]) return;
const h = hist[i];
showResult(h.result, h.meta);
}
// ── Helpers ────────────────────────────────────────────────────────────────────
function formatTranscript(text) {
return text.split('\n').map(line => {
const m = line.match(/^([A-Z][a-zA-Z\s]+):\s(.+)/);
if (m) return `<span class="speaker">${esc(m[1])}:</span> ${esc(m[2])}`;
return esc(line);
}).join('\n');
}
function sentimentEmoji(k) {
return {positive:'😊',neutral:'😐',concern:'😟',negative:'😔',mixed:'🤔'}[k]||'💬';
}
function capitalize(s) { return s ? s[0].toUpperCase()+s.slice(1) : ''; }
function showToast(msg) {
const t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'position:fixed;bottom:24px;right:24px;background:#F0FDFA;color:#0F766E;border:1px solid #99F6E4;border-radius:10px;padding:11px 18px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,.1)';
document.body.appendChild(t);
setTimeout(()=>t.remove(),2500);
}
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Boot ───────────────────────────────────────────────────────────────────────
renderHistory();
</script>
<script src="auth.js"></script>
<script src="branding.js"></script>
</body>
</html>