Brings the ISO-packaged portal copy current with the live cezen-portal/ working directory, which had drifted across the entire V1.0 GA polish initiative (Phases 1-10) plus this session's topnav icon-order bugfix. - 52 existing files updated, 2 new pages added (about.html, health.html) - cgit/cezen-portal/ is now byte-identical to cezen-portal/ - Backend files (rag_ingest.py, cezen_license.py) already in sync; main.py intentionally left as-is (cgit is ahead there with workstation-category commercial-fit logic not yet backported to the live working copy) Next: rebuild the ISO (bash autoinstall/build-iso.sh) to bake this in.
959 lines
54 KiB
HTML
959 lines
54 KiB
HTML
<!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=12">
|
|
<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 { width:32px; height:32px; margin:0 auto 8px; color:var(--purple); }
|
|
.ma-drop-icon svg { width:100%; height:100%; }
|
|
.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 { width:20px; height:20px; color:var(--purple); }
|
|
.ma-file-icon svg { width:100%; height:100%; }
|
|
.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; width:16px; height:16px; padding:0; }
|
|
.ma-file-remove svg { width:100%; height:100%; }
|
|
.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 { width:56px; height:56px; margin:0 auto 14px; color:var(--purple); }
|
|
.ma-empty-icon svg { width:100%; height:100%; }
|
|
.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 { width:20px; height:20px; flex-shrink:0; color:var(--purple); }
|
|
.ma-hist-icon svg { width:100%; height:100%; }
|
|
.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 & 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">Governance & Audit</a>
|
|
<a href="health.html">Health Center</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 Catalog</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">MCP & Enterprise 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">AI Workspace</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 Studio</a>
|
|
<a href="schedules.html">Scheduled Jobs</a>
|
|
<a href="workflows.html">Workflow Designer</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" class="nav-icon-btn" style="position:relative" aria-label="Notifications">
|
|
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></span>
|
|
</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"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><path d="M12 17v4"/><path d="M8 21h8"/></svg></span><span>Audio / Transcript Input</span></span></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 12a8 8 0 0 1 16 0"/><rect x="3" y="12" width="4" height="7" rx="2"/><rect x="17" y="12" width="4" height="7" rx="2"/></svg></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()"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span></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()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m8 5 11 7-11 7z"/></svg></span><span>Process Meeting</span></span></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"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01A1.65 1.65 0 0 0 10.5 3.1V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span><span>Processing Meeting...</span></span></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><path d="M12 17v4"/><path d="M8 21h8"/></svg></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()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m8 5 11 7-11 7z"/></svg></span><span>Try Demo Transcript</span></span></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()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg></span><span>PDF Minutes</span></span></button>
|
|
<button class="ma-export-btn" onclick="exportMarkdown()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><path d="M8 7h8M8 11h8M8 15h5"/></svg></span><span>Markdown</span></span></button>
|
|
<button class="ma-export-btn" onclick="copyToClipboard()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg></span><span>Copy</span></span></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 class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 3h6"/><path d="M10 7h4"/><rect x="5" y="5" width="14" height="16" rx="2"/></svg></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 class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M20 12 12 20l-8-8 8-8 8 8Z"/><circle cx="12" cy="12" r="2.5"/></svg></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 class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M21 12a8.5 8.5 0 0 1-8.5 8.5H7l-4 2 1.5-4.5A8.5 8.5 0 1 1 21 12Z"/></svg></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 class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m5 12 5 5L20 7"/></svg></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 class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 10h18"/><path d="M5 10v8M10 10v8M14 10v8M19 10v8"/><path d="M2 18h20"/><path d="m12 3 9 5H3l9-5Z"/></svg></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 class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M8 4h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H8a3 3 0 0 1 0-6h7"/><path d="M8 14a3 3 0 0 0 0 6"/></svg></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 class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h6"/></svg></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;
|
|
function maIconSvg(name, sizeClass = 'icon-sm') {
|
|
const icons = {
|
|
music:'<svg viewBox="0 0 24 24"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>',
|
|
close:'<svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg>',
|
|
user:'<svg viewBox="0 0 24 24"><path d="M20 21a8 8 0 0 0-16 0"/><circle cx="12" cy="8" r="4"/></svg>',
|
|
positive:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8.5 14a4 4 0 0 0 7 0"/><path d="M9 10h.01M15 10h.01"/></svg>',
|
|
neutral:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M9 15h6"/><path d="M9 10h.01M15 10h.01"/></svg>',
|
|
concern:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8.5 15.5a4 4 0 0 1 7 0"/><path d="M9 10h.01M15 10h.01"/></svg>',
|
|
negative:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8.5 16a4 4 0 0 1 7 0"/><path d="M9 10h.01M15 10h.01"/></svg>',
|
|
mixed:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 15c1-.8 2-.8 3 0s2 .8 3 0 2-.8 3 0"/><path d="M9 10h.01M15 10h.01"/></svg>',
|
|
message:'<svg viewBox="0 0 24 24"><path d="M21 12a8.5 8.5 0 0 1-8.5 8.5H7l-4 2 1.5-4.5A8.5 8.5 0 1 1 21 12Z"/></svg>'
|
|
};
|
|
return `<span class="icon-svg ${sizeClass}" aria-hidden="true">${icons[name] || icons.music}</span>`;
|
|
}
|
|
|
|
// ── 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">${maIconSvg('music')}</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">${maIconSvg('close')}</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})" aria-label="Remove attendee">×</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 ? 'Done' : 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) { showToast('Upload an audio file or paste a transcript first.', 'warn'); return; }
|
|
|
|
const btn = document.getElementById('run-btn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = `<span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01A1.65 1.65 0 0 0 10.5 3.1V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span><span>Processing...</span></span>`;
|
|
|
|
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.innerHTML = `<span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m8 5 11 7-11 7z"/></svg></span><span>Process Meeting</span></span>`;
|
|
}
|
|
|
|
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">${maIconSvg(sentimentIcon(k), 'icon-lg')}</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">${maIconSvg('user')} ${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"><span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 3h6"/><path d="M10 7h4"/><rect x="5" y="5" width="14" height="16" rx="2"/></svg></span></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 sentimentIcon(k) {
|
|
return {positive:'positive',neutral:'neutral',concern:'concern',negative:'negative',mixed:'mixed'}[k] || 'message';
|
|
}
|
|
|
|
function capitalize(s) { return s ? s[0].toUpperCase()+s.slice(1) : ''; }
|
|
|
|
function esc(s) {
|
|
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// ── Boot ───────────────────────────────────────────────────────────────────────
|
|
renderHistory();
|
|
</script>
|
|
|
|
<script src="auth.js"></script>
|
|
<script src="branding.js?v=3"></script>
|
|
</body>
|
|
</html>
|