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 */