Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
65259cf
pdf generation on leaving
Ishitajoshii Jan 19, 2026
5d5466c
Merge branch 'main' of https://github.com/Ishitajoshii/conclave
Ishitajoshii Jan 19, 2026
ebaee5a
Merge branch 'main' of https://github.com/Ishitajoshii/conclave
Ishitajoshii Jan 21, 2026
6c77963
works but socket closes immediately
Ishitajoshii Jan 21, 2026
312d323
fix
Ishitajoshii Jan 21, 2026
214e11b
yay
Ishitajoshii Jan 21, 2026
13601e6
Merge branch 'main' of https://github.com/Ishitajoshii/conclave
Ishitajoshii Feb 19, 2026
3c1bc88
fix: lint issues
Ishitajoshii Feb 19, 2026
92e7f9e
fix: server error resolved
Ishitajoshii Feb 24, 2026
a2e7f56
everything should workk
Ishitajoshii Feb 25, 2026
425544f
fix: remove double transcrippt generation
Ishitajoshii Feb 25, 2026
db160cc
fix: caching transcript
Ishitajoshii Feb 25, 2026
c726713
pdf format changed
Ishitajoshii Feb 25, 2026
f2b99cb
ft: basic summarisation logic
Ishitajoshii Feb 25, 2026
1b5862d
fix: ws error handling
Ishitajoshii Feb 25, 2026
9c28851
it workskskssks
Ishitajoshii Feb 25, 2026
914735b
fix: date format
Ishitajoshii Feb 25, 2026
71fba47
fix: transcript and summarisation format
Ishitajoshii Feb 25, 2026
1bc0e32
Merge branch 'main' of https://github.com/Ishitajoshii/conclave
Ishitajoshii Feb 25, 2026
a86c05e
fix: server isue
Ishitajoshii Feb 25, 2026
a541c0b
ft: ensure only host or people who remain after host has left can gen…
Ishitajoshii Feb 25, 2026
aab3b85
Merge branch 'ACM-VIT:main' into main
Ishitajoshii Feb 25, 2026
9a33a62
Merge branch 'main' of https://github.com/Ishitajoshii/conclave
Ishitajoshii Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
.sfu*.log
.web*.log

# local tooling
.corepack/
.tmp/

# env files (can opt-in for committing if needed)
.env*
Expand Down
6 changes: 4 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@conclave/apps-sdk": "workspace:*",
"@conclave/meeting-core": "workspace:*",
"@tanstack/react-hotkeys": "^0.1.1",
"@tanstack/react-hotkeys": "^0.1.3",
"better-auth": "^1.4.10",
"jose": "^6.1.3",
"jsonwebtoken": "^9.0.3",
Expand All @@ -21,14 +21,16 @@
"next": "^16.1.6",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"socket.io-client": "^4.8.3"
"socket.io-client": "^4.8.3",
"ws": "^8.18.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.5.10",
"babel-plugin-react-compiler": "^1.0.0",
"tailwindcss": "^4",
"typescript": "^5.9.3"
Expand Down
35 changes: 35 additions & 0 deletions apps/web/src/app/api/minutes/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextResponse } from "next/server";

const SFU_URL = process.env.SFU_URL || "http://localhost:3031";
const SFU_SECRET = process.env.SFU_SECRET || "development-secret";
const SFU_CLIENT_ID = process.env.SFU_CLIENT_ID || "default";

export async function POST(request: Request) {
const { roomId } = await request.json().catch(() => ({ roomId: undefined }));
if (!roomId || typeof roomId !== "string") {
return NextResponse.json({ error: "roomId required" }, { status: 400 });
}

const res = await fetch(`${SFU_URL}/minutes`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-sfu-secret": SFU_SECRET,
},
body: JSON.stringify({ roomId, clientId: SFU_CLIENT_ID }),
});

if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text || "Failed to generate" }, { status: res.status });
}

const buffer = Buffer.from(await res.arrayBuffer());
return new NextResponse(buffer, {
status: 200,
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="minutes-${roomId}.pdf"`,
},
});
}
3 changes: 2 additions & 1 deletion apps/web/src/app/components/DevMeetToolsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ export default function DevMeetToolsPanel({ roomId }: DevMeetToolsPanelProps) {
};
const { io } = await import("socket.io-client");
const socket = io(data.sfuUrl, {
transports: ["websocket", "polling"],
transports: ["polling", "websocket"],
tryAllTransports: true,
timeout: 10000,
reconnection: false,
forceNew: true,
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/app/hooks/useMeetSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,7 @@ export function useMeetSocket({
for (const [producerId, consumer] of consumersRef.current.entries()) {
if (consumer.closed || consumer.track?.readyState === "ended") {
staleConsumerIds.push(producerId);

}
}

Expand Down Expand Up @@ -1473,7 +1474,8 @@ export function useMeetSocket({
}

const socket = io(sfuUrl, {
transports: ["websocket", "polling"],
transports: ["polling", "websocket"],
tryAllTransports: true,
timeout: SOCKET_TIMEOUT_MS,
reconnection: false,
auth: { token },
Expand Down
94 changes: 91 additions & 3 deletions apps/web/src/app/meets-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -820,10 +820,59 @@ export default function MeetsClient({
}
}, [clearGuestStorage, currentUser, isSigningOut]);

const [showMinutesPrompt, setShowMinutesPrompt] = useState(false);
const [minutesBusy, setMinutesBusy] = useState(false);
const [minutesError, setMinutesError] = useState<string | null>(null);

const leaveRoom = useCallback(() => {
playNotificationSoundForEvents("leave");
socket.cleanup();
}, [playNotificationSoundForEvents, socket.cleanup]);
}, [playNotificationSound, socket.cleanup]);

const requestLeaveRoom = useCallback(() => {
if (isAdminFlag) {
setShowMinutesPrompt(true);
return;
}
leaveRoom();
}, [isAdminFlag, leaveRoom]);

const requestMinutesAndLeave = useCallback(async () => {
if (!isAdminFlag) {
leaveRoom();
return;
}
setMinutesError(null);
setMinutesBusy(true);
try {
const res = await fetch("/api/minutes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomId }),
});

if (!res.ok) {
const text = await res.text();
throw new Error(text || "failed to generate minutes");
}

const blob = await res.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
const ts = new Date().toISOString().replace(/[:.]/g, "-");
link.download = `minutes-${roomId}-${ts}.pdf`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
leaveRoom();
} catch (err) {
setMinutesError((err as Error).message);
} finally {
setMinutesBusy(false);
}
}, [isAdminFlag, roomId, leaveRoom]);

useEffect(() => {
leaveRoomCommandRef.current = leaveRoom;
Expand Down Expand Up @@ -967,6 +1016,43 @@ export default function MeetsClient({
connectionState === "reconnecting" ||
connectionState === "waiting"; // Waiting is a kind of loading state visually, or handled separately

const renderMinutesPrompt = showMinutesPrompt && isAdminFlag && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4">
<div className="w-full max-w-md rounded-lg bg-[#0b1021] p-5 shadow-xl border border-white/10">
<h3 className="text-lg font-semibold text-white mb-2">
Generate meeting minutes?
</h3>
<p className="text-sm text-white/80 mb-4">
Before ending the meet, do you want to generate minutes (PDF) with the transcript summary?
</p>
{minutesError && (
<div className="mb-3 rounded bg-red-500/10 text-red-200 text-sm px-3 py-2">
{minutesError}
</div>
)}
<div className="flex justify-end gap-2">
<button
onClick={() => {
setShowMinutesPrompt(false);
leaveRoom();
}}
className="px-3 py-2 text-sm rounded bg-white/10 text-white hover:bg-white/20"
disabled={minutesBusy}
>
No, just leave
</button>
<button
onClick={requestMinutesAndLeave}
className="px-3 py-2 text-sm rounded bg-green-500 text-white hover:bg-green-600 disabled:opacity-60"
disabled={minutesBusy}
>
{minutesBusy ? "generating..." : "yes, generate"}
</button>
</div>
</div>
</div>
);

const renderWithApps = (content: React.ReactNode) => (
<AppsProvider
socket={appsSocket}
Expand Down Expand Up @@ -1065,6 +1151,7 @@ export default function MeetsClient({
<div
className={`flex flex-col h-dvh w-full bg-[#0d0e0d] text-white ${fontClassName ?? ""}`}
>
{renderMinutesPrompt}
{isJoined && meetError && (
<MeetsErrorBanner
meetError={meetError}
Expand Down Expand Up @@ -1129,7 +1216,7 @@ export default function MeetsClient({
toggleChat={toggleChat}
toggleHandRaised={toggleHandRaised}
sendReaction={sendReaction}
leaveRoom={leaveRoom}
leaveRoom={requestLeaveRoom}
isParticipantsOpen={isParticipantsOpen}
setIsParticipantsOpen={setIsParticipantsOpen}
pendingUsers={pendingUsers}
Expand Down Expand Up @@ -1196,6 +1283,7 @@ export default function MeetsClient({
<div
className={`flex flex-col h-full w-full bg-[#1a1a1a] text-white ${fontClassName ?? ""}`}
>
{renderMinutesPrompt}
<MeetsHeader
isJoined={isJoined}
isAdmin={isAdminFlag}
Expand Down Expand Up @@ -1284,7 +1372,7 @@ export default function MeetsClient({
toggleChat={toggleChat}
toggleHandRaised={toggleHandRaised}
sendReaction={sendReaction}
leaveRoom={leaveRoom}
leaveRoom={requestLeaveRoom}
isParticipantsOpen={isParticipantsOpen}
setIsParticipantsOpen={setIsParticipantsOpen}
pendingUsers={pendingUsers}
Expand Down
9 changes: 7 additions & 2 deletions packages/sfu/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//dont addd thisss

import dotenv from "dotenv";
import path from "path";
import { fileURLToPath } from "url";
Expand Down Expand Up @@ -161,15 +163,18 @@ export const config = {
listenIps: [
{
ip: "0.0.0.0",
announcedIp,
announcedIp: process.env.ANNOUNCED_IP || "127.0.0.1",
},
],
maxIncomingBitrate: 1500000,
initialAvailableOutgoingBitrate: 1000000,
},
plainTransport: {
listenIp: process.env.PLAIN_TRANSPORT_LISTEN_IP || "0.0.0.0",
announcedIp: plainTransportAnnouncedIp,
announcedIp:
process.env.PLAIN_TRANSPORT_ANNOUNCED_IP ||
process.env.ANNOUNCED_IP ||
"127.0.0.1",
},
};

Expand Down
6 changes: 5 additions & 1 deletion packages/sfu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"dev": "npx tsx server.ts",
"start": "npx tsx server.ts",
"typecheck": "npx tsc --noEmit"
"typecheck": "tsc --noEmit"
},
"keywords": [],
"author": "",
Expand All @@ -22,7 +22,9 @@
"jsonwebtoken": "^9.0.3",
"lib0": "^0.2.117",
"mediasoup": "^3.19.17",
"pdfkit": "^0.15.0",
"socket.io": "^4.8.3",
"ws": "^8.18.0",
"y-protocols": "^1.0.7",
"yjs": "^13.6.29"
},
Expand All @@ -31,6 +33,8 @@
"@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/pdfkit": "^0.13.6",
"@types/ws": "^8.5.10",
"tsx": "^4.20.6",
"typescript": "^5"
}
Expand Down
26 changes: 26 additions & 0 deletions packages/sfu/scripts/test-vosk-ws.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import WebSocket from 'ws';

const WS_URL = 'ws://127.0.0.1:2800';
const ws = new WebSocket(WS_URL);

ws.on('open', () => {
console.log('✅ Connected! Sending config...');

// 1. Send Config
ws.send(JSON.stringify({ config: { sample_rate: 16000 } }));

// 2. Send some "silence" to keep it alive (1 second of 16-bit PCM)
console.log('Sending 1 second of silence to keep connection open...');
const silence = Buffer.alloc(32000); // 16000 samples * 2 bytes
ws.send(silence);
});

ws.on('message', (data) => {
console.log('📝 Received from Vosk:', data.toString());
});

ws.on('close', (code, reason) => {
console.log(`🔌 Connection closed (Code: ${code}, Reason: ${reason || 'None'})`);
});

ws.on('error', (err) => console.error('❌ Error:', err));
Loading