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]
This commit is contained in:
2025-07-17 19:42:09 -06:00
parent 5a1d662dc4
commit fc38345d65
9 changed files with 520 additions and 362 deletions

View File

@@ -3,7 +3,7 @@ from typing import Annotated, Literal, Optional
import reflector.auth as auth import reflector.auth as auth
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi_pagination import Page from fastapi_pagination import Page, Params
from fastapi_pagination.ext.databases import paginate from fastapi_pagination.ext.databases import paginate
from jose import jwt from jose import jwt
from pydantic import BaseModel, Field, field_serializer from pydantic import BaseModel, Field, field_serializer
@@ -128,6 +128,7 @@ async def transcripts_list(
order_by="-created_at", order_by="-created_at",
return_query=True, return_query=True,
), ),
params=Params(size=10),
) )

View File

@@ -0,0 +1,51 @@
import React from "react";
import {
Button,
AlertDialog,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogBody,
AlertDialogFooter,
} from "@chakra-ui/react";
interface DeleteTranscriptDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
cancelRef: React.RefObject<any>;
}
export default function DeleteTranscriptDialog({
isOpen,
onClose,
onConfirm,
cancelRef,
}: DeleteTranscriptDialogProps) {
return (
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Delete Transcript
</AlertDialogHeader>
<AlertDialogBody>
Are you sure? You can't undo this action afterwards.
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
Cancel
</Button>
<Button colorScheme="red" onClick={onConfirm} ml={3}>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
}

View File

@@ -0,0 +1,120 @@
import React from "react";
import { Box, Stack, Link, Heading, Divider } from "@chakra-ui/react";
import NextLink from "next/link";
import { Room, SourceKind } from "../../../api";
interface FilterSidebarProps {
rooms: Room[];
selectedSourceKind: SourceKind | null;
selectedRoomId: string;
onFilterChange: (sourceKind: SourceKind | null, roomId: string) => void;
}
export default function FilterSidebar({
rooms,
selectedSourceKind,
selectedRoomId,
onFilterChange,
}: FilterSidebarProps) {
const myRooms = rooms.filter((room) => !room.is_shared);
const sharedRooms = rooms.filter((room) => room.is_shared);
return (
<Box w={{ base: "full", md: "300px" }} p={4} bg="gray.100">
<Stack spacing={3}>
<Link
as={NextLink}
href="#"
onClick={() => onFilterChange(null, "")}
color={selectedSourceKind === null ? "blue.500" : "gray.600"}
_hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === null ? "bold" : "normal"}
>
All Transcripts
</Link>
<Divider />
{myRooms.length > 0 && (
<>
<Heading size="sm">My Rooms</Heading>
{myRooms.map((room) => (
<Link
key={room.id}
as={NextLink}
href="#"
onClick={() => onFilterChange("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}
</Link>
))}
</>
)}
{sharedRooms.length > 0 && (
<>
<Heading size="sm">Shared Rooms</Heading>
{sharedRooms.map((room) => (
<Link
key={room.id}
as={NextLink}
href="#"
onClick={() => onFilterChange("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}
</Link>
))}
</>
)}
<Divider />
<Link
as={NextLink}
href="#"
onClick={() => onFilterChange("live", "")}
color={selectedSourceKind === "live" ? "blue.500" : "gray.600"}
_hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === "live" ? "bold" : "normal"}
>
Live Transcripts
</Link>
<Link
as={NextLink}
href="#"
onClick={() => onFilterChange("file", "")}
color={selectedSourceKind === "file" ? "blue.500" : "gray.600"}
_hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === "file" ? "bold" : "normal"}
>
Uploaded Files
</Link>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,34 @@
import React, { useState } from "react";
import { Flex, Input, Button } from "@chakra-ui/react";
interface SearchBarProps {
onSearch: (searchTerm: string) => void;
}
export default function SearchBar({ onSearch }: SearchBarProps) {
const [searchInputValue, setSearchInputValue] = useState("");
const handleSearch = () => {
onSearch(searchInputValue);
};
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Enter") {
handleSearch();
}
};
return (
<Flex mb={4} alignItems="center">
<Input
placeholder="Search transcriptions..."
value={searchInputValue}
onChange={(e) => setSearchInputValue(e.target.value)}
onKeyDown={handleKeyDown}
/>
<Button ml={2} onClick={handleSearch}>
Search
</Button>
</Flex>
);
}

View File

@@ -0,0 +1,39 @@
import React from "react";
import {
Menu,
MenuButton,
MenuList,
MenuItem,
IconButton,
Icon,
} from "@chakra-ui/react";
import { FaEllipsisVertical } from "react-icons/fa6";
interface TranscriptActionsMenuProps {
transcriptId: string;
onDelete: (transcriptId: string) => (e: any) => void;
onReprocess: (transcriptId: string) => (e: any) => void;
}
export default function TranscriptActionsMenu({
transcriptId,
onDelete,
onReprocess,
}: TranscriptActionsMenuProps) {
return (
<Menu closeOnSelect={true} isLazy={true}>
<MenuButton
as={IconButton}
icon={<Icon as={FaEllipsisVertical} />}
variant="outline"
aria-label="Options"
/>
<MenuList>
<MenuItem onClick={(e) => onDelete(transcriptId)(e)}>Delete</MenuItem>
<MenuItem onClick={(e) => onReprocess(transcriptId)(e)}>
Reprocess
</MenuItem>
</MenuList>
</Menu>
);
}

View File

@@ -0,0 +1,81 @@
import React from "react";
import { Box, Stack, Text, Flex, Link, Spinner } from "@chakra-ui/react";
import NextLink from "next/link";
import { GetTranscriptMinimal } from "../../../api";
import { formatTimeMs, formatLocalDate } from "../../../lib/time";
import TranscriptStatusIcon from "./TranscriptStatusIcon";
import TranscriptActionsMenu from "./TranscriptActionsMenu";
interface TranscriptCardsProps {
transcripts: GetTranscriptMinimal[];
onDelete: (transcriptId: string) => (e: any) => void;
onReprocess: (transcriptId: string) => (e: any) => void;
loading?: boolean;
}
export default function TranscriptCards({
transcripts,
onDelete,
onReprocess,
loading,
}: TranscriptCardsProps) {
return (
<Box display={{ base: "block", md: "none" }} position="relative">
{loading && (
<Flex
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
bg="rgba(255, 255, 255, 0.8)"
zIndex={10}
align="center"
justify="center"
>
<Spinner size="xl" color="gray.700" thickness="4px" />
</Flex>
)}
<Box
opacity={loading ? 0.9 : 1}
pointerEvents={loading ? "none" : "auto"}
transition="opacity 0.2s ease-in-out"
>
<Stack spacing={2}>
{transcripts.map((item) => (
<Box key={item.id} borderWidth={1} p={4} borderRadius="md">
<Flex justify="space-between" alignItems="flex-start" gap="2">
<Box>
<TranscriptStatusIcon status={item.status} />
</Box>
<Box flex="1">
<Link
as={NextLink}
href={`/transcripts/${item.id}`}
fontWeight="bold"
display="block"
>
{item.title || "Unnamed Transcript"}
</Link>
<Text>
Source:{" "}
{item.source_kind === "room"
? item.room_name
: item.source_kind}
</Text>
<Text>Date: {formatLocalDate(item.created_at)}</Text>
<Text>Duration: {formatTimeMs(item.duration)}</Text>
</Box>
<TranscriptActionsMenu
transcriptId={item.id}
onDelete={onDelete}
onReprocess={onReprocess}
/>
</Flex>
</Box>
))}
</Stack>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,62 @@
import React from "react";
import { Icon, Tooltip } from "@chakra-ui/react";
import {
FaCheck,
FaTrash,
FaStar,
FaMicrophone,
FaGear,
} from "react-icons/fa6";
interface TranscriptStatusIconProps {
status: string;
}
export default function TranscriptStatusIcon({
status,
}: TranscriptStatusIconProps) {
switch (status) {
case "ended":
return (
<Tooltip label="Processing done">
<span>
<Icon color="green" as={FaCheck} />
</span>
</Tooltip>
);
case "error":
return (
<Tooltip label="Processing error">
<span>
<Icon color="red.500" as={FaTrash} />
</span>
</Tooltip>
);
case "idle":
return (
<Tooltip label="New meeting, no recording">
<span>
<Icon color="yellow.500" as={FaStar} />
</span>
</Tooltip>
);
case "processing":
return (
<Tooltip label="Processing in progress">
<span>
<Icon color="gray.500" as={FaGear} />
</span>
</Tooltip>
);
case "recording":
return (
<Tooltip label="Recording in progress">
<span>
<Icon color="blue.500" as={FaMicrophone} />
</span>
</Tooltip>
);
default:
return null;
}
}

View File

@@ -0,0 +1,99 @@
import React from "react";
import {
Box,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Link,
Flex,
Spinner,
} from "@chakra-ui/react";
import NextLink from "next/link";
import { GetTranscriptMinimal } from "../../../api";
import { formatTimeMs, formatLocalDate } from "../../../lib/time";
import TranscriptStatusIcon from "./TranscriptStatusIcon";
import TranscriptActionsMenu from "./TranscriptActionsMenu";
interface TranscriptTableProps {
transcripts: GetTranscriptMinimal[];
onDelete: (transcriptId: string) => (e: any) => void;
onReprocess: (transcriptId: string) => (e: any) => void;
loading?: boolean;
}
export default function TranscriptTable({
transcripts,
onDelete,
onReprocess,
loading,
}: TranscriptTableProps) {
return (
<Box display={{ base: "none", md: "block" }} position="relative">
{loading && (
<Flex
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
bg="rgba(255, 255, 255, 0.8)"
zIndex={10}
align="center"
justify="center"
>
<Spinner size="xl" color="gray.700" thickness="4px" />
</Flex>
)}
<Box
opacity={loading ? 0.9 : 1}
pointerEvents={loading ? "none" : "auto"}
transition="opacity 0.2s ease-in-out"
>
<Table colorScheme="gray">
<Thead>
<Tr>
<Th pl={12} width="400px">
Transcription Title
</Th>
<Th width="150px">Source</Th>
<Th width="200px">Date</Th>
<Th width="100px">Duration</Th>
<Th width="50px"></Th>
</Tr>
</Thead>
<Tbody>
{transcripts.map((item) => (
<Tr key={item.id}>
<Td>
<Flex alignItems="start">
<TranscriptStatusIcon status={item.status} />
<Link as={NextLink} href={`/transcripts/${item.id}`} ml={2}>
{item.title || "Unnamed Transcript"}
</Link>
</Flex>
</Td>
<Td>
{item.source_kind === "room"
? item.room_name
: item.source_kind}
</Td>
<Td>{formatLocalDate(item.created_at)}</Td>
<Td>{formatTimeMs(item.duration)}</Td>
<Td>
<TranscriptActionsMenu
transcriptId={item.id}
onDelete={onDelete}
onReprocess={onReprocess}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
</Box>
);
}

View File

@@ -1,55 +1,18 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { import { Flex, Spinner, Heading, Text, Link } from "@chakra-ui/react";
Flex,
Spinner,
Heading,
Box,
Text,
Link,
Stack,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Button,
Divider,
Input,
Icon,
Tooltip,
Menu,
MenuButton,
MenuList,
MenuItem,
IconButton,
AlertDialog,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogBody,
AlertDialogFooter,
Spacer,
} from "@chakra-ui/react";
import {
FaCheck,
FaTrash,
FaStar,
FaMicrophone,
FaGear,
FaEllipsisVertical,
FaArrowRotateRight,
} from "react-icons/fa6";
import useTranscriptList from "../transcripts/useTranscriptList"; import useTranscriptList from "../transcripts/useTranscriptList";
import useSessionUser from "../../lib/useSessionUser"; import useSessionUser from "../../lib/useSessionUser";
import NextLink from "next/link"; import { Room } from "../../api";
import { Room, GetTranscriptMinimal } from "../../api";
import Pagination from "./pagination"; import Pagination from "./pagination";
import { formatTimeMs, formatLocalDate } from "../../lib/time";
import useApi from "../../lib/useApi"; import useApi from "../../lib/useApi";
import { useError } from "../../(errors)/errorContext"; import { useError } from "../../(errors)/errorContext";
import { SourceKind } from "../../api"; 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() { export default function TranscriptBrowser() {
const [selectedSourceKind, setSelectedSourceKind] = const [selectedSourceKind, setSelectedSourceKind] =
@@ -58,7 +21,6 @@ export default function TranscriptBrowser() {
const [rooms, setRooms] = useState<Room[]>([]); const [rooms, setRooms] = useState<Room[]>([]);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [searchInputValue, setSearchInputValue] = useState("");
const { loading, response, refetch } = useTranscriptList( const { loading, response, refetch } = useTranscriptList(
page, page,
selectedSourceKind, selectedSourceKind,
@@ -74,9 +36,6 @@ export default function TranscriptBrowser() {
React.useState<string>(); React.useState<string>();
const [deletedItemIds, setDeletedItemIds] = React.useState<string[]>(); const [deletedItemIds, setDeletedItemIds] = React.useState<string[]>();
const myRooms = rooms.filter((room) => !room.is_shared);
const sharedRooms = rooms.filter((room) => room.is_shared);
useEffect(() => { useEffect(() => {
setDeletedItemIds([]); setDeletedItemIds([]);
}, [page, response]); }, [page, response]);
@@ -103,20 +62,14 @@ export default function TranscriptBrowser() {
refetch(); refetch();
}; };
const handleSearch = () => { const handleSearch = (searchTerm: string) => {
setPage(1); setPage(1);
setSearchTerm(searchInputValue); setSearchTerm(searchTerm);
setSelectedSourceKind(null); setSelectedSourceKind(null);
setSelectedRoomId(""); setSelectedRoomId("");
refetch(); refetch();
}; };
const handleKeyDown = (event) => {
if (event.key === "Enter") {
handleSearch();
}
};
if (loading && !response) if (loading && !response)
return ( return (
<Flex flexDir="column" align="center" justify="center" h="100%"> <Flex flexDir="column" align="center" justify="center" h="100%">
@@ -195,324 +148,42 @@ export default function TranscriptBrowser() {
</Flex> </Flex>
<Flex flexDir={{ base: "column", md: "row" }}> <Flex flexDir={{ base: "column", md: "row" }}>
<Box w={{ base: "full", md: "300px" }} p={4} bg="gray.100"> <FilterSidebar
<Stack spacing={3}> rooms={rooms}
<Link selectedSourceKind={selectedSourceKind}
as={NextLink} selectedRoomId={selectedRoomId}
href="#" onFilterChange={handleFilterTranscripts}
onClick={() => handleFilterTranscripts(null, "")} />
color={selectedSourceKind === null ? "blue.500" : "gray.600"}
_hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === null ? "bold" : "normal"}
>
All Transcripts
</Link>
<Divider />
{myRooms.length > 0 && (
<>
<Heading size="sm">My Rooms</Heading>
{myRooms.map((room) => (
<Link
key={room.id}
as={NextLink}
href="#"
onClick={() => 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}
</Link>
))}
</>
)}
{sharedRooms.length > 0 && (
<>
<Heading size="sm">Shared Rooms</Heading>
{sharedRooms.map((room) => (
<Link
key={room.id}
as={NextLink}
href="#"
onClick={() => 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}
</Link>
))}
</>
)}
<Divider />
<Link
as={NextLink}
href="#"
onClick={() => handleFilterTranscripts("live", "")}
color={selectedSourceKind === "live" ? "blue.500" : "gray.600"}
_hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === "live" ? "bold" : "normal"}
>
Live Transcripts
</Link>
<Link
as={NextLink}
href="#"
onClick={() => handleFilterTranscripts("file", "")}
color={selectedSourceKind === "file" ? "blue.500" : "gray.600"}
_hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === "file" ? "bold" : "normal"}
>
Uploaded Files
</Link>
</Stack>
</Box>
<Flex flexDir="column" flex="1" p={4} gap={4}> <Flex flexDir="column" flex="1" p={4} gap={4}>
<Flex mb={4} alignItems="center"> <SearchBar onSearch={handleSearch} />
<Input
placeholder="Search transcriptions..."
value={searchInputValue}
onChange={(e) => setSearchInputValue(e.target.value)}
onKeyDown={handleKeyDown}
/>
<Button ml={2} onClick={handleSearch}>
Search
</Button>
</Flex>
<Pagination <Pagination
page={page} page={page}
setPage={setPage} setPage={setPage}
total={response?.total || 0} total={response?.total || 0}
size={response?.size || 0} size={response?.size || 0}
/> />
<Box display={{ base: "none", md: "block" }}> <TranscriptTable
<Table colorScheme="gray"> transcripts={response?.items || []}
<Thead> onDelete={handleDeleteTranscript}
<Tr> onReprocess={handleProcessTranscript}
<Th pl={12} width="400px"> loading={loading}
Transcription Title />
</Th> <TranscriptCards
<Th width="150px">Source</Th> transcripts={response?.items || []}
<Th width="200px">Date</Th> onDelete={handleDeleteTranscript}
<Th width="100px">Duration</Th> onReprocess={handleProcessTranscript}
<Th width="50px"></Th> loading={loading}
</Tr> />
</Thead>
<Tbody>
{response?.items?.map((item: GetTranscriptMinimal) => (
<Tr key={item.id}>
<Td>
<Flex alignItems="start">
{item.status === "ended" && (
<Tooltip label="Processing done">
<span>
<Icon color="green" as={FaCheck} />
</span>
</Tooltip>
)}
{item.status === "error" && (
<Tooltip label="Processing error">
<span>
<Icon color="red.500" as={FaTrash} />
</span>
</Tooltip>
)}
{item.status === "idle" && (
<Tooltip label="New meeting, no recording">
<span>
<Icon color="yellow.500" as={FaStar} />
</span>
</Tooltip>
)}
{item.status === "processing" && (
<Tooltip label="Processing in progress">
<span>
<Icon color="gray.500" as={FaGear} />
</span>
</Tooltip>
)}
{item.status === "recording" && (
<Tooltip label="Recording in progress">
<span>
<Icon color="blue.500" as={FaMicrophone} />
</span>
</Tooltip>
)}
<Link
as={NextLink}
href={`/transcripts/${item.id}`}
ml={2}
>
{item.title || "Unnamed Transcript"}
</Link>
</Flex>
</Td>
<Td>
{item.source_kind === "room"
? item.room_name
: item.source_kind}
</Td>
<Td>{formatLocalDate(item.created_at)}</Td>
<Td>{formatTimeMs(item.duration)}</Td>
<Td>
<Menu closeOnSelect={true}>
<MenuButton
as={IconButton}
icon={<Icon as={FaEllipsisVertical} />}
variant="outline"
aria-label="Options"
/>
<MenuList>
<MenuItem onClick={handleDeleteTranscript(item.id)}>
Delete
</MenuItem>
<MenuItem onClick={handleProcessTranscript(item.id)}>
Reprocess
</MenuItem>
</MenuList>
</Menu>
</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
<Box display={{ base: "block", md: "none" }}>
<Stack spacing={2}>
{response?.items?.map((item: GetTranscriptMinimal) => (
<Box key={item.id} borderWidth={1} p={4} borderRadius="md">
<Flex justify="space-between" alignItems="flex-start" gap="2">
<Box>
{item.status === "ended" && (
<Tooltip label="Processing done">
<span>
<Icon color="green" as={FaCheck} />
</span>
</Tooltip>
)}
{item.status === "error" && (
<Tooltip label="Processing error">
<span>
<Icon color="red.500" as={FaTrash} />
</span>
</Tooltip>
)}
{item.status === "idle" && (
<Tooltip label="New meeting, no recording">
<span>
<Icon color="yellow.500" as={FaStar} />
</span>
</Tooltip>
)}
{item.status === "processing" && (
<Tooltip label="Processing in progress">
<span>
<Icon color="gray.500" as={FaGear} />
</span>
</Tooltip>
)}
{item.status === "recording" && (
<Tooltip label="Recording in progress">
<span>
<Icon color="blue.500" as={FaMicrophone} />
</span>
</Tooltip>
)}
</Box>
<Box flex="1">
<Text fontWeight="bold">
{item.title || "Unnamed Transcript"}
</Text>
<Text>
Source:{" "}
{item.source_kind === "room"
? item.room_name
: item.source_kind}
</Text>
<Text>Date: {formatLocalDate(item.created_at)}</Text>
<Text>Duration: {formatTimeMs(item.duration)}</Text>
</Box>
<Menu>
<MenuButton
as={IconButton}
icon={<Icon as={FaEllipsisVertical} />}
variant="outline"
aria-label="Options"
/>
<MenuList>
<MenuItem onClick={handleDeleteTranscript(item.id)}>
Delete
</MenuItem>
<MenuItem onClick={handleProcessTranscript(item.id)}>
Reprocess
</MenuItem>
</MenuList>
</Menu>
</Flex>
</Box>
))}
</Stack>
</Box>
</Flex> </Flex>
</Flex> </Flex>
<AlertDialog <DeleteTranscriptDialog
isOpen={!!transcriptToDeleteId} isOpen={!!transcriptToDeleteId}
leastDestructiveRef={cancelRef}
onClose={onCloseDeletion} onClose={onCloseDeletion}
> onConfirm={() => handleDeleteTranscript(transcriptToDeleteId)(null)}
<AlertDialogOverlay> cancelRef={cancelRef}
<AlertDialogContent> />
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Delete Transcript
</AlertDialogHeader>
<AlertDialogBody>
Are you sure? You can't undo this action afterwards.
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onCloseDeletion}>
Cancel
</Button>
<Button
colorScheme="red"
onClick={handleDeleteTranscript(transcriptToDeleteId)}
ml={3}
>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</Flex> </Flex>
); );
} }