From ebb32ee613348e64d71aaf93fa7cae260052956d Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Thu, 3 Oct 2024 18:25:53 +0200 Subject: [PATCH 1/5] Transcriptions filtering and search --- server/reflector/db/transcripts.py | 32 +- server/reflector/views/transcripts.py | 6 + www/app/(app)/browse/page.tsx | 528 +++++++++++------- .../(app)/transcripts/useTranscriptList.ts | 10 +- www/app/api/schemas.gen.ts | 24 + www/app/api/services.gen.ts | 6 +- www/app/api/types.gen.ts | 4 + 7 files changed, 410 insertions(+), 200 deletions(-) diff --git a/server/reflector/db/transcripts.py b/server/reflector/db/transcripts.py index 2f8999c0..d9a9eaf9 100644 --- a/server/reflector/db/transcripts.py +++ b/server/reflector/db/transcripts.py @@ -291,6 +291,8 @@ class TranscriptController: order_by: str | None = None, filter_empty: bool | None = False, filter_recording: bool | None = False, + room_id: str | None = None, + search_term: str | None = None, return_query: bool = False, ) -> list[Transcript]: """ @@ -303,8 +305,36 @@ class TranscriptController: - `order_by`: field to order by, e.g. "-created_at" - `filter_empty`: filter out empty transcripts - `filter_recording`: filter out transcripts that are currently recording + - `room_id`: filter transcripts by room ID + - `search_term`: filter transcripts by search term """ - query = transcripts.select().where(transcripts.c.user_id == user_id) + from reflector.db.meetings import meetings + from reflector.db.rooms import rooms + + query = ( + transcripts.select() + .join(meetings, transcripts.c.meeting_id == meetings.c.id, isouter=True) + .join(rooms, meetings.c.room_id == rooms.c.id, isouter=True) + ) + + if user_id: + query = query.where(transcripts.c.user_id == user_id) + + if room_id: + query = query.where(rooms.c.id == room_id) + + if search_term: + query = query.where( + transcripts.c.title.ilike(f"%{search_term}%") + ) # Assuming there's a 'title' column + + query = query.with_only_columns( + [ + transcripts, + rooms.c.id.label("room_id"), + rooms.c.name.label("room_name"), + ] + ) if order_by is not None: field = getattr(transcripts.c, order_by[1:]) diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index c9551eef..cd531889 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -59,6 +59,8 @@ class GetTranscript(BaseModel): participants: list[TranscriptParticipant] | None reviewed: bool meeting_id: str | None + room_id: str | None + room_name: str | None class CreateTranscript(BaseModel): @@ -84,6 +86,8 @@ class DeletionStatus(BaseModel): @router.get("/transcripts", response_model=Page[GetTranscript]) async def transcripts_list( + room_id: str | None, + search_term: str | None, user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], ): from reflector.db import database @@ -101,6 +105,8 @@ async def transcripts_list( database, await transcripts_controller.get_all( user_id=user_id, + room_id=room_id, + search_term=search_term, order_by="-created_at", return_query=True, ), diff --git a/www/app/(app)/browse/page.tsx b/www/app/(app)/browse/page.tsx index 7b4b10dd..bc9cad09 100644 --- a/www/app/(app)/browse/page.tsx +++ b/www/app/(app)/browse/page.tsx @@ -1,50 +1,66 @@ "use client"; -import React, { useEffect, useState } from "react"; - -import { GetTranscript } from "../../api"; -import Pagination from "./pagination"; -import NextLink from "next/link"; -import { FaArrowRotateRight, FaGear } from "react-icons/fa6"; -import { FaCheck, FaTrash, FaStar, FaMicrophone } from "react-icons/fa"; -import { MdError } from "react-icons/md"; -import useTranscriptList from "../transcripts/useTranscriptList"; -import { formatTimeMs } from "../../lib/time"; -import useApi from "../../lib/useApi"; -import { useError } from "../../(errors)/errorContext"; -import { FaEllipsisVertical } from "react-icons/fa6"; -import useSessionUser from "../../lib/useSessionUser"; +import React, { useState, useEffect } from "react"; import { Flex, Spinner, Heading, - Button, - Card, - Link, - CardBody, - Stack, + Box, Text, + Link, + Stack, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + Button, + Divider, + Input, Icon, - Grid, - IconButton, - Spacer, + Tooltip, Menu, MenuButton, - MenuItem, MenuList, + MenuItem, + IconButton, AlertDialog, AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogBody, AlertDialogFooter, - Tooltip, + Spacer, } from "@chakra-ui/react"; -import { PlusSquareIcon } from "@chakra-ui/icons"; -import { ExpandableText } from "../../lib/expandableText"; +import { + FaCheck, + FaTrash, + FaStar, + FaMicrophone, + FaGear, + FaEllipsisVertical, + FaArrowRotateRight, +} from "react-icons/fa6"; +import useTranscriptList from "../transcripts/useTranscriptList"; +import useSessionUser from "../../lib/useSessionUser"; +import NextLink from "next/link"; +import { Room, GetTranscript } from "../../api"; +import Pagination from "./pagination"; +import { formatTimeMs } from "../../lib/time"; +import useApi from "../../lib/useApi"; +import { useError } from "../../(errors)/errorContext"; export default function TranscriptBrowser() { - const [page, setPage] = useState(1); - const { loading, response, refetch } = useTranscriptList(page); + const [selectedRoomId, setSelectedRoomId] = useState(""); + const [rooms, setRooms] = useState([]); + const [page, setPage] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const [searchInputValue, setSearchInputValue] = useState(""); + const { loading, response, refetch } = useTranscriptList( + page, + selectedRoomId, + searchTerm, + ); const userName = useSessionUser().name; const [deletionLoading, setDeletionLoading] = useState(false); const api = useApi(); @@ -58,6 +74,36 @@ export default function TranscriptBrowser() { setDeletedItemIds([]); }, [page, response]); + useEffect(() => { + refetch(); + }, [selectedRoomId, page, searchTerm]); + + useEffect(() => { + if (!api) return; + api + .v1RoomsList({ page: 1 }) + .then((rooms) => setRooms(rooms.items)) + .catch((err) => setError(err, "There was an error fetching the rooms")); + }, [api]); + + const handleFilterTranscripts = (roomId: string) => { + setSelectedRoomId(roomId); + setPage(1); + }; + + const handleSearch = () => { + setPage(1); + setSearchTerm(searchInputValue); + setSelectedRoomId(""); + refetch(); + }; + + const handleKeyDown = (event) => { + if (event.key === "Enter") { + handleSearch(); + } + }; + if (loading && !response) return ( @@ -77,6 +123,7 @@ export default function TranscriptBrowser() { ); + const onCloseDeletion = () => setTranscriptToDeleteId(undefined); const handleDeleteTranscript = (transcriptId) => (e) => { @@ -88,7 +135,6 @@ export default function TranscriptBrowser() { .then(() => { refetch(); setDeletionLoading(false); - refetch(); onCloseDeletion(); setDeletedItemIds((deletedItemIds) => [ deletedItemIds, @@ -120,169 +166,138 @@ export default function TranscriptBrowser() { }); } }; + return ( - - {userName ? ( - {userName}'s Meetings - ) : ( - Your meetings - )} - - {loading || (deletionLoading && )} - - - + + + {userName ? `${userName}'s Transcriptions` : "Your Transcriptions"}{" "} + {loading || (deletionLoading && )} + - - {response?.items - .filter((i) => !deletedItemIds?.includes(i.id)) - .map((item: GetTranscript) => ( - - - - - - {item.title || item.name || "Unnamed Transcript"} - - - - - } - aria-label="actions" - /> - - setTranscriptToDeleteId(item.id)} - icon={} - > - Delete - - } - > - Process - - - - - - Delete{" "} - {item.title || item.name || "Unnamed Transcript"} - + + + + handleFilterTranscripts("")} + color={selectedRoomId === "" ? "blue.500" : "gray.600"} + _hover={{ color: "blue.300" }} + fontWeight={selectedRoomId === "" ? "bold" : "normal"} + > + All Transcripts + - - Are you sure? You can't undo this action - afterwards. - + - - - - - - - - - - - - - {item.status == "ended" && ( - - - - - - )} - {item.status == "error" && ( - - - - - - )} - {item.status == "idle" && ( - - - - - - )} - {item.status == "processing" && ( - - - - - - )} - {item.status == "recording" && ( - - - - - - )} - + {rooms.length > 0 && ( + <> + + My Rooms + + + {rooms.map((room) => ( + handleFilterTranscripts(room.id)} + color={selectedRoomId === room.id ? "blue.500" : "gray.600"} + _hover={{ color: "blue.300" }} + fontWeight={selectedRoomId === room.id ? "bold" : "normal"} + ml={4} + > + {room.name} + + ))} + + )} + + + + + + setSearchInputValue(e.target.value)} + onKeyDown={handleKeyDown} + /> + + + + + + + + + + + + + + + {response?.items?.map((item: GetTranscript) => ( + + + + + + + + ))} + +
+ Transcription Title + RoomDateDuration
+ + {item.status === "ended" && ( + + + + + + )} + {item.status === "error" && ( + + + + + + )} + {item.status === "idle" && ( + + + + + + )} + {item.status === "processing" && ( + + + + + + )} + {item.status === "recording" && ( + + + + + + )} + + {item.title || "Unnamed Transcript"} + + + {item.room_name} {new Date(item.created_at).toLocaleString("en-US", { year: "numeric", month: "long", @@ -290,18 +305,141 @@ export default function TranscriptBrowser() { hour: "numeric", minute: "numeric", })} - {"\u00A0"}-{"\u00A0"} - {formatTimeMs(item.duration)} - + {formatTimeMs(item.duration)} + + } + variant="outline" + aria-label="Options" + /> + + + Delete + + + Reprocess + + + +
+
+ + + {response?.items?.map((item: GetTranscript) => ( + + + + {item.status === "ended" && ( + + + + + + )} + {item.status === "error" && ( + + + + + + )} + {item.status === "idle" && ( + + + + + + )} + {item.status === "processing" && ( + + + + + + )} + {item.status === "recording" && ( + + + + + + )} + + + + {item.title || "Unnamed Transcript"} + + Room: {item.room_name} + + Date: {new Date(item.created_at).toLocaleString()} + + Duration: {formatTimeMs(item.duration)} + + + } + variant="outline" + aria-label="Options" + /> + + + Delete + + + Reprocess + + + - - {item.short_summary} - - -
-
- ))} -
+ + ))} + + + +
+
+ + + + + + Delete Transcript + + + Are you sure? You can't undo this action afterwards. + + + + + + + + ); } diff --git a/www/app/(app)/transcripts/useTranscriptList.ts b/www/app/(app)/transcripts/useTranscriptList.ts index d0f62eee..ef61b8ba 100644 --- a/www/app/(app)/transcripts/useTranscriptList.ts +++ b/www/app/(app)/transcripts/useTranscriptList.ts @@ -10,7 +10,11 @@ type TranscriptList = { refetch: () => void; }; -const useTranscriptList = (page: number): TranscriptList => { +const useTranscriptList = ( + page: number, + roomId: string | null, + searchTerm: string | null, +): TranscriptList => { const [response, setResponse] = useState(null); const [loading, setLoading] = useState(true); const [error, setErrorState] = useState(null); @@ -27,7 +31,7 @@ const useTranscriptList = (page: number): TranscriptList => { if (!api) return; setLoading(true); api - .v1TranscriptsList({ page }) + .v1TranscriptsList({ page, roomId, searchTerm }) .then((response) => { setResponse(response); setLoading(false); @@ -38,7 +42,7 @@ const useTranscriptList = (page: number): TranscriptList => { setError(err); setErrorState(err); }); - }, [!api, page, refetchCount]); + }, [!api, page, refetchCount, roomId, searchTerm]); return { response, loading, error, refetch }; }; diff --git a/www/app/api/schemas.gen.ts b/www/app/api/schemas.gen.ts index 4327ba02..ec34c9e4 100644 --- a/www/app/api/schemas.gen.ts +++ b/www/app/api/schemas.gen.ts @@ -263,6 +263,28 @@ export const $GetTranscript = { ], title: "Meeting Id", }, + room_id: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Room Id", + }, + room_name: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Room Name", + }, }, type: "object", required: [ @@ -281,6 +303,8 @@ export const $GetTranscript = { "participants", "reviewed", "meeting_id", + "room_id", + "room_name", ], title: "GetTranscript", } as const; diff --git a/www/app/api/services.gen.ts b/www/app/api/services.gen.ts index 179d8bb6..9ddc1b8c 100644 --- a/www/app/api/services.gen.ts +++ b/www/app/api/services.gen.ts @@ -199,18 +199,22 @@ export class DefaultService { /** * Transcripts List * @param data The data for the request. + * @param data.roomId + * @param data.searchTerm * @param data.page Page number * @param data.size Page size * @returns Page_GetTranscript_ Successful Response * @throws ApiError */ public v1TranscriptsList( - data: V1TranscriptsListData = {}, + data: V1TranscriptsListData, ): CancelablePromise { return this.httpRequest.request({ method: "GET", url: "/v1/transcripts", query: { + room_id: data.roomId, + search_term: data.searchTerm, page: data.page, size: data.size, }, diff --git a/www/app/api/types.gen.ts b/www/app/api/types.gen.ts index 269b8a6b..ffdf5e4b 100644 --- a/www/app/api/types.gen.ts +++ b/www/app/api/types.gen.ts @@ -52,6 +52,8 @@ export type GetTranscript = { participants: Array | null; reviewed: boolean; meeting_id: string | null; + room_id: string | null; + room_name: string | null; }; export type GetTranscriptSegmentTopic = { @@ -274,6 +276,8 @@ export type V1TranscriptsListData = { * Page number */ page?: number; + roomId: string | null; + searchTerm: string | null; /** * Page size */ From 39d02ab265127f34c5ae824c49a75b5dcc279018 Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Fri, 4 Oct 2024 16:38:29 +0200 Subject: [PATCH 2/5] Add transcript source kind --- ...74b2b0236931_add_transcript_source_kind.py | 48 +++++++++++++ server/reflector/db/transcripts.py | 21 ++++++ server/reflector/views/transcripts.py | 5 ++ server/reflector/worker/process.py | 3 +- www/app/(app)/browse/page.tsx | 69 ++++++++++++++++--- .../(app)/transcripts/useTranscriptList.ts | 10 ++- www/app/api/schemas.gen.ts | 10 +++ www/app/api/services.gen.ts | 2 + www/app/api/types.gen.ts | 4 ++ 9 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 server/migrations/versions/74b2b0236931_add_transcript_source_kind.py diff --git a/server/migrations/versions/74b2b0236931_add_transcript_source_kind.py b/server/migrations/versions/74b2b0236931_add_transcript_source_kind.py new file mode 100644 index 00000000..3c4c5600 --- /dev/null +++ b/server/migrations/versions/74b2b0236931_add_transcript_source_kind.py @@ -0,0 +1,48 @@ +"""Add transcript source kind + +Revision ID: 74b2b0236931 +Revises: 0925da921477 +Create Date: 2024-10-04 14:19:23.625447 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "74b2b0236931" +down_revision: Union[str, None] = "0925da921477" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "transcript", + sa.Column( + "source_kind", + sa.Enum("ROOM", "LIVE", "FILE", name="sourcekind"), + nullable=True, + ), + ) + + op.execute( + "UPDATE transcript SET source_kind = 'room' WHERE meeting_id IS NOT NULL" + ) + op.execute("UPDATE transcript SET source_kind = 'live' WHERE meeting_id IS NULL") + + with op.batch_alter_table("transcript", schema=None) as batch_op: + batch_op.alter_column("source_kind", nullable=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("transcript", "source_kind") + + +# ### end Alembic commands ### diff --git a/server/reflector/db/transcripts.py b/server/reflector/db/transcripts.py index d9a9eaf9..2459b239 100644 --- a/server/reflector/db/transcripts.py +++ b/server/reflector/db/transcripts.py @@ -1,3 +1,4 @@ +import enum import json import os import shutil @@ -14,8 +15,16 @@ from reflector.db import database, metadata from reflector.processors.types import Word as ProcessorWord from reflector.settings import settings from reflector.storage import Storage +from sqlalchemy import Enum from sqlalchemy.sql import false + +class SourceKind(enum.StrEnum): + ROOM = enum.auto() + LIVE = enum.auto() + FILE = enum.auto() + + transcripts = sqlalchemy.Table( "transcript", metadata, @@ -55,6 +64,11 @@ transcripts = sqlalchemy.Table( sqlalchemy.String, ), sqlalchemy.Column("zulip_message_id", sqlalchemy.Integer, nullable=True), + sqlalchemy.Column( + "source_kind", + Enum(SourceKind, values_callable=lambda obj: [e.value for e in obj]), + nullable=False, + ), ) @@ -152,6 +166,7 @@ class Transcript(BaseModel): reviewed: bool = False meeting_id: str | None = None zulip_message_id: int | None = None + source_kind: SourceKind def add_event(self, event: str, data: BaseModel) -> TranscriptEvent: ev = TranscriptEvent(event=event, data=data.model_dump()) @@ -291,6 +306,7 @@ class TranscriptController: order_by: str | None = None, filter_empty: bool | None = False, filter_recording: bool | None = False, + source_kind: SourceKind | None = None, room_id: str | None = None, search_term: str | None = None, return_query: bool = False, @@ -320,6 +336,9 @@ class TranscriptController: if user_id: query = query.where(transcripts.c.user_id == user_id) + if source_kind: + query = query.where(transcripts.c.source_kind == source_kind) + if room_id: query = query.where(rooms.c.id == room_id) @@ -422,6 +441,7 @@ class TranscriptController: async def add( self, name: str, + source_kind: SourceKind, source_language: str = "en", target_language: str = "en", user_id: str | None = None, @@ -433,6 +453,7 @@ class TranscriptController: """ transcript = Transcript( name=name, + source_kind=source_kind, source_language=source_language, target_language=target_language, user_id=user_id, diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index cd531889..05d7bfb0 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -9,6 +9,7 @@ from jose import jwt from pydantic import BaseModel, Field from reflector.db.migrate_user import migrate_user from reflector.db.transcripts import ( + SourceKind, TranscriptParticipant, TranscriptTopic, transcripts_controller, @@ -61,6 +62,7 @@ class GetTranscript(BaseModel): meeting_id: str | None room_id: str | None room_name: str | None + source_kind: SourceKind class CreateTranscript(BaseModel): @@ -89,6 +91,7 @@ async def transcripts_list( room_id: str | None, search_term: str | None, user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], + source_kind: SourceKind | None = None, ): from reflector.db import database @@ -105,6 +108,7 @@ async def transcripts_list( database, await transcripts_controller.get_all( user_id=user_id, + source_kind=SourceKind(source_kind) if source_kind else None, room_id=room_id, search_term=search_term, order_by="-created_at", @@ -121,6 +125,7 @@ async def transcripts_create( user_id = user["sub"] if user else None return await transcripts_controller.add( info.name, + source_kind=SourceKind.LIVE, source_language=info.source_language, target_language=info.target_language, user_id=user_id, diff --git a/server/reflector/worker/process.py b/server/reflector/worker/process.py index 614f7c4c..fba09996 100644 --- a/server/reflector/worker/process.py +++ b/server/reflector/worker/process.py @@ -8,7 +8,7 @@ import structlog from celery import shared_task from celery.utils.log import get_task_logger from reflector.db.meetings import meetings_controller -from reflector.db.transcripts import transcripts_controller +from reflector.db.transcripts import SourceKind, transcripts_controller from reflector.pipelines.main_live_pipeline import asynctask, task_pipeline_process from reflector.settings import settings @@ -66,6 +66,7 @@ async def process_recording(bucket_name: str, object_key: str): meeting = await meetings_controller.get_by_room_name(room_name) transcript = await transcripts_controller.add( "", + source_kind=SourceKind.ROOM source_language="en", target_language="en", user_id=meeting.user_id, diff --git a/www/app/(app)/browse/page.tsx b/www/app/(app)/browse/page.tsx index bc9cad09..bc1a1254 100644 --- a/www/app/(app)/browse/page.tsx +++ b/www/app/(app)/browse/page.tsx @@ -49,8 +49,11 @@ import Pagination from "./pagination"; import { formatTimeMs } from "../../lib/time"; import useApi from "../../lib/useApi"; import { useError } from "../../(errors)/errorContext"; +import { SourceKind } from "../../api"; export default function TranscriptBrowser() { + const [selectedSourceKind, setSelectedSourceKind] = + useState(null); const [selectedRoomId, setSelectedRoomId] = useState(""); const [rooms, setRooms] = useState([]); const [page, setPage] = useState(1); @@ -58,6 +61,7 @@ export default function TranscriptBrowser() { const [searchInputValue, setSearchInputValue] = useState(""); const { loading, response, refetch } = useTranscriptList( page, + selectedSourceKind, selectedRoomId, searchTerm, ); @@ -86,7 +90,11 @@ export default function TranscriptBrowser() { .catch((err) => setError(err, "There was an error fetching the rooms")); }, [api]); - const handleFilterTranscripts = (roomId: string) => { + const handleFilterTranscripts = ( + sourceKind: SourceKind | null, + roomId: string, + ) => { + setSelectedSourceKind(sourceKind); setSelectedRoomId(roomId); setPage(1); }; @@ -187,10 +195,10 @@ export default function TranscriptBrowser() { handleFilterTranscripts("")} - color={selectedRoomId === "" ? "blue.500" : "gray.600"} + onClick={() => handleFilterTranscripts(null, "")} + color={selectedSourceKind === null ? "blue.500" : "gray.600"} _hover={{ color: "blue.300" }} - fontWeight={selectedRoomId === "" ? "bold" : "normal"} + fontWeight={selectedSourceKind === null ? "bold" : "normal"} > All Transcripts @@ -208,10 +216,20 @@ export default function TranscriptBrowser() { key={room.id} as={NextLink} href="#" - onClick={() => handleFilterTranscripts(room.id)} - color={selectedRoomId === room.id ? "blue.500" : "gray.600"} + onClick={() => handleFilterTranscripts("room", room.id)} + color={ + selectedSourceKind === "room" && + selectedRoomId === room.id + ? "blue.500" + : "gray.600" + } _hover={{ color: "blue.300" }} - fontWeight={selectedRoomId === room.id ? "bold" : "normal"} + fontWeight={ + selectedSourceKind === "room" && + selectedRoomId === room.id + ? "bold" + : "normal" + } ml={4} > {room.name} @@ -219,6 +237,28 @@ export default function TranscriptBrowser() { ))} )} + + + handleFilterTranscripts("live", "")} + color={selectedSourceKind === "live" ? "blue.500" : "gray.600"} + _hover={{ color: "blue.300" }} + fontWeight={selectedSourceKind === "live" ? "bold" : "normal"} + > + Live Transcripts + + handleFilterTranscripts("file", "")} + color={selectedSourceKind === "file" ? "blue.500" : "gray.600"} + _hover={{ color: "blue.300" }} + fontWeight={selectedSourceKind === "file" ? "bold" : "normal"} + > + Uploaded Files + @@ -241,7 +281,7 @@ export default function TranscriptBrowser() { Transcription Title - Room + Source Date Duration @@ -296,7 +336,11 @@ export default function TranscriptBrowser() { - {item.room_name} + + {item.source_kind === "room" + ? item.room_name + : item.source_kind} + {new Date(item.created_at).toLocaleString("en-US", { year: "numeric", @@ -376,7 +420,12 @@ export default function TranscriptBrowser() { {item.title || "Unnamed Transcript"} - Room: {item.room_name} + + Source:{" "} + {item.source_kind === "room" + ? item.room_name + : item.source_kind} + Date: {new Date(item.created_at).toLocaleString()} diff --git a/www/app/(app)/transcripts/useTranscriptList.ts b/www/app/(app)/transcripts/useTranscriptList.ts index ef61b8ba..e7bf2dd0 100644 --- a/www/app/(app)/transcripts/useTranscriptList.ts +++ b/www/app/(app)/transcripts/useTranscriptList.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useError } from "../../(errors)/errorContext"; import useApi from "../../lib/useApi"; -import { Page_GetTranscript_ } from "../../api"; +import { Page_GetTranscript_, SourceKind } from "../../api"; type TranscriptList = { response: Page_GetTranscript_ | null; @@ -12,6 +12,7 @@ type TranscriptList = { const useTranscriptList = ( page: number, + sourceKind: SourceKind | null, roomId: string | null, searchTerm: string | null, ): TranscriptList => { @@ -31,7 +32,12 @@ const useTranscriptList = ( if (!api) return; setLoading(true); api - .v1TranscriptsList({ page, roomId, searchTerm }) + .v1TranscriptsList({ + page, + sourceKind, + roomId, + searchTerm, + }) .then((response) => { setResponse(response); setLoading(false); diff --git a/www/app/api/schemas.gen.ts b/www/app/api/schemas.gen.ts index ec34c9e4..1e11600c 100644 --- a/www/app/api/schemas.gen.ts +++ b/www/app/api/schemas.gen.ts @@ -285,6 +285,9 @@ export const $GetTranscript = { ], title: "Room Name", }, + source_kind: { + $ref: "#/components/schemas/SourceKind", + }, }, type: "object", required: [ @@ -305,6 +308,7 @@ export const $GetTranscript = { "meeting_id", "room_id", "room_name", + "source_kind", ], title: "GetTranscript", } as const; @@ -766,6 +770,12 @@ export const $RtcOffer = { title: "RtcOffer", } as const; +export const $SourceKind = { + type: "string", + enum: ["room", "live", "file"], + title: "SourceKind", +} as const; + export const $SpeakerAssignment = { properties: { speaker: { diff --git a/www/app/api/services.gen.ts b/www/app/api/services.gen.ts index 9ddc1b8c..f3e07a30 100644 --- a/www/app/api/services.gen.ts +++ b/www/app/api/services.gen.ts @@ -201,6 +201,7 @@ export class DefaultService { * @param data The data for the request. * @param data.roomId * @param data.searchTerm + * @param data.sourceKind * @param data.page Page number * @param data.size Page size * @returns Page_GetTranscript_ Successful Response @@ -215,6 +216,7 @@ export class DefaultService { query: { room_id: data.roomId, search_term: data.searchTerm, + source_kind: data.sourceKind, page: data.page, size: data.size, }, diff --git a/www/app/api/types.gen.ts b/www/app/api/types.gen.ts index ffdf5e4b..de3167cb 100644 --- a/www/app/api/types.gen.ts +++ b/www/app/api/types.gen.ts @@ -54,6 +54,7 @@ export type GetTranscript = { meeting_id: string | null; room_id: string | null; room_name: string | null; + source_kind: SourceKind; }; export type GetTranscriptSegmentTopic = { @@ -149,6 +150,8 @@ export type RtcOffer = { type: string; }; +export type SourceKind = "room" | "live" | "file"; + export type SpeakerAssignment = { speaker?: number | null; participant?: string | null; @@ -282,6 +285,7 @@ export type V1TranscriptsListData = { * Page size */ size?: number; + sourceKind?: SourceKind | null; }; export type V1TranscriptsListResponse = Page_GetTranscript_; From ecb91bedc3a47c828ba9a0dcc7e6c385787f16e7 Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Fri, 4 Oct 2024 17:09:59 +0200 Subject: [PATCH 3/5] Add shared rooms --- .../versions/a7122bc0b2ca_add_shared_rooms.py | 40 +++++++++ server/reflector/db/rooms.py | 6 ++ server/reflector/views/rooms.py | 4 + www/app/(app)/browse/page.tsx | 43 ++++++++-- www/app/(app)/rooms/page.tsx | 82 +++++++++++++++++-- www/app/api/schemas.gen.ts | 5 ++ www/app/api/types.gen.ts | 1 + 7 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 server/migrations/versions/a7122bc0b2ca_add_shared_rooms.py diff --git a/server/migrations/versions/a7122bc0b2ca_add_shared_rooms.py b/server/migrations/versions/a7122bc0b2ca_add_shared_rooms.py new file mode 100644 index 00000000..996f5722 --- /dev/null +++ b/server/migrations/versions/a7122bc0b2ca_add_shared_rooms.py @@ -0,0 +1,40 @@ +"""Add shared rooms + +Revision ID: a7122bc0b2ca +Revises: 74b2b0236931 +Create Date: 2024-10-04 16:41:28.841889 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "a7122bc0b2ca" +down_revision: Union[str, None] = "74b2b0236931" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "room", + sa.Column( + "is_shared", + sa.Boolean(), + server_default=sa.text("0"), + nullable=False, + ), + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("room", "is_shared") + + # ### end Alembic commands ### diff --git a/server/reflector/db/rooms.py b/server/reflector/db/rooms.py index 5dc09542..09eb53b5 100644 --- a/server/reflector/db/rooms.py +++ b/server/reflector/db/rooms.py @@ -36,6 +36,9 @@ rooms = sqlalchemy.Table( nullable=False, server_default="automatic-2nd-participant", ), + sqlalchemy.Column( + "is_shared", sqlalchemy.Boolean, nullable=False, server_default=false() + ), ) @@ -53,6 +56,7 @@ class Room(BaseModel): recording_trigger: Literal[ "none", "prompt", "automatic", "automatic-2nd-participant" ] = "automatic-2nd-participant" + is_shared: bool = False class RoomController: @@ -98,6 +102,7 @@ class RoomController: room_mode: str, recording_type: str, recording_trigger: str, + is_shared: bool, ): """ Add a new room @@ -112,6 +117,7 @@ class RoomController: room_mode=room_mode, recording_type=recording_type, recording_trigger=recording_trigger, + is_shared=is_shared, ) query = rooms.insert().values(**room.model_dump()) try: diff --git a/server/reflector/views/rooms.py b/server/reflector/views/rooms.py index 50570ff7..03a73450 100644 --- a/server/reflector/views/rooms.py +++ b/server/reflector/views/rooms.py @@ -28,6 +28,7 @@ class Room(BaseModel): room_mode: str recording_type: str recording_trigger: str + is_shared: bool class Meeting(BaseModel): @@ -49,6 +50,7 @@ class CreateRoom(BaseModel): room_mode: str recording_type: str recording_trigger: str + is_shared: bool class UpdateRoom(BaseModel): @@ -60,6 +62,7 @@ class UpdateRoom(BaseModel): room_mode: str recording_type: str recording_trigger: str + is_shared: bool class DeletionStatus(BaseModel): @@ -100,6 +103,7 @@ async def rooms_create( room_mode=room.room_mode, recording_type=room.recording_type, recording_trigger=room.recording_trigger, + is_shared=room.is_shared, ) diff --git a/www/app/(app)/browse/page.tsx b/www/app/(app)/browse/page.tsx index bc1a1254..5595cdcd 100644 --- a/www/app/(app)/browse/page.tsx +++ b/www/app/(app)/browse/page.tsx @@ -74,6 +74,9 @@ export default function TranscriptBrowser() { React.useState(); const [deletedItemIds, setDeletedItemIds] = React.useState(); + const myRooms = rooms.filter((room) => !room.is_shared); + const sharedRooms = rooms.filter((room) => room.is_shared); + useEffect(() => { setDeletedItemIds([]); }, [page, response]); @@ -102,6 +105,7 @@ export default function TranscriptBrowser() { const handleSearch = () => { setPage(1); setSearchTerm(searchInputValue); + setSelectedSourceKind(null); setSelectedRoomId(""); refetch(); }; @@ -205,13 +209,42 @@ export default function TranscriptBrowser() { - {rooms.length > 0 && ( + {myRooms.length > 0 && ( <> - - My Rooms - + My Rooms - {rooms.map((room) => ( + {myRooms.map((room) => ( + handleFilterTranscripts("room", room.id)} + color={ + selectedSourceKind === "room" && + selectedRoomId === room.id + ? "blue.500" + : "gray.600" + } + _hover={{ color: "blue.300" }} + fontWeight={ + selectedSourceKind === "room" && + selectedRoomId === room.id + ? "bold" + : "normal" + } + ml={4} + > + {room.name} + + ))} + + )} + + {sharedRooms.length > 0 && ( + <> + Shared Rooms + + {sharedRooms.map((room) => ( !roomData.is_shared) || []; + const sharedRooms = + response?.items.filter((roomData) => roomData.is_shared) || []; + if (loading && !response) return ( @@ -375,6 +383,15 @@ export default function RoomsList() { isDisabled={!room.zulipAutoPost} /> + + + Shared room + + @@ -396,9 +413,10 @@ export default function RoomsList() { - - {response?.items && response.items.length > 0 ? ( - response.items.map((roomData) => ( + + My Rooms + {myRooms.length > 0 ? ( + myRooms.map((roomData) => ( @@ -445,9 +463,61 @@ export default function RoomsList() { )) ) : ( - - No rooms found - + No rooms found + )} + + + + Shared Rooms + {sharedRooms.length > 0 ? ( + sharedRooms.map((roomData) => ( + + + + + {roomData.name} + + + {linkCopied === roomData.name ? ( + + Link copied! + + ) : ( + } + onClick={() => handleCopyUrl(roomData.name)} + mr={2} + /> + )} + + + } + aria-label="actions" + /> + + handleEditRoom(roomData.id, roomData)} + icon={} + > + Edit + + handleDeleteRoom(roomData.id)} + icon={} + > + Delete + + + + + + + )) + ) : ( + No shared rooms found )} diff --git a/www/app/api/schemas.gen.ts b/www/app/api/schemas.gen.ts index 1e11600c..d0ada862 100644 --- a/www/app/api/schemas.gen.ts +++ b/www/app/api/schemas.gen.ts @@ -736,6 +736,10 @@ export const $Room = { type: "string", title: "Recording Trigger", }, + is_shared: { + type: "boolean", + title: "Is Shared", + }, }, type: "object", required: [ @@ -750,6 +754,7 @@ export const $Room = { "room_mode", "recording_type", "recording_trigger", + "is_shared", ], title: "Room", } as const; diff --git a/www/app/api/types.gen.ts b/www/app/api/types.gen.ts index de3167cb..ba00e0a0 100644 --- a/www/app/api/types.gen.ts +++ b/www/app/api/types.gen.ts @@ -143,6 +143,7 @@ export type Room = { room_mode: string; recording_type: string; recording_trigger: string; + is_shared: boolean; }; export type RtcOffer = { From bc33c6fb88ddcd1d5cb207d1ba9ef7aa6240dada Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Fri, 4 Oct 2024 18:24:45 +0200 Subject: [PATCH 4/5] Fix validation --- server/reflector/views/transcripts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index 05d7bfb0..f51f5aca 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -60,9 +60,9 @@ class GetTranscript(BaseModel): participants: list[TranscriptParticipant] | None reviewed: bool meeting_id: str | None - room_id: str | None - room_name: str | None source_kind: SourceKind + room_id: str | None = None + room_name: str | None = None class CreateTranscript(BaseModel): @@ -88,10 +88,10 @@ class DeletionStatus(BaseModel): @router.get("/transcripts", response_model=Page[GetTranscript]) async def transcripts_list( - room_id: str | None, - search_term: str | None, user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], source_kind: SourceKind | None = None, + room_id: str | None = None, + search_term: str | None = None, ): from reflector.db import database From f1031f93d8122ad20f13f365579bee907a2557dc Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Fri, 4 Oct 2024 18:27:47 +0200 Subject: [PATCH 5/5] Fix processing --- server/reflector/worker/process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/reflector/worker/process.py b/server/reflector/worker/process.py index fba09996..c0281737 100644 --- a/server/reflector/worker/process.py +++ b/server/reflector/worker/process.py @@ -66,7 +66,7 @@ async def process_recording(bucket_name: str, object_key: str): meeting = await meetings_controller.get_by_room_name(room_name) transcript = await transcripts_controller.add( "", - source_kind=SourceKind.ROOM + source_kind=SourceKind.ROOM, source_language="en", target_language="en", user_id=meeting.user_id,