Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/auto group #162

Merged
merged 12 commits into from
Mar 17, 2025
13 changes: 13 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,19 @@
"loadingGroups": "Loading groups",
"Groups": "Groups",
"noParticipantsInGroup": "no Participants in current group",
"joinWaitlist": "Join the waiting list",
"leaveWaitlist": "Leave the waiting list",
"failedJoinWaitlist": "Failed to join waiting list",
"successJoinWaitlist": "Joined waiting list successfully",
"failedLeaveWaitlist": "Failed to leave waiting list",
"successLeaveWaitlist": "Left waiting list successfully",
"isInWaitlist": "You are already in the waiting list",
"autoGrouping": "Auto",
"manualGrouping": "Manual",
"autoGroupingDesc": "System will automatically assign participants to groups",
"manualGroupingDesc": "Participants can freely choose which group to join",
"groupModeApplied": "Apply",
"autoGroupingUnit": "Group(s)",
"settings": "Settings",
"settingsInfo": "Customize your application preferences and features",
"settingsSuccessMessage": "Settings updated successfully!",
Expand Down
6 changes: 6 additions & 0 deletions messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@
"loadingGroups": "載入群組...",
"Groups": "組別",
"noParticipantsInGroup": "此群組中沒有參與者",
"autoGrouping": "自動分組",
"manualGrouping": "手動分組",
"autoGroupingDesc": "系統將自動為參與者分配群組",
"manualGroupingDesc": "參與者可以自由選擇要加入的群組",
"groupModeApplied": "套用",
"autoGroupingUnit": "組",
"settings": "設定",
"settingsInfo": "自定義您的應用程式偏好和功能",
"settingsSuccessMessage": "設定更新成功!",
Expand Down
114 changes: 113 additions & 1 deletion src/lib/components/session/HostView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import LabelManager from './LabelManager.svelte';
import ResolveUsername from '../ResolveUsername.svelte';
import * as m from '$lib/paraglide/messages.js';
import { Toggle, Input } from 'flowbite-svelte';

let { session }: { session: Readable<Session> } = $props();
let code = $state('');
Expand Down Expand Up @@ -80,6 +81,72 @@
} | null>(null);
let conversationsData = $state<Array<Conversation>>([]);
let keywordData = $state<Record<string, number>>({});
let groupNumber = $state(1);
let autoGroup = $state(true);
let settings = $state<Session['settings']>({ autoGroup: true });

async function handleApplyGroups() {
if (!$session?.waitlist || !autoGroup || groupNumber < 1) return;
let waitlist = $session.waitlist;
const groupSizeBig = Math.ceil(waitlist.length / groupNumber);
const groupSizeSmall = Math.floor(waitlist.length / groupNumber);
const Bignum = waitlist.length % groupNumber;
let nums = 0;
for (let i = 0; i < groupNumber; i++) {
const groupSize = i < Bignum ? groupSizeBig : groupSizeSmall;
const group = waitlist.slice(nums, groupSize);
nums += groupSize;
if (group.length <= 0) break;
const response = await fetch(`/api/session/${$page.params.id}/group/auto_group`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(group)
});
if (!response.ok) {
const data = await response.json();
notifications.error(data.error || 'Auto group failed');
return;
}
}
waitlist = [];
}

async function updateSettings() {
try {
settings.autoGroup = autoGroup;
const response = await fetch(`/api/session/${$page.params.id}/settings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(settings)
});

if (!response.ok) {
throw new Error('Failed to update settings');
} else {
notifications.success('Settings updated', 3000);
}
} catch (error) {
console.error('Error updating settings:', error);
notifications.error('Failed to update settings');
}
}

async function handleAutoGroupToggle(event: Event) {
const newValue = (event.target as HTMLInputElement).checked;
try {
await updateSettings();
autoGroup = newValue;
} catch (error) {
console.error('Error updating auto group setting:', error);
notifications.error('Failed to update auto group setting');
// Revert the toggle if update fails
autoGroup = !newValue;
}
}

onMount(() => {
const unsubscribes: (() => void)[] = [];
Expand Down Expand Up @@ -571,7 +638,52 @@
? 'md:col-span-4'
: ''}"
>
<h2 class="mb-4 text-xl font-semibold">{m.Groups()}</h2>
<!-- Replace the waitlist participants section with this -->
<div class="mb-6 border-b pb-4">
<h3 class="mb-3 text-lg font-semibold">Waitlist Participants</h3>
<div class="flex flex-wrap gap-2">
{#if $session?.waitlist && $session.waitlist.length > 0}
{#each $session.waitlist as participantId}
<div class="flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1">
<span class="h-2 w-2 rounded-full bg-green-500"></span>
<span class="text-sm">
<ResolveUsername id={participantId} />
</span>
</div>
{/each}
{:else}
<p class="text-sm text-gray-500">No participants in waitlist</p>
{/if}
</div>
</div>

<!-- Existing Groups Section -->
<div class="mb-4 flex items-center justify-between">
<h2 class="text-xl font-semibold">{m.Groups()}</h2>
{#if $session?.status === 'preparing'}
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<Toggle bind:checked={autoGroup} on:change={handleAutoGroupToggle}>
{autoGroup ? m.autoGrouping() : m.manualGrouping()}
</Toggle>
<Tooltip placement="right">
{autoGroup ? m.autoGroupingDesc() : m.manualGroupingDesc()}
</Tooltip>
</div>
{#if autoGroup}
<div class="flex items-center gap-2">
<Input type="number" min="1" max="50" class="w-20" bind:value={groupNumber} />
<span class="text-sm text-gray-500">{m.autoGroupingUnit()}</span>
</div>
{/if}
{#if autoGroup}
<Button color="primary" size="sm" on:click={handleApplyGroups}>
{m.groupModeApplied()}
</Button>
{/if}
</div>
{/if}
</div>
{#if $groups.length === 0}
<Alert>{m.waitingForParticipants()}</Alert>
{:else}
Expand Down
135 changes: 119 additions & 16 deletions src/lib/components/session/ParticipantView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { page } from '$app/stores';
import { UserPlus, User, Users, CircleCheck, LogOut } from 'lucide-svelte';
import { db } from '$lib/firebase';
import { collection, query, where, onSnapshot } from 'firebase/firestore';
import { collection, query, where, onSnapshot, getDoc, doc } from 'firebase/firestore';
import { onDestroy, onMount } from 'svelte';
import { getUser } from '$lib/utils/getUser';
import Chatroom from '$lib/components/Chatroom.svelte';
Expand All @@ -30,11 +30,14 @@
avatar?: string;
}

let { session, user } = $props<{
let {
session,
user
}: {
session: Readable<Session>;
// eslint-disable-next-line no-undef
user: App.Locals['user'];
}>();
user: Exclude<App.Locals['user'], null>;
} = $props();

let groupDoc = $state<{ data: Group; id: string } | null>(null);
let groupStatus = $derived.by(() => groupDoc?.data.status || 'discussion');
Expand All @@ -49,6 +52,17 @@

let isCreatingGroup = $state(false);

let isGroupManagementEnabled = $state(false);
$effect(() => {
if ($session?.settings?.autoGroup) {
isGroupManagementEnabled = false;
} else {
isGroupManagementEnabled = true;
}
});

let waitlistjoined = $state(false);

onMount(() => {
const groupsRef = collection(db, 'sessions', $page.params.id, 'groups');
const groupDocQuery = query(groupsRef, where('participants', 'array-contains', user.uid));
Expand All @@ -66,6 +80,8 @@
updateConversationDoc();
});

getwaitlist();

pInitFFmpeg = initFFmpeg();

return unsbscribe;
Expand Down Expand Up @@ -173,6 +189,47 @@
}
}

async function handleWaitlist() {
if (waitlistjoined) {
try {
const response = await fetch(`/api/session/${$page.params.id}/action/leaveWaitlist`, {
method: 'POST'
});

if (!response.ok) {
await response.json();
notifications.error(m.failedLeaveWaitlist());
return;
}

waitlistjoined = false;
notifications.success(m.successLeaveWaitlist());
} catch (error) {
console.error('Error leaving waiting:', error);
notifications.error(m.failedLeaveWaitlist());
}
return;
} else {
try {
const response = await fetch(`/api/session/${$page.params.id}/action/joinWaitlist`, {
method: 'POST'
});

if (!response.ok) {
await response.json(); // ignore result
notifications.error(m.failedJoinWaitlist());
return;
}

waitlistjoined = true; // Set state immediately after successful join
notifications.success(m.successJoinWaitlist());
} catch (error) {
console.error('Error joining waiting:', error);
notifications.error(m.failedJoinWaitlist());
}
}
}

async function sendAudioToSTT(file: File) {
const formData = new FormData();
formData.append('file', file);
Expand Down Expand Up @@ -237,6 +294,21 @@
};
}

async function getwaitlist() {
const docRef = doc(db, 'sessions', $page.params.id);
const docSnap = await getDoc(docRef);
if (!docSnap.exists()) {
waitlistjoined = false;
} else {
const data = docSnap.data();
if (data.waitlist.includes(user.uid)) {
waitlistjoined = true;
} else {
waitlistjoined = false;
}
}
}

async function handleRecord() {
if (!conversationDoc || !groupDoc) {
notifications.error('No group or conversation found');
Expand Down Expand Up @@ -618,6 +690,7 @@
size="xs"
class="hidden sm:flex"
onclick={() => groupDoc?.id && handleLeaveGroup(groupDoc.id, user.uid)}
disabled={!isGroupManagementEnabled}
>
<LogOut class="mr-2 h-4 w-4" />
{m.leaveGroup()}
Expand All @@ -627,6 +700,7 @@
size="xs"
class="sm:hidden"
onclick={() => groupDoc?.id && handleLeaveGroup(groupDoc.id, user.uid)}
disabled={!isGroupManagementEnabled}
>
<LogOut class="h-4 w-4" />
</Button>
Expand All @@ -636,7 +710,12 @@
{:else if $session?.status === 'preparing'}
<div class="mt-6 space-y-4">
<h3 class="font-medium">{m.groupManagement()}</h3>
{#if creating}
{#if !isGroupManagementEnabled}
<Button color="primary" on:click={handleWaitlist}>
<Users class="mr-2 h-4 w-4" />
{!waitlistjoined ? m.joinWaitlist() : m.leaveWaitlist()}
</Button>
{:else if creating}
<div class="space-y-4">
<div class="flex items-center gap-2">
<Label for="groupNumber">{m.groupNum()}</Label>
Expand All @@ -647,22 +726,46 @@
min="1"
max="50"
placeholder={m.enterGroupNumber()}
disabled={!isGroupManagementEnabled}
/>
</div>
<Button color="primary" on:click={handleJoinGroup}>
<UserPlus class="mr-2 h-4 w-4" />
{m.jointGroup()}
</Button>
<div class="space-y-2">
<Button
color="primary"
on:click={handleJoinGroup}
disabled={!isGroupManagementEnabled}
>
<UserPlus class="mr-2 h-4 w-4" />
{m.jointGroup()}
</Button>
<Button
color="alternative"
on:click={() => (creating = false)}
disabled={!isGroupManagementEnabled}
>
{m.cancel()}
</Button>
</div>
</div>
{:else}
<Button color="primary" on:click={handleCreateGroup} disabled={isCreatingGroup}>
<Users class="mr-2 h-4 w-4" />
{isCreatingGroup ? m.creatingGroup() : m.createNewGroup()}
</Button>
<div class="space-y-2">
<Button
color="primary"
on:click={handleCreateGroup}
disabled={isCreatingGroup || !isGroupManagementEnabled}
>
<Users class="mr-2 h-4 w-4" />
{isCreatingGroup ? m.creatingGroup() : m.createNewGroup()}
</Button>
<Button
color="alternative"
on:click={() => (creating = true)}
disabled={!isGroupManagementEnabled}
>
{m.joinExistingGroup()}
</Button>
</div>
{/if}
<Button color="alternative" on:click={() => (creating = !creating)}>
{creating ? m.cancel() : m.joinExistingGroup()}
</Button>
</div>
{:else}
<p class="text-gray-600">{m.notInGroup()}</p>
Expand Down
9 changes: 8 additions & 1 deletion src/lib/schema/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ export const SessionSchema = z.object({
start: Timestamp.nullable(),
end: Timestamp.nullable()
})
})
}),
settings: z
.object({
autoGroup: z.boolean()
})
.optional()
.default({ autoGroup: true }),
waitlist: z.array(z.string()).default([])
});

export type Session = z.infer<typeof SessionSchema>;
Loading