Files
reflector/www/app/(app)/browse/page.tsx
Mathieu Virbel fc38345d65 fix: separate browsing page into different components, limit to 10 by default (#498)
* feat: limit the amount of transcripts to 10 by default

* feat: separate page into different component, greatly improving the
loading and reactivity

* fix: current implementation immediately invokes the onDelete and
onReprocess

From pr-agent-monadical: Suggestion: The current implementation
immediately invokes the onDelete and onReprocess functions when the
component renders, rather than when the menu items are clicked. This can
cause unexpected behavior and potential memory leaks. Use callback
functions that only execute when the menu items are actually clicked.
[possible issue, importance: 9]
2025-07-17 20:18:00 -06:00

190 lines
5.6 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Flex, Spinner, Heading, Text, Link } from "@chakra-ui/react";
import useTranscriptList from "../transcripts/useTranscriptList";
import useSessionUser from "../../lib/useSessionUser";
import { Room } from "../../api";
import Pagination from "./pagination";
import useApi from "../../lib/useApi";
import { useError } from "../../(errors)/errorContext";
import { SourceKind } from "../../api";
import FilterSidebar from "./_components/FilterSidebar";
import SearchBar from "./_components/SearchBar";
import TranscriptTable from "./_components/TranscriptTable";
import TranscriptCards from "./_components/TranscriptCards";
import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog";
export default function TranscriptBrowser() {
const [selectedSourceKind, setSelectedSourceKind] =
useState<SourceKind | null>(null);
const [selectedRoomId, setSelectedRoomId] = useState("");
const [rooms, setRooms] = useState<Room[]>([]);
const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const { loading, response, refetch } = useTranscriptList(
page,
selectedSourceKind,
selectedRoomId,
searchTerm,
);
const userName = useSessionUser().name;
const [deletionLoading, setDeletionLoading] = useState(false);
const api = useApi();
const { setError } = useError();
const cancelRef = React.useRef(null);
const [transcriptToDeleteId, setTranscriptToDeleteId] =
React.useState<string>();
const [deletedItemIds, setDeletedItemIds] = React.useState<string[]>();
useEffect(() => {
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 = (
sourceKind: SourceKind | null,
roomId: string,
) => {
setSelectedSourceKind(sourceKind);
setSelectedRoomId(roomId);
setPage(1);
refetch();
};
const handleSearch = (searchTerm: string) => {
setPage(1);
setSearchTerm(searchTerm);
setSelectedSourceKind(null);
setSelectedRoomId("");
refetch();
};
if (loading && !response)
return (
<Flex flexDir="column" align="center" justify="center" h="100%">
<Spinner size="xl" />
</Flex>
);
if (!loading && !response)
return (
<Flex flexDir="column" align="center" justify="center" h="100%">
<Text>
No transcripts found, but you can&nbsp;
<Link href="/transcripts/new" className="underline">
record a meeting
</Link>
&nbsp;to get started.
</Text>
</Flex>
);
const onCloseDeletion = () => setTranscriptToDeleteId(undefined);
const handleDeleteTranscript = (transcriptId) => (e) => {
e.stopPropagation();
if (api && !deletionLoading) {
setDeletionLoading(true);
api
.v1TranscriptDelete({ transcriptId })
.then(() => {
refetch();
setDeletionLoading(false);
onCloseDeletion();
setDeletedItemIds((deletedItemIds) => [
deletedItemIds,
...transcriptId,
]);
})
.catch((err) => {
setDeletionLoading(false);
setError(err, "There was an error deleting the transcript");
});
}
};
const handleProcessTranscript = (transcriptId) => (e) => {
if (api) {
api
.v1TranscriptProcess({ transcriptId })
.then((result) => {
const status = (result as any).status;
if (status === "already running") {
setError(
new Error("Processing is already running, please wait"),
"Processing is already running, please wait",
);
}
})
.catch((err) => {
setError(err, "There was an error processing the transcript");
});
}
};
return (
<Flex
flexDir="column"
w={{ base: "full", md: "container.xl" }}
mx="auto"
p={4}
>
<Flex flexDir="row" justify="space-between" align="center" mb={4}>
<Heading size="md">
{userName ? `${userName}'s Transcriptions` : "Your Transcriptions"}{" "}
{loading || (deletionLoading && <Spinner size="sm" />)}
</Heading>
</Flex>
<Flex flexDir={{ base: "column", md: "row" }}>
<FilterSidebar
rooms={rooms}
selectedSourceKind={selectedSourceKind}
selectedRoomId={selectedRoomId}
onFilterChange={handleFilterTranscripts}
/>
<Flex flexDir="column" flex="1" p={4} gap={4}>
<SearchBar onSearch={handleSearch} />
<Pagination
page={page}
setPage={setPage}
total={response?.total || 0}
size={response?.size || 0}
/>
<TranscriptTable
transcripts={response?.items || []}
onDelete={handleDeleteTranscript}
onReprocess={handleProcessTranscript}
loading={loading}
/>
<TranscriptCards
transcripts={response?.items || []}
onDelete={handleDeleteTranscript}
onReprocess={handleProcessTranscript}
loading={loading}
/>
</Flex>
</Flex>
<DeleteTranscriptDialog
isOpen={!!transcriptToDeleteId}
onClose={onCloseDeletion}
onConfirm={() => handleDeleteTranscript(transcriptToDeleteId)(null)}
cancelRef={cancelRef}
/>
</Flex>
);
}