Skip to content

Commit

Permalink
feat: group summarize stage and editable summary
Browse files Browse the repository at this point in the history
- Added a new GroupSummary component to handle displaying and editing group discussion summaries and keywords.
- Enhanced ParticipantView to integrate GroupSummary, allowing users to view and update summaries based on session status.
- Updated the Group schema to include status and updatedAt fields for better state management.
- Implemented API endpoints for fetching and updating group summaries, including handling the transition between discussion and summary phases.
- Improved error handling and notifications for group summary operations.

This update enhances the collaborative experience by providing clear summaries and facilitating updates during group discussions.
  • Loading branch information
howard9199 committed Dec 23, 2024
1 parent 09eaf30 commit 4b03917
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 18 deletions.
114 changes: 114 additions & 0 deletions src/lib/components/session/GroupSummary.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script lang="ts">
import type { Group } from '$lib/schema/group';
export let group: {
data: Group;
id: string;
};
export let loading = false;
export let onRefresh: () => Promise<void>;
export let onUpdate: (summary: string, keywords: string[]) => Promise<void>;
export let readonly = false;
let isEditing = false;
let editedSummary = '';
let editedKeywords: string[] = [];
$: summaryData = group.data.summary
? {
summary: group.data.summary,
keywords: Object.entries(group.data.keywords || {}).map(([, text]) => `${text}`)
}
: null;
$: if (summaryData && !isEditing) {
editedSummary = summaryData.summary;
editedKeywords = [...summaryData.keywords];
}
async function handleUpdateSummary() {
await onUpdate(editedSummary, editedKeywords);
isEditing = false;
}
</script>

<div class="space-y-6 p-6">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold">群組討論總結</h2>
<div class="space-x-2">
{#if !loading && !readonly}
<button
class="rounded-lg bg-gray-500 px-4 py-2 text-white hover:bg-gray-600 disabled:opacity-50"
on:click={isEditing ? handleUpdateSummary : () => (isEditing = true)}
disabled={loading}
>
{isEditing ? '儲存' : '編輯'}
</button>

{#if !isEditing}
<button
class="rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 disabled:opacity-50"
on:click={onRefresh}
disabled={loading}
>
重新生成總結
</button>
{/if}
{:else if loading}
<button
class="rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 disabled:opacity-50"
disabled={true}
>
生成中...
</button>
{/if}
</div>
</div>

{#if summaryData}
<div class="space-y-4">
<div>
<h3 class="mb-2 font-medium">討論總結:</h3>
{#if isEditing && !readonly}
<textarea
class="w-full rounded-lg border p-4 text-gray-700"
bind:value={editedSummary}
rows="4"
></textarea>
{:else}
<p class="rounded-lg bg-gray-50 p-4 text-gray-700">{summaryData.summary}</p>
{/if}
</div>

{#if summaryData.keywords.length > 0}
<div>
<h3 class="mb-2 font-medium">討論關鍵字:</h3>
{#if isEditing && !readonly}
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
{#each editedKeywords as _, i}
<input
type="text"
class="mb-2 w-full rounded-lg border p-2 text-gray-700"
bind:value={editedKeywords[i]}
/>
{/each}
{:else}
<ul class="list-inside list-disc space-y-2">
{#each summaryData.keywords as point}
<li class="text-gray-700">{point}</li>
{/each}
</ul>
{/if}
</div>
{/if}
</div>
{:else if loading}
<div class="text-center text-gray-600">
<p>正在生成討論總結,請稍候...</p>
</div>
{:else}
<div class="text-center text-gray-600">
<p>點擊上方按鈕生成討論總結</p>
</div>
{/if}
</div>
161 changes: 150 additions & 11 deletions src/lib/components/session/ParticipantView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
import { Button, Input, Label } from 'flowbite-svelte';
import { notifications } from '$lib/stores/notifications';
import { page } from '$app/stores';
import { UserPlus, Users } from 'lucide-svelte';
import { UserPlus, Users, CircleCheck } from 'lucide-svelte';
import { db } from '$lib/firebase';
import { collection, query, where, onSnapshot } from 'firebase/firestore';
import { onDestroy, onMount } from 'svelte';
import { getUser } from '$lib/utils/getUser';
import Chatroom from '$lib/components/Chatroom.svelte';
import { MicVAD, utils } from '@ricky0123/vad-web';
import Summary from '$lib/components/session/Summary.svelte';
import GroupSummary from '$lib/components/session/GroupSummary.svelte';
interface ChatroomConversation {
name: string;
Expand All @@ -31,10 +32,13 @@
}>();
let groupDoc = $state<{ data: Group; id: string } | null>(null);
let groupStatus = $derived.by(() => groupDoc?.data.status || 'discussion');
let conversationDoc = $state<{ data: Conversation; id: string } | null>(null);
let conversationDocUnsubscribe: (() => void) | null = null;
onDestroy(() => conversationDocUnsubscribe?.());
let loadingGroupSummary = $state(false);
onMount(() => {
const groupsRef = collection(db, 'sessions', $page.params.id, 'groups');
const groupDocQuery = query(groupsRef, where('participants', 'array-contains', user.uid));
Expand Down Expand Up @@ -315,7 +319,8 @@
},
body: JSON.stringify({
content: text,
speaker: $authUser?.displayName || 'Unknown User'
speaker: $authUser?.displayName || 'Unknown User',
audio: null
})
}
);
Expand Down Expand Up @@ -382,10 +387,123 @@
vad.destroy();
};
}
async function fetchGroupSummary() {
if (!groupDoc) {
notifications.error('找不到群組');
return;
}
loadingGroupSummary = true;
try {
const response = await fetch(
`/api/session/${$page.params.id}/group/${groupDoc.id}/discussions/summary`
);
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '無法獲取群組討論總結');
}
notifications.success('成功獲取群組討論總結');
} catch (error) {
console.error('獲取群組討論總結時出錯:', error);
notifications.error('無法獲取群組討論總結');
} finally {
loadingGroupSummary = false;
}
}
async function handleUpdateGroupSummary(summary: string, keywords: string[]) {
if (!groupDoc) {
notifications.error('找不到群組');
return;
}
try {
const response = await fetch(
`/api/session/${$page.params.id}/group/${groupDoc.id}/discussions/summary`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
updated_summary: summary,
keywords: keywords
})
}
);
if (!response.ok) {
throw new Error('更新失敗');
}
notifications.success('成功更新群組討論總結');
} catch (error) {
console.error('更新群組討論總結時出錯:', error);
notifications.error('無法更新群組討論總結');
}
}
async function handleEndGroup() {
if (!groupDoc) {
notifications.error('找不到群組');
return;
}
try {
await fetchGroupSummary();
notifications.success('成功結束群組討論');
} catch (error) {
console.error('結束群組階段時出錯:', error);
notifications.error('無法結束群組階段');
}
}
async function handleEndSummarize() {
if (!groupDoc) {
notifications.error('找不到群組');
return;
}
try {
const response = await fetch(
`/api/session/${$page.params.id}/group/${groupDoc.id}/discussions/end`,
{
method: 'POST'
}
);
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '無法結束總結階段');
}
notifications.success('成功完成群組總結');
} catch (error) {
console.error('結束總結階段時出錯:', error);
notifications.error('無法結束總結階段');
}
}
</script>

<main class="mx-auto max-w-7xl px-2 py-8">
<h1 class="mb-8 text-3xl font-bold">{$session?.title}</h1>
<div class="flex items-center justify-between">
<h1 class="mb-8 text-3xl font-bold">{$session?.title}</h1>
{#if $session?.status === 'group' && groupDoc && groupDoc.data.participants[0] === user.uid}
{#if groupStatus === 'discussion'}
<Button color="green" on:click={handleEndGroup}>
<CircleCheck class="mr-2 h-4 w-4" />
Finish Group Discussion
</Button>
{:else if groupStatus === 'summarize'}
<Button color="green" on:click={handleEndSummarize}>
<CircleCheck class="mr-2 h-4 w-4" />
Confirm Group Summary
</Button>
{/if}
{/if}
</div>

<div class="grid gap-8 md:grid-cols-4">
<div class="space-y-8 md:col-span-1">
Expand Down Expand Up @@ -430,17 +548,17 @@
<div>
<h3 class="mb-2 font-medium">Members:</h3>
<ul class="space-y-2">
{#each groupDoc.data.participants as participant}
{#each groupDoc.data.participants as participant, index}
<li class="flex items-center gap-2">
<Users class="h-4 w-4" />
<span>
{#if participant === user.uid}
You
You{index === 0 ? ' (Leader)' : ''}
{:else}
{#await getUser(participant)}
<span class="text-gray-500">載入中...</span>
{:then profile}
{profile.displayName}
{profile.displayName}{index === 0 ? ' (Leader)' : ''}
{:catch}
<span class="text-red-500">未知使用者</span>
{/await}
Expand Down Expand Up @@ -508,11 +626,32 @@
</div>
{:else if $session?.status === 'group'}
<div class="space-y-6">
<Chatroom
conversations={groupDiscussions}
record={handleGroupRecord}
send={handleGroupSend}
/>
{#if groupStatus === 'discussion' && !loadingGroupSummary}
<Chatroom
conversations={groupDiscussions}
record={handleGroupRecord}
send={handleGroupSend}
/>
{:else if groupStatus === 'summarize' || loadingGroupSummary}
{#if groupDoc}
<GroupSummary
group={groupDoc}
loading={loadingGroupSummary}
onRefresh={fetchGroupSummary}
onUpdate={handleUpdateGroupSummary}
/>
{/if}
{:else if groupStatus === 'end'}
{#if groupDoc}
<GroupSummary
readonly
group={groupDoc}
loading={loadingGroupSummary}
onRefresh={fetchGroupSummary}
onUpdate={handleUpdateGroupSummary}
/>
{/if}
{/if}
</div>
{/if}
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/schema/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const GroupSchema = z.object({
audio: z.string().nullable() // to find the raw file
})
),
updatedAt: z.date().nullable(),
status: z.enum(['discussion', 'summarize', 'end']).default('discussion'),
summary: z.string().nullable(), // lock on stage 2 finalize transaction
keywords: z.record(z.string(), z.number().min(1).max(5))
});
Expand Down
7 changes: 5 additions & 2 deletions src/lib/server/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,10 @@ export async function summarizeConcepts(

export async function summarizeGroupOpinions(
student_opinion: StudentSpeak[]
): Promise<{ success: boolean; summary: string; error?: string }> {
): Promise<{ success: boolean; summary: string; keywords: string[]; error?: string }> {
try {
const formatted_opinions = student_opinion
.filter((opinion) => opinion.role !== '摘要小幫手')
.map((opinion) => `${opinion.role}: ${opinion.content}`)
.join('\n');

Expand All @@ -374,13 +375,15 @@ export async function summarizeGroupOpinions(

return {
success: true,
summary: message.group_summary
summary: message.group_summary,
keywords: message.group_key_points
};
} catch (error) {
console.error('Error in summarizeGroupOpinions:', error);
return {
success: false,
summary: '',
keywords: [],
error: 'Failed to summarize group opinions'
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export const POST: RequestHandler = async ({ request, params, locals }) => {
speaker: speaker,
audio: audio
}
]
],
updatedAt: new Date()
});
});

Expand Down
Loading

0 comments on commit 4b03917

Please sign in to comment.