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/group stage history #62

Merged
merged 6 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
107 changes: 107 additions & 0 deletions src/lib/components/session/EndedView.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script lang="ts">
import type { Conversation } from '$lib/schema/conversation';
import type { Group } from '$lib/schema/group';
import Summary from './Summary.svelte';
import GroupSummary from './GroupSummary.svelte';
import Chatroom from '$lib/components/Chatroom.svelte';

let { conversationDoc, groupDoc, user } = $props<{
conversationDoc: { data: Conversation; id: string } | null;
groupDoc: { data: Group; id: string } | null;
user: { uid: string };
}>();

let activeTab = $state('summary'); // 'summary', 'groupSummary', 'chat', 'groupChat'

let individualConversations = $derived.by(() => {
if (!conversationDoc) return [];
return conversationDoc.data.history.map(
(message: { role: string; content: string; audio?: string }) => ({
name: message.role === 'user' ? 'You' : 'AI Assistant',
content: message.content,
self: message.role === 'user',
audio: message.audio || undefined
})
);
});

let groupConversations = $derived.by(() => {
if (!groupDoc) return [];
return groupDoc.data.discussions.map(
(discussion: {
speaker: string;
content: string;
id: string | null;
audio: string | null;
}) => ({
name: discussion.speaker,
content: discussion.content,
self: discussion.id === user.uid,
audio: discussion.audio || undefined
})
);
});
</script>

<div class="space-y-4">
<div class="flex space-x-4">
<button
class="rounded-lg px-4 py-2 {activeTab === 'summary'
? 'bg-blue-500 text-white'
: 'bg-gray-200'}"
onclick={() => (activeTab = 'summary')}
>
個人總結
</button>
<button
class="rounded-lg px-4 py-2 {activeTab === 'groupSummary'
? 'bg-blue-500 text-white'
: 'bg-gray-200'}"
onclick={() => (activeTab = 'groupSummary')}
>
群組總結
</button>
<button
class="rounded-lg px-4 py-2 {activeTab === 'chat' ? 'bg-blue-500 text-white' : 'bg-gray-200'}"
onclick={() => (activeTab = 'chat')}
>
個人對話歷史
</button>
<button
class="rounded-lg px-4 py-2 {activeTab === 'groupChat'
? 'bg-blue-500 text-white'
: 'bg-gray-200'}"
onclick={() => (activeTab = 'groupChat')}
>
群組討論歷史
</button>
</div>

{#if activeTab === 'summary' && conversationDoc}
<Summary
conversation={conversationDoc}
loading={false}
readonly={true}
onRefresh={async () => {}}
/>
{:else if activeTab === 'groupSummary' && groupDoc}
<GroupSummary
group={{
data: groupDoc.data,
id: groupDoc.id
}}
loading={false}
readonly={true}
onRefresh={async () => {}}
onUpdate={async () => {}}
/>
{:else if activeTab === 'chat' && conversationDoc}
<div class="h-[600px] rounded-lg border bg-white p-4">
<Chatroom conversations={individualConversations} readonly={true} />
</div>
{:else if activeTab === 'groupChat' && groupDoc}
<div class="h-[600px] rounded-lg border bg-white p-4">
<Chatroom conversations={groupConversations} readonly={true} />
</div>
{/if}
</div>
57 changes: 57 additions & 0 deletions src/lib/components/session/GroupChatHistory.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script lang="ts">
import { Modal, TabItem, Tabs } from 'flowbite-svelte';
import Chatroom from '$lib/components/Chatroom.svelte';
import GroupSummary from './GroupSummary.svelte';
import type { Group } from '$lib/schema/group';

let {
open = $bindable(false),
group = null,
readonly = true
} = $props<{
open: boolean;
group: {
data: Group;
id: string;
discussions: Array<{
name: string;
content: string;
self?: boolean;
audio?: string;
avatar?: string;
}>;
} | null;
readonly?: boolean;
}>();

let loadingGroupSummary = $state(false);
</script>

{#if open && group}
<Modal bind:open size="xl" outsideclose class="w-full">
<div class="mb-4">
<h3 class="text-xl font-semibold">
第 {group.data.number} 組的討論記錄
</h3>
</div>

<Tabs style="underline">
<TabItem open title="討論歷史">
<div class="messages h-[400px] overflow-y-auto rounded-lg border border-gray-200 p-4">
<Chatroom readonly conversations={group.discussions} />
</div>
</TabItem>
<TabItem title="討論總結">
<div class="h-[400px] overflow-y-auto rounded-lg border border-gray-200 p-4">
<GroupSummary
{group}
loading={loadingGroupSummary}
onRefresh={() => Promise.resolve()}
onUpdate={() => Promise.resolve()}
{readonly}
/>
</div>
</TabItem>
</Tabs>
</Modal>
{/if}
52 changes: 52 additions & 0 deletions src/lib/components/session/GroupStatus.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { Timestamp } from 'firebase/firestore';

let { group, showStatus = false } = $props<{
group: {
id: string;
status?: string;
updatedAt?: Timestamp;
};
showStatus?: boolean;
}>();

let status = $state('');

function getGroupStatus() {
if (group.status === 'discussion') {
if (group.updatedAt) {
const diffInSeconds = Math.floor((Date.now() - group.updatedAt.toMillis()) / 1000);
if (diffInSeconds > 20) {
return `idle ${diffInSeconds} seconds`;
}
}
return 'discussion';
}
return group.status || 'waiting';
}

onMount(() => {
const statusInterval = setInterval(() => {
status = getGroupStatus();
}, 1000);

return () => clearInterval(statusInterval);
});
</script>

{#if showStatus}
<span
class="rounded-full px-2 py-0.5 text-xs {status?.startsWith('idle')
? 'bg-red-100 text-red-600'
: status === 'discussion'
? 'bg-yellow-100 text-yellow-600'
: status === 'summarize'
? 'bg-blue-100 text-blue-600'
: status === 'end'
? 'bg-green-100 text-green-600'
: 'bg-gray-100 text-gray-600'}"
>
{status || getGroupStatus()}
</span>
{/if}
2 changes: 1 addition & 1 deletion src/lib/components/session/GroupSummary.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
</div>
{:else}
<div class="text-center text-gray-600">
<p>點擊上方按鈕生成討論總結</p>
<p>尚無討論總結</p>
</div>
{/if}
</div>
55 changes: 53 additions & 2 deletions src/lib/components/session/HostView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@
import { SvelteMap } from 'svelte/reactivity';
import { renderMarkdown } from '$lib/utils/renderMarkdown';
import ChatHistory from './ChatHistory.svelte';
import GroupChatHistory from './GroupChatHistory.svelte';
import GroupStatus from './GroupStatus.svelte';

let { session }: { session: Readable<Session> } = $props();
let code = $state('');
type GroupWithId = Group & { id: string };
type GroupWithId = Group & {
id: string;
updatedAt: Timestamp | undefined;
};
let groups = writable<GroupWithId[]>([]);
let participantNames = $state(new Map<string, string>());
type ParticipantProgress = {
Expand All @@ -42,6 +47,18 @@
}>;
} | null>(null);
let selectedConversation = $state<{ data: Conversation; id: string } | null>(null);
let showGroupChatHistory = $state(false);
let selectedGroup = $state<{
data: Group;
id: string;
discussions: Array<{
name: string;
content: string;
self?: boolean;
audio?: string;
avatar?: string;
}>;
} | null>(null);

onMount(() => {
const unsubscribes: (() => void)[] = [];
Expand Down Expand Up @@ -299,6 +316,27 @@
notifications.error('無法加載對話歷史');
}
}

async function handleGroupClick(group: GroupWithId) {
try {
const discussions = group.discussions.map((discussion) => ({
name: discussion.speaker,
content: discussion.content,
self: false,
audio: discussion.audio || undefined
}));

selectedGroup = {
data: group,
id: group.id,
discussions
};
showGroupChatHistory = true;
} catch (error) {
console.error('無法加載群組討論:', error);
notifications.error('無法加載群組討論');
}
}
</script>

<main class="mx-auto max-w-7xl px-4 py-16">
Expand Down Expand Up @@ -371,7 +409,16 @@
<div class="grid grid-cols-3 gap-4">
{#each [...$groups].sort((a, b) => a.number - b.number) as group}
<div class="rounded border p-3">
<h3 class="mb-2 text-sm font-semibold">Group #{group.number}</h3>
<div class="mb-2 flex items-center justify-between">
<button
class="cursor-pointer text-sm font-semibold hover:text-primary-600"
onclick={() => handleGroupClick(group)}
onkeydown={(e) => e.key === 'Enter' && handleGroupClick(group)}
>
Group #{group.number}
</button>
<GroupStatus {group} showStatus={$session?.status === 'group'} />
</div>
{#if group.participants.length === 0}
<p class="text-xs text-gray-500">No participants</p>
{:else}
Expand Down Expand Up @@ -470,4 +517,8 @@
conversation={selectedConversation}
/>
{/if}

{#if showGroupChatHistory && selectedGroup}
<GroupChatHistory bind:open={showGroupChatHistory} group={selectedGroup} readonly={true} />
{/if}
</main>
6 changes: 6 additions & 0 deletions src/lib/components/session/ParticipantView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import Summary from '$lib/components/session/Summary.svelte';
import GroupSummary from '$lib/components/session/GroupSummary.svelte';
import { initFFmpeg, float32ArrayToWav, wav2mp3 } from '$lib/utils/wav2mp3';
import EndedView from '$lib/components/session/EndedView.svelte';

interface ChatroomConversation {
name: string;
Expand Down Expand Up @@ -67,6 +68,9 @@
if ($session?.status === 'before-group' && conversationDoc && !conversationDoc.data.summary) {
fetchSummary();
}
if ($session?.status === 'ended' && groupDoc && !groupDoc.data.summary) {
fetchGroupSummary();
}
});

function updateConversationDoc() {
Expand Down Expand Up @@ -668,6 +672,8 @@
{/if}
{/if}
</div>
{:else if $session?.status === 'ended'}
<EndedView {conversationDoc} {groupDoc} {user} />
{/if}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/session/Summary.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
</div>
{:else}
<div class="text-center text-gray-600">
<p>點擊上方按鈕生成對話總結</p>
<p>尚無對話總結</p>
</div>
{/if}
</div>
4 changes: 3 additions & 1 deletion src/routes/api/session/[id]/group/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export const POST: RequestHandler = async ({ params, locals }) => {
summary: null,
keywords: {},
number: groupNumber,
createdAt: new Date()
createdAt: new Date(),
status: 'discussion',
updatedAt: new Date()
};

const result = GroupSchema.safeParse(groupData);
Expand Down
Loading