Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m36s Details

This commit is contained in:
Jeff Emmett 2026-04-10 18:37:57 -04:00
commit 71482f0e2a
1 changed files with 129 additions and 3 deletions

View File

@ -182,7 +182,8 @@ routes.get("/meet", (c) => {
routes.get("/recordings", async (c) => { routes.get("/recordings", async (c) => {
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const base = `/${escapeHtml(space)}/rmeets`; const base = `/${escapeHtml(space)}/rmeets`;
const result = await miApiFetch("/meetings?limit=50&sort=-created_at"); const prefix = encodeURIComponent(space + "_");
const result = await miApiFetch(`/meetings?limit=50&sort=-created_at&conference_prefix=${prefix}`);
let body: string; let body: string;
if (!result.ok) { if (!result.ok) {
@ -490,6 +491,13 @@ routes.get("/", (c) => {
<p>Open the full Jitsi Meet interface directly</p> <p>Open the full Jitsi Meet interface directly</p>
</div> </div>
</a> </a>
<a href="${base}/meeting-intelligence">
<span class="nav-icon">&#129504;</span>
<div class="nav-body">
<h3>Meeting Intelligence</h3>
<p>Knowledge objects: action items, decisions, and insights across all meetings</p>
</div>
</a>
<a href="${base}/recordings"> <a href="${base}/recordings">
<span class="nav-icon">&#127909;</span> <span class="nav-icon">&#127909;</span>
<div class="nav-body"> <div class="nav-body">
@ -509,6 +517,123 @@ routes.get("/", (c) => {
})); }));
}); });
// ── Meeting Intelligence knowledge page ──
routes.get("/meeting-intelligence", async (c) => {
const space = c.req.param("space") || "demo";
const base = `/${escapeHtml(space)}/rmeets`;
const prefix = encodeURIComponent(space + "_");
// Fetch space-scoped meetings from MI API
const result = await miApiFetch(`/meetings?limit=50&sort=-created_at&conference_prefix=${prefix}`);
let meetingCards = "";
let aggregateHtml = "";
if (!result.ok) {
meetingCards = miUnavailableHtml();
} else {
const meetings = Array.isArray(result.data) ? result.data : (result.data?.meetings ?? result.data?.items ?? []);
if (meetings.length === 0) {
meetingCards = `<p style="color:var(--rs-text-secondary)">No meetings recorded yet for this space. Start a meeting and enable recording to see it here.</p>`;
} else {
// Build meeting cards
meetingCards = `<div class="mi-cards">${meetings.map((m: any) => {
const date = m.created_at ? new Date(m.created_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "";
const duration = m.duration_seconds ? `${Math.round(m.duration_seconds / 60)}min` : "";
const status = (m.status || "completed").toLowerCase();
const badgeClass = `mi-badge--${status}`;
const hasTranscript = m.has_transcript ?? (m.segment_count != null && m.segment_count > 0);
const hasSummary = m.has_summary ?? m.summary_status === "completed";
return `<a class="mi-card" href="${base}/recordings/${escapeHtml(String(m.id))}">
<h3>${escapeHtml(m.title || m.room_name || m.conference_name || `Meeting ${m.id}`)}</h3>
<div class="mi-meta">${[date, duration].filter(Boolean).join(" &middot; ")}</div>
<span class="mi-badge ${badgeClass}">${escapeHtml(status)}</span>
<div class="mi-chips">
${hasTranscript ? `<span class="mi-chip mi-chip--ok">Transcript</span>` : `<span class="mi-chip mi-chip--pending">No transcript</span>`}
${hasSummary ? `<span class="mi-chip mi-chip--ok">Summary</span>` : `<span class="mi-chip mi-chip--pending">No summary</span>`}
</div>
</a>`;
}).join("\n")}</div>`;
// Aggregate knowledge from completed meetings with summaries
const readyMeetings = meetings.filter((m: any) => (m.status === "completed" || m.has_summary));
if (readyMeetings.length > 0) {
// Fetch summaries for up to 10 completed meetings
const summaryPromises = readyMeetings.slice(0, 10).map((m: any) =>
miApiFetch(`/meetings/${m.id}/summary`).then(r => r.ok ? r.data : null)
);
const summaries = (await Promise.all(summaryPromises)).filter(Boolean);
const allActionItems: string[] = [];
const allDecisions: string[] = [];
const allTopics: string[] = [];
for (const s of summaries) {
const actions = s.action_items ?? s.actionItems ?? [];
for (const a of actions) {
const text = typeof a === "string" ? a : (a.task || a.text || a.description || "");
if (text) allActionItems.push(text);
}
const decisions = s.decisions ?? [];
for (const d of decisions) {
const text = typeof d === "string" ? d : (d.text || d.description || "");
if (text) allDecisions.push(text);
}
const topics = s.topics ?? [];
for (const t of topics) {
const text = typeof t === "string" ? t : (t.name || t.topic || t.text || "");
if (text) allTopics.push(text);
}
}
if (allActionItems.length > 0 || allDecisions.length > 0 || allTopics.length > 0) {
aggregateHtml = `<div class="mi-aggregate">`;
if (allActionItems.length > 0) {
aggregateHtml += `<h2>Action Items</h2><ul>${allActionItems.map(a => `<li>${escapeHtml(a)}</li>`).join("")}</ul>`;
}
if (allDecisions.length > 0) {
aggregateHtml += `<h2>Decisions</h2><ul>${allDecisions.map(d => `<li>${escapeHtml(d)}</li>`).join("")}</ul>`;
}
if (allTopics.length > 0) {
aggregateHtml += `<h2>Topics</h2><div class="mi-chips">${allTopics.map(t => `<span class="mi-chip mi-chip--ok">${escapeHtml(t)}</span>`).join("")}</div>`;
}
aggregateHtml += `</div>`;
}
}
}
}
const extraStyles = `<style>
.mi-aggregate{margin-bottom:2rem;padding:1.5rem;border-radius:12px;background:var(--rs-bg-surface);border:1px solid var(--rs-border)}
.mi-aggregate h2{font-size:1.1rem;margin:1rem 0 .5rem;color:var(--rs-text-primary)}
.mi-aggregate h2:first-child{margin-top:0}
.mi-aggregate ul{padding-left:1.5rem;margin:.5rem 0}
.mi-aggregate li{margin:.25rem 0;color:var(--rs-text-primary);line-height:1.5}
.mi-section-title{font-size:1.2rem;margin:2rem 0 1rem;color:var(--rs-text-primary)}
</style>`;
return c.html(renderShell({
title: `Meeting Intelligence — rMeets | rSpace`,
moduleId: "rmeets",
spaceSlug: space,
modules: getModuleInfoList(),
styles: MI_STYLES + extraStyles,
body: `<div class="mi-page">
<h1>Meeting Intelligence</h1>
<p class="mi-subtitle">Knowledge objects, action items, and insights across all meetings</p>
<form class="mi-search-form" method="get" action="${base}/search">
<input type="text" name="q" placeholder="Search across all meetings...">
<button type="submit">Search</button>
</form>
${aggregateHtml ? `<h2 class="mi-section-title">Aggregate Knowledge</h2>${aggregateHtml}` : ""}
<h2 class="mi-section-title">Meetings</h2>
${meetingCards}
</div>`,
}));
});
// ── Room embed (catch-all — must be LAST so /meet, /recordings, /search match first) ── // ── Room embed (catch-all — must be LAST so /meet, /recordings, /search match first) ──
routes.get("/:room", (c) => { routes.get("/:room", (c) => {
@ -543,7 +668,7 @@ routes.get("/:room", (c) => {
} }
// Default: clean full-screen Jitsi — no rSpace shell, mobile-friendly // Default: clean full-screen Jitsi — no rSpace shell, mobile-friendly
const jitsiRoom = encodeURIComponent(room); const jitsiRoom = encodeURIComponent(space + "_" + room);
const meetsBase = `/${escapeHtml(space)}/rmeets`; const meetsBase = `/${escapeHtml(space)}/rmeets`;
return c.html(`<!DOCTYPE html> return c.html(`<!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -585,6 +710,7 @@ routes.get("/:room", (c) => {
<div class="mi-fab"> <div class="mi-fab">
<button class="mi-fab-btn" id="mi-fab-toggle" title="Meeting Intelligence">&#129504;</button> <button class="mi-fab-btn" id="mi-fab-toggle" title="Meeting Intelligence">&#129504;</button>
<div class="mi-dropdown" id="mi-dropdown"> <div class="mi-dropdown" id="mi-dropdown">
<a href="${meetsBase}/meeting-intelligence"><span class="mi-icon">&#129504;</span> Meeting Intelligence</a>
<a href="${meetsBase}/recordings"><span class="mi-icon">&#127909;</span> Recordings</a> <a href="${meetsBase}/recordings"><span class="mi-icon">&#127909;</span> Recordings</a>
<a href="${meetsBase}/search"><span class="mi-icon">&#128269;</span> Search Transcripts</a> <a href="${meetsBase}/search"><span class="mi-icon">&#128269;</span> Search Transcripts</a>
<div class="mi-sep"></div> <div class="mi-sep"></div>
@ -629,7 +755,7 @@ routes.get("/:room", (c) => {
"raisehand","tileview","toggle-camera", "raisehand","tileview","toggle-camera",
"fullscreen","chat","settings", "fullscreen","chat","settings",
"participants-pane","select-background", "participants-pane","select-background",
"sharedvideo", "sharedvideo","shareaudio","meetingintelligence",
], ],
}, },
interfaceConfigOverwrite: { interfaceConfigOverwrite: {