diff --git a/src/lib/server/llm.ts b/src/lib/server/llm.ts index 691da42..c8778dc 100644 --- a/src/lib/server/llm.ts +++ b/src/lib/server/llm.ts @@ -18,11 +18,6 @@ interface ChatMessage { content: string; } -interface Document { - title?: string; - content: string; -} - interface Student_opinion { student_id: string; student_conclusions: string; @@ -55,20 +50,24 @@ export async function chatWithLLMByDocs( messages: ChatMessage[], mainQuestion: string, secondaryGoal: string[], - documents: Document[], + documents: { + name: string; + text: string; + }[], temperature = 0.7 ) { try { if (await isHarmfulContent(messages[messages.length - 1].content)) { return { success: false, + message: '', error: 'Harmful content detected' }; } const formattedDocs = documents .map((doc, index) => { - const title = doc.title || `Document ${index + 1}`; - return `[${title}]:\n${doc.content}`; + const title = doc.name || `Document ${index + 1}`; + return `[${title}]:\n${doc.text}`; }) .join('\n\n'); @@ -91,7 +90,7 @@ export async function chatWithLLMByDocs( temperature }); - const result = response.choices[0].message; + const result = response.choices[0].message.content; if (!result) { throw new Error('Failed to parse response'); } @@ -105,11 +104,13 @@ export async function chatWithLLMByDocs( if (error instanceof z.ZodError) { return { success: false, + message: '', error: 'Type error: ' + error.errors.map((e) => e.message).join(', ') }; } return { success: false, + message: '', error: 'Failed to process documents and generate response' }; } diff --git a/src/lib/server/parse.ts b/src/lib/server/parse.ts new file mode 100644 index 0000000..8602e9e --- /dev/null +++ b/src/lib/server/parse.ts @@ -0,0 +1,5 @@ +export async function parsePdf2Text(filePath: string): Promise { + // TODO: pares pdf file to text + + return filePath; +} diff --git a/src/lib/types/IndividualDiscussion.ts b/src/lib/types/IndividualDiscussion.ts index 7ef7c50..585cbd6 100644 --- a/src/lib/types/IndividualDiscussion.ts +++ b/src/lib/types/IndividualDiscussion.ts @@ -5,10 +5,16 @@ export interface FirestoreIndividualDiscussion { userId: string; sessionId: string; groupId: string; + goal: string; + subQuestions: string[]; + resourcesTexts: { + name: string; + text: string; + }[]; history: { role: 'system' | 'assistant' | 'user'; + fileId: string | null; content: string; - speechId: string | null; timestamp: Timestamp; }[]; summary: string; @@ -19,10 +25,16 @@ export interface IndividualDiscussion { userId: string; sessionId: string; groupId: string; + goal: string; + subQuestions: string[]; + resourcesTexts: { + name: string; + text: string; + }[]; history: { role: 'system' | 'assistant' | 'user'; + fileId: string | null; content: string; - speechId: string | null; timestamp: string; }[]; summary: string; @@ -36,10 +48,13 @@ export function convertFirestoreIndividualDiscussion( userId: data.userId, sessionId: data.sessionId, groupId: data.groupId, + goal: data.goal, + subQuestions: data.subQuestions, + resourcesTexts: data.resourcesTexts, history: data.history.map((history) => ({ role: history.role, + fileId: history.fileId, content: history.content, - speechId: history.speechId, timestamp: history.timestamp.toDate().toISOString() })), summary: data.summary diff --git a/src/lib/types/groupDiscussion.ts b/src/lib/types/groupDiscussion.ts index c041026..74b4cf7 100644 --- a/src/lib/types/groupDiscussion.ts +++ b/src/lib/types/groupDiscussion.ts @@ -4,6 +4,7 @@ import type { Timestamp } from 'firebase-admin/firestore'; export interface FirestoreGroupDiscussion { sessionId: string; groupId: string; + groupName: string; history: { name: string; content: string; @@ -20,6 +21,7 @@ export interface FirestoreGroupDiscussion { export interface GroupDiscussion { sessionId: string; groupId: string; + groupName: string; history: { name: string; content: string; @@ -37,6 +39,7 @@ export function convertFirestoreGroupDiscussion(data: FirestoreGroupDiscussion): return { sessionId: data.sessionId, groupId: data.groupId, + groupName: data.groupName, history: data.history.map((history) => ({ name: history.name, content: history.content, diff --git a/src/lib/types/session.ts b/src/lib/types/session.ts index 597c1d5..cfc9bb7 100644 --- a/src/lib/types/session.ts +++ b/src/lib/types/session.ts @@ -9,18 +9,29 @@ export interface FirestoreSession { title: string; createdAt: Timestamp; status: 'draft' | 'waiting' | 'active' | 'ended'; + stage: 'grouping' | 'individual' | 'group' | 'ended'; tempIdExpiry: Timestamp | null; goal: string; subQuestions: string[]; - resourcesIds: string[]; + resourceIds: string[]; participants: { [userId: string]: { name: string; groupId: string | null; + groupName: string | null; joinedAt: Timestamp; }; }; - stage: 'grouping' | 'individual' | 'group' | 'ended'; + groups: { + [groupId: string]: { + groupName: string; + members: { + [userId: string]: { + name: string; + }; + }; + }; + }; } // Client-side data structure (serializable) @@ -32,10 +43,11 @@ export interface Session { title: string; createdAt: string; status: 'draft' | 'waiting' | 'active' | 'ended'; + stage: 'grouping' | 'individual' | 'group' | 'ended'; tempIdExpiry: string | null; goal: string; subQuestions: string[]; - resourcesIds: string[]; + resourceIds: string[]; participants: { [userId: string]: { name: string; @@ -43,7 +55,16 @@ export interface Session { joinedAt: string; }; }; - stage: 'grouping' | 'individual' | 'group' | 'ended'; + groups: { + [groupId: string]: { + groupName: string; + members: { + [userId: string]: { + name: string; + }; + }; + }; + }; } // convert Firestore data to client-side data @@ -56,20 +77,37 @@ export function convertFirestoreSession(data: FirestoreSession): Session { title: data.title, createdAt: data.createdAt.toDate().toISOString(), status: data.status, + stage: data.stage, tempIdExpiry: data.tempIdExpiry ? data.tempIdExpiry.toDate().toISOString() : null, goal: data.goal, subQuestions: data.subQuestions, - resourcesIds: data.resourcesIds, + resourceIds: data.resourceIds, participants: Object.fromEntries( Object.entries(data.participants).map(([userId, participant]) => [ userId, { name: participant.name, groupId: participant.groupId, + groupName: participant.groupName, joinedAt: participant.joinedAt.toDate().toISOString() } ]) ), - stage: data.stage + groups: Object.fromEntries( + Object.entries(data.groups).map(([groupId, group]) => [ + groupId, + { + groupName: group.groupName, + members: Object.fromEntries( + Object.entries(group.members).map(([userId, member]) => [ + userId, + { + name: member.name + } + ]) + ) + } + ]) + ) }; } diff --git a/src/lib/types/speech.ts b/src/lib/types/speech.ts deleted file mode 100644 index d2c20a8..0000000 --- a/src/lib/types/speech.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface FirestoreSpeech { - speechId: string; - fileId: string; - transcription: string; -} - -export interface Speech { - speechId: string; - fileId: string; - transcription: string; -} - -export function convertFirestoreSpeech(data: FirestoreSpeech): Speech { - return { - speechId: data.speechId, - fileId: data.fileId, - transcription: data.transcription - }; -} diff --git a/src/routes/session/[id]/+page.svelte b/src/routes/session/[id]/+page.svelte index f5c59aa..2c77189 100644 --- a/src/routes/session/[id]/+page.svelte +++ b/src/routes/session/[id]/+page.svelte @@ -8,7 +8,6 @@ Trash2, Play, Users, - Link, FileText, Clock, Pencil, @@ -213,24 +212,9 @@
{#each Object.entries(session.resources) as [, resource]}
- {#if resource.type === 'link'} - - {:else} - - {/if} +
- {#if resource.type === 'link'} - - {resource.content} - - {:else} -

{resource.content}

- {/if} +

{resource.text}

Added {formatDate(resource.addedAt)}

diff --git a/src/routes/session/[id]/participant/+page.server.ts b/src/routes/session/[id]/participant/+page.server.ts index a15f1dd..56f4ab4 100644 --- a/src/routes/session/[id]/participant/+page.server.ts +++ b/src/routes/session/[id]/participant/+page.server.ts @@ -1,4 +1,12 @@ +import { env } from '$env/dynamic/private'; import { adminDb } from '$lib/server/firebase'; +import { chatWithLLMByDocs } from '$lib/server/llm'; +import { parsePdf2Text } from '$lib/server/parse'; +import { transcribe } from '$lib/stt/core'; +import { + convertFirestoreIndividualDiscussion, + type FirestoreIndividualDiscussion +} from '$lib/types/IndividualDiscussion'; import type { FirestoreSession } from '$lib/types/session'; import { convertFirestoreSession } from '$lib/types/session'; import { error, fail, redirect } from '@sveltejs/kit'; @@ -60,5 +68,91 @@ export const actions = { }); return { success: true, groupId }; + }, + + startIndividualStage: async ({ locals, params }) => { + const sessionId = params.id; + const sessionRef = adminDb.collection('sessions').doc(sessionId); + const sessionData = (await sessionRef.get()).data() as FirestoreSession; + if (sessionData.hostId != locals.user?.uid) { + throw redirect(401, 'Unauthorized'); + } + + const resourcesTexts = []; + // parse pdf files to text + for (const resourceId of sessionData.resourceIds) { + const resourceRef = adminDb.collection('resources').doc(resourceId); + const resourceData = (await resourceRef.get()).data(); + const text = await parsePdf2Text(resourceData?.fileId); + resourcesTexts.push(text); + } + + // create conversation for each participants + for (const participantId in sessionData.participants) { + const individualDiscussionRef = adminDb.collection('individualDiscussion').doc(); + await individualDiscussionRef.set({ + userId: participantId, + sessionId: sessionId, + groupId: sessionData.participants[participantId].groupId, + goal: sessionData.goal, + subQuestions: sessionData.subQuestions, + resourcesTexts: resourcesTexts, + history: [], + summary: '' + }); + } + + // set session stage to individual + await sessionRef.update({ stage: 'individual' }); + + return { success: true }; + }, + + sendConversation: async ({ request, locals, params }) => { + if (!locals.user) { + throw error(401, 'Unauthorized'); + } + const { speech } = await request.json(); + const userId = locals.user.uid; + const sessionId = params.id; + + const fileId = 'Unknown file id'; // TODO: get file id from the file upload + const text = await transcribe(speech, env.HUGGINGFACE_TOKEN); + + const individualDiscussionRef = adminDb + .collection('individualDiscussion') + .where('userId', '==', userId) + .where('sessionId', '==', sessionId); + const individualDiscussionData = ( + await individualDiscussionRef.get() + ).docs[0].data() as FirestoreIndividualDiscussion; + + if (!individualDiscussionData) { + throw error(404, 'Individual discussion not found'); + } + + const { history, goal, subQuestions, resourcesTexts } = + convertFirestoreIndividualDiscussion(individualDiscussionData); + + history.push({ + role: 'user', + fileId: fileId, + content: text, + timestamp: new Date().toISOString() + }); + + const response = await chatWithLLMByDocs(history, goal, subQuestions, resourcesTexts); + if (!response.success) { + throw error(500, response.error); + } + + history.push({ + role: 'user', + fileId: null, + content: response.message, + timestamp: new Date().toISOString() + }); + + return { success: true }; } } satisfies Actions; diff --git a/src/routes/session/[id]/participant/+page.svelte b/src/routes/session/[id]/participant/+page.svelte index 63571fb..7093966 100644 --- a/src/routes/session/[id]/participant/+page.svelte +++ b/src/routes/session/[id]/participant/+page.svelte @@ -1,6 +1,5 @@