Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-22 18:21:58 -07:00
commit b4fa61d99a
1 changed files with 18 additions and 11 deletions

View File

@ -18,11 +18,17 @@ const routes = new Hono();
// ── Meeting Intelligence API helper ── // ── Meeting Intelligence API helper ──
async function miApiFetch(path: string): Promise<{ ok: boolean; data?: any; error?: string }> { async function miApiFetch(path: string, options?: { method?: string; body?: any }): Promise<{ ok: boolean; data?: any; error?: string }> {
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000); const timeout = setTimeout(() => controller.abort(), 8000);
const res = await fetch(`${MI_API_URL}${path}`, { signal: controller.signal }); const fetchOpts: RequestInit = { signal: controller.signal };
if (options?.method) fetchOpts.method = options.method;
if (options?.body) {
fetchOpts.headers = { "Content-Type": "application/json" };
fetchOpts.body = JSON.stringify(options.body);
}
const res = await fetch(`${MI_API_URL}${path}`, fetchOpts);
clearTimeout(timeout); clearTimeout(timeout);
if (!res.ok) return { ok: false, error: `API returned ${res.status}` }; if (!res.ok) return { ok: false, error: `API returned ${res.status}` };
return { ok: true, data: await res.json() }; return { ok: true, data: await res.json() };
@ -58,6 +64,7 @@ const MI_STYLES = `<style>
.mi-badge--processing{background:#eab30822;color:#eab308} .mi-badge--processing{background:#eab30822;color:#eab308}
.mi-badge--recording{background:#ef444422;color:#ef4444} .mi-badge--recording{background:#ef444422;color:#ef4444}
.mi-badge--failed{background:#ef444422;color:#ef4444} .mi-badge--failed{background:#ef444422;color:#ef4444}
.mi-badge--transcribing,.mi-badge--diarizing,.mi-badge--summarizing,.mi-badge--extracting_audio{background:#3b82f622;color:#3b82f6}
.mi-tabs{display:flex;gap:0;border-bottom:1px solid var(--rs-border);margin-bottom:1.5rem} .mi-tabs{display:flex;gap:0;border-bottom:1px solid var(--rs-border);margin-bottom:1.5rem}
.mi-tab{padding:.75rem 1.25rem;text-decoration:none;color:var(--rs-text-secondary);font-size:.9rem;font-weight:500;border-bottom:2px solid transparent;transition:color .15s,border-color .15s} .mi-tab{padding:.75rem 1.25rem;text-decoration:none;color:var(--rs-text-secondary);font-size:.9rem;font-weight:500;border-bottom:2px solid transparent;transition:color .15s,border-color .15s}
.mi-tab:hover{color:var(--rs-text-primary)} .mi-tab:hover{color:var(--rs-text-primary)}
@ -160,7 +167,7 @@ routes.get("/recordings", async (c) => {
const participants = m.participant_count ?? m.participants?.length ?? ""; const participants = m.participant_count ?? m.participants?.length ?? "";
const status = (m.status || "completed").toLowerCase(); const status = (m.status || "completed").toLowerCase();
const badgeClass = `mi-badge--${status}`; const badgeClass = `mi-badge--${status}`;
const hasTranscript = m.has_transcript ?? m.transcript_status === "completed"; const hasTranscript = m.has_transcript ?? (m.segment_count != null && m.segment_count > 0) ?? m.transcript_status === "completed";
const hasSummary = m.has_summary ?? m.summary_status === "completed"; const hasSummary = m.has_summary ?? m.summary_status === "completed";
return `<a class="mi-card" href="${base}/recordings/${escapeHtml(String(m.id))}"> return `<a class="mi-card" href="${base}/recordings/${escapeHtml(String(m.id))}">
<h3>${escapeHtml(m.title || m.room_name || `Meeting ${m.id}`)}</h3> <h3>${escapeHtml(m.title || m.room_name || `Meeting ${m.id}`)}</h3>
@ -241,8 +248,8 @@ routes.get("/recordings/:id/:tab", async (c) => {
const speakers = speakersRes.ok ? (Array.isArray(speakersRes.data) ? speakersRes.data : speakersRes.data?.speakers ?? []) : []; const speakers = speakersRes.ok ? (Array.isArray(speakersRes.data) ? speakersRes.data : speakersRes.data?.speakers ?? []) : [];
const speakerList = speakers.length > 0 const speakerList = speakers.length > 0
? `<div class="mi-speakers">${speakers.map((s: any) => { ? `<div class="mi-speakers">${speakers.map((s: any) => {
const time = s.speaking_time_seconds ? `${Math.round(s.speaking_time_seconds / 60)}min speaking` : ""; const time = s.speaking_time ? `${Math.round(s.speaking_time / 60)}min speaking` : s.speaking_time_seconds ? `${Math.round(s.speaking_time_seconds / 60)}min speaking` : "";
return `<div class="mi-speaker-card"><h4>${escapeHtml(s.name || s.label || `Speaker ${s.id}`)}</h4><p>${escapeHtml(time)}</p></div>`; return `<div class="mi-speaker-card"><h4>${escapeHtml(s.name || s.speaker_label || s.label || `Speaker ${s.speaker_id || s.id}`)}</h4><p>${escapeHtml(time)}</p></div>`;
}).join("")}</div>` }).join("")}</div>`
: `<p style="color:var(--rs-text-secondary)">No speaker data available.</p>`; : `<p style="color:var(--rs-text-secondary)">No speaker data available.</p>`;
tabContent = `<div class="mi-detail-meta"> tabContent = `<div class="mi-detail-meta">
@ -261,7 +268,7 @@ ${speakerList}`;
tabContent = `<p style="color:var(--rs-text-secondary)">Transcript is empty.</p>`; tabContent = `<p style="color:var(--rs-text-secondary)">Transcript is empty.</p>`;
} else { } else {
tabContent = `<div class="mi-transcript">${segments.map((seg: any) => { tabContent = `<div class="mi-transcript">${segments.map((seg: any) => {
const speaker = seg.speaker || seg.speaker_name || seg.label || "Unknown"; const speaker = seg.speaker || seg.speaker_name || seg.speaker_label || seg.label || "Unknown";
const ts = seg.start_time != null ? formatTimestamp(seg.start_time) : ""; const ts = seg.start_time != null ? formatTimestamp(seg.start_time) : "";
const text = seg.text || seg.content || ""; const text = seg.text || seg.content || "";
return `<div class="mi-segment"> return `<div class="mi-segment">
@ -276,7 +283,7 @@ ${speakerList}`;
tabContent = `<p style="color:var(--rs-text-secondary)">Summary not available yet.</p>`; tabContent = `<p style="color:var(--rs-text-secondary)">Summary not available yet.</p>`;
} else { } else {
const summary = summaryRes.data; const summary = summaryRes.data;
const overview = summary.overview || summary.summary || summary.text || ""; const overview = summary.summary_text || summary.overview || summary.summary || summary.text || "";
const keyPoints = summary.key_points ?? summary.keyPoints ?? []; const keyPoints = summary.key_points ?? summary.keyPoints ?? [];
const actionItems = summary.action_items ?? summary.actionItems ?? []; const actionItems = summary.action_items ?? summary.actionItems ?? [];
const decisions = summary.decisions ?? []; const decisions = summary.decisions ?? [];
@ -284,7 +291,7 @@ ${speakerList}`;
let html = `<div class="mi-summary">`; let html = `<div class="mi-summary">`;
if (overview) html += `<h2>Overview</h2><p>${escapeHtml(overview)}</p>`; if (overview) html += `<h2>Overview</h2><p>${escapeHtml(overview)}</p>`;
if (keyPoints.length > 0) html += `<h2>Key Points</h2><ul>${keyPoints.map((p: string) => `<li>${escapeHtml(p)}</li>`).join("")}</ul>`; if (keyPoints.length > 0) html += `<h2>Key Points</h2><ul>${keyPoints.map((p: string) => `<li>${escapeHtml(p)}</li>`).join("")}</ul>`;
if (actionItems.length > 0) html += `<h2>Action Items</h2><ul>${actionItems.map((a: any) => `<li>${escapeHtml(typeof a === "string" ? a : a.text || a.description || "")}</li>`).join("")}</ul>`; if (actionItems.length > 0) html += `<h2>Action Items</h2><ul>${actionItems.map((a: any) => `<li>${escapeHtml(typeof a === "string" ? a : a.task || a.text || a.description || "")}</li>`).join("")}</ul>`;
if (decisions.length > 0) html += `<h2>Decisions</h2><ul>${decisions.map((d: any) => `<li>${escapeHtml(typeof d === "string" ? d : d.text || d.description || "")}</li>`).join("")}</ul>`; if (decisions.length > 0) html += `<h2>Decisions</h2><ul>${decisions.map((d: any) => `<li>${escapeHtml(typeof d === "string" ? d : d.text || d.description || "")}</li>`).join("")}</ul>`;
if (!overview && keyPoints.length === 0 && actionItems.length === 0) html += `<p style="color:var(--rs-text-secondary)">Summary content is empty.</p>`; if (!overview && keyPoints.length === 0 && actionItems.length === 0) html += `<p style="color:var(--rs-text-secondary)">Summary content is empty.</p>`;
html += `</div>`; html += `</div>`;
@ -296,9 +303,9 @@ ${speakerList}`;
tabContent = `<p style="color:var(--rs-text-secondary)">No speaker data available.</p>`; tabContent = `<p style="color:var(--rs-text-secondary)">No speaker data available.</p>`;
} else { } else {
tabContent = `<div class="mi-speakers">${speakers.map((s: any) => { tabContent = `<div class="mi-speakers">${speakers.map((s: any) => {
const time = s.speaking_time_seconds ? `${Math.round(s.speaking_time_seconds / 60)}min speaking` : ""; const time = s.speaking_time ? `${Math.round(s.speaking_time / 60)}min speaking` : s.speaking_time_seconds ? `${Math.round(s.speaking_time_seconds / 60)}min speaking` : "";
const segments = s.segment_count ?? s.num_segments ?? ""; const segments = s.segment_count ?? s.num_segments ?? "";
return `<div class="mi-speaker-card"><h4>${escapeHtml(s.name || s.label || `Speaker ${s.id}`)}</h4><p>${[time, segments ? `${segments} segments` : ""].filter(Boolean).join(" &middot; ")}</p></div>`; return `<div class="mi-speaker-card"><h4>${escapeHtml(s.name || s.speaker_label || s.label || `Speaker ${s.speaker_id || s.id}`)}</h4><p>${[time, segments ? `${segments} segments` : ""].filter(Boolean).join(" &middot; ")}</p></div>`;
}).join("")}</div>`; }).join("")}</div>`;
} }
} else { } else {
@ -328,7 +335,7 @@ routes.get("/search", async (c) => {
let resultsHtml = ""; let resultsHtml = "";
if (q) { if (q) {
const result = await miApiFetch(`/search?q=${encodeURIComponent(q)}&limit=30`); const result = await miApiFetch("/search", { method: "POST", body: { query: q, limit: 30 } });
if (!result.ok) { if (!result.ok) {
resultsHtml = miUnavailableHtml(); resultsHtml = miUnavailableHtml();
} else { } else {