"use client"; import React, { useState, useEffect, useMemo } from "react"; import { Flex, Spinner, Heading, Text, Link, Box, Stack, Input, Button, IconButton, } from "@chakra-ui/react"; import { useQueryState, parseAsString, parseAsInteger, parseAsStringLiteral, } from "nuqs"; import { LuX } from "react-icons/lu"; import type { components } from "../../reflector-api"; type Room = components["schemas"]["Room"]; type SourceKind = components["schemas"]["SourceKind"]; type SearchResult = components["schemas"]["SearchResult"]; import { useRoomsList, useTranscriptsSearch, useTranscriptDelete, useTranscriptProcess, } from "../../lib/apiHooks"; import FilterSidebar from "./_components/FilterSidebar"; import Pagination, { FIRST_PAGE, PaginationPage, parsePaginationPage, totalPages as getTotalPages, paginationPageTo0Based, } from "./_components/Pagination"; import TranscriptCards from "./_components/TranscriptCards"; import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog"; import { formatLocalDate } from "../../lib/time"; import { RECORD_A_MEETING_URL } from "../../api/urls"; import { useUserName } from "../../lib/useUserName"; const SEARCH_FORM_QUERY_INPUT_NAME = "query" as const; const SearchForm: React.FC<{ setPage: (page: PaginationPage) => void; sourceKind: SourceKind | null; roomId: string | null; setSourceKind: (sourceKind: SourceKind | null) => void; setRoomId: (roomId: string | null) => void; rooms: Room[]; searchQuery: string | null; setSearchQuery: (query: string | null) => void; }> = ({ setPage, sourceKind, roomId, setRoomId, setSourceKind, rooms, searchQuery, setSearchQuery, }) => { const [searchInputValue, setSearchInputValue] = useState(searchQuery || ""); const handleSearchQuerySubmit = async (d: FormData) => { await setSearchQuery((d.get(SEARCH_FORM_QUERY_INPUT_NAME) as string) || ""); }; const handleClearSearch = () => { setSearchInputValue(""); setSearchQuery(null); setPage(FIRST_PAGE); }; return (
setSearchInputValue(e.target.value)} name={SEARCH_FORM_QUERY_INPUT_NAME} pr={searchQuery ? "2.5rem" : undefined} /> {searchQuery && ( )}
); }; const UnderSearchFormFilterIndicators: React.FC<{ sourceKind: SourceKind | null; roomId: string | null; setSourceKind: (sourceKind: SourceKind | null) => void; setRoomId: (roomId: string | null) => void; rooms: Room[]; }> = ({ sourceKind, roomId, setRoomId, setSourceKind, rooms }) => { return ( <> {(sourceKind || roomId) && ( Active filters: {sourceKind && ( {roomId ? `Room: ${ rooms.find((r) => r.id === roomId)?.name || roomId }` : `Source: ${sourceKind}`} )} )} ); }; const EmptyResult: React.FC<{ searchQuery: string; }> = ({ searchQuery }) => { return ( {searchQuery ? `No results found for "${searchQuery}". Try adjusting your search terms.` : "No transcripts found, but you can "} {!searchQuery && ( <> record a meeting {" to get started."} )} ); }; export default function TranscriptBrowser() { const [urlSearchQuery, setUrlSearchQuery] = useQueryState( "q", parseAsString.withDefault("").withOptions({ shallow: false }), ); const [urlSourceKind, setUrlSourceKind] = useQueryState( "source", parseAsStringLiteral([ "room", "live", "file", ] as const satisfies SourceKind[]).withOptions({ shallow: false, }), ); const [urlRoomId, setUrlRoomId] = useQueryState( "room", parseAsString.withDefault("").withOptions({ shallow: false }), ); const [urlPage, setPage] = useQueryState( "page", parseAsInteger.withDefault(1).withOptions({ shallow: false }), ); const [page, _setSafePage] = useState(FIRST_PAGE); // safety net useEffect(() => { const maybePage = parsePaginationPage(urlPage); if ("error" in maybePage) { setPage(FIRST_PAGE).then(() => {}); return; } _setSafePage(maybePage.value); }, [urlPage]); const pageSize = 20; // must be json-able const searchFilters = useMemo( () => ({ q: urlSearchQuery, extras: { room_id: urlRoomId || undefined, source_kind: urlSourceKind || undefined, }, }), [urlSearchQuery, urlRoomId, urlSourceKind], ); const { data: searchData, isLoading: searchLoading, refetch: reloadSearch, } = useTranscriptsSearch(searchFilters.q, { limit: pageSize, offset: paginationPageTo0Based(page) * pageSize, ...searchFilters.extras, }); const results = searchData?.results || []; const totalResults = searchData?.total || 0; // Fetch rooms const { data: roomsData } = useRoomsList(1); const rooms = roomsData?.items || []; const totalPages = getTotalPages(totalResults, pageSize); // reset pagination when search results change (detected by total change; good enough approximation) useEffect(() => { // operation is idempotent setPage(FIRST_PAGE).then(() => {}); }, [JSON.stringify(searchFilters)]); const userName = useUserName(); const [deletionLoading, setDeletionLoading] = useState(false); const cancelRef = React.useRef(null); const [transcriptToDeleteId, setTranscriptToDeleteId] = React.useState(); const handleFilterTranscripts = ( sourceKind: SourceKind | null, roomId: string, ) => { setUrlSourceKind(sourceKind); setUrlRoomId(roomId); setPage(1); }; const onCloseDeletion = () => setTranscriptToDeleteId(undefined); const deleteTranscript = useTranscriptDelete(); const processTranscript = useTranscriptProcess(); const confirmDeleteTranscript = (transcriptId: string) => { if (deletionLoading) return; setDeletionLoading(true); deleteTranscript.mutate( { params: { path: { transcript_id: transcriptId }, }, }, { onSuccess: () => { setDeletionLoading(false); onCloseDeletion(); reloadSearch(); }, onError: () => { setDeletionLoading(false); }, }, ); }; const handleProcessTranscript = (transcriptId: string) => { processTranscript.mutate({ params: { path: { transcript_id: transcriptId }, }, }); }; const transcriptToDelete = results?.find( (i) => i.id === transcriptToDeleteId, ); const dialogTitle = transcriptToDelete?.title || "Unnamed Transcript"; const dialogDate = transcriptToDelete?.created_at ? formatLocalDate(transcriptToDelete.created_at) : undefined; const dialogSource = transcriptToDelete?.source_kind === "room" && transcriptToDelete?.room_id ? transcriptToDelete.room_name || transcriptToDelete.room_id : transcriptToDelete?.source_kind; if (searchLoading && results.length === 0) { return ( ); } return ( {userName ? `${userName}'s Transcriptions` : "Your Transcriptions"}{" "} {(searchLoading || deletionLoading) && } {totalPages > 1 ? ( ) : null} {!searchLoading && results.length === 0 && ( )} transcriptToDeleteId && confirmDeleteTranscript(transcriptToDeleteId) } cancelRef={cancelRef} isLoading={deletionLoading} title={dialogTitle} date={dialogDate} source={dialogSource} /> ); }