"use client"; import { Button, Card, CardBody, Flex, FormControl, FormHelperText, FormLabel, Heading, Input, Link, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Spacer, Spinner, useDisclosure, VStack, Text, Menu, MenuButton, MenuList, MenuItem, IconButton, Checkbox, } from "@chakra-ui/react"; import { useEffect, useState } from "react"; import { Container } from "@chakra-ui/react"; import { FaEllipsisVertical, FaTrash, FaPencil, FaLink } from "react-icons/fa6"; import useApi from "../../lib/useApi"; import useRoomList from "./useRoomList"; import { Select, Options, OptionBase } from "chakra-react-select"; import { ApiError } from "../../api"; interface SelectOption extends OptionBase { label: string; value: string; } const RESERVED_PATHS = ["browse", "rooms", "transcripts"]; const roomModeOptions: Options = [ { label: "2-4 people", value: "normal" }, { label: "2-200 people", value: "group" }, ]; const recordingTriggerOptions: Options = [ { label: "None", value: "none" }, { label: "Prompt", value: "prompt" }, { label: "Automatic", value: "automatic-2nd-participant" }, ]; const roomInitialState = { name: "", zulipAutoPost: false, zulipStream: "", zulipTopic: "", isLocked: false, roomMode: "normal", recordingType: "cloud", recordingTrigger: "automatic-2nd-participant", }; export default function RoomsList() { const { isOpen, onOpen, onClose } = useDisclosure(); const [room, setRoom] = useState(roomInitialState); const [isEditing, setIsEditing] = useState(false); const [editRoomId, setEditRoomId] = useState(""); const api = useApi(); const [page, setPage] = useState(1); const { loading, response, refetch } = useRoomList(page); const [streams, setStreams] = useState([]); const [topics, setTopics] = useState([]); const [nameError, setNameError] = useState(""); const [linkCopied, setLinkCopied] = useState(""); interface Stream { stream_id: number; name: string; } interface Topic { name: string; } useEffect(() => { const fetchZulipStreams = async () => { if (!api) return; try { const response = await api.v1ZulipGetStreams(); setStreams(response); } catch (error) { console.error("Error fetching Zulip streams:", error); } }; if (room.zulipAutoPost) { fetchZulipStreams(); } }, [room.zulipAutoPost, !api]); useEffect(() => { const fetchZulipTopics = async () => { if (!api || !room.zulipStream) return; try { const selectedStream = streams.find((s) => s.name === room.zulipStream); if (selectedStream) { const response = await api.v1ZulipGetTopics({ streamId: selectedStream.stream_id, }); setTopics(response); } } catch (error) { console.error("Error fetching Zulip topics:", error); } }; fetchZulipTopics(); }, [room.zulipStream, streams, api]); const streamOptions: Options = streams.map((stream) => { return { label: stream.name, value: stream.name }; }); const topicOptions: Options = topics.map((topic) => ({ label: topic.name, value: topic.name, })); const handleCopyUrl = (roomName: string) => { const roomUrl = `${window.location.origin}/${roomName}`; navigator.clipboard.writeText(roomUrl); setLinkCopied(roomName); setTimeout(() => { setLinkCopied(""); }, 2000); }; const handleSaveRoom = async () => { try { if (RESERVED_PATHS.includes(room.name)) { setNameError("This room name is reserved. Please choose another name."); return; } const roomData = { name: room.name, zulip_auto_post: room.zulipAutoPost, zulip_stream: room.zulipStream, zulip_topic: room.zulipTopic, is_locked: room.isLocked, room_mode: room.roomMode, recording_type: room.recordingType, recording_trigger: room.recordingTrigger, }; if (isEditing) { await api?.v1RoomsUpdate({ roomId: editRoomId, requestBody: roomData, }); } else { await api?.v1RoomsCreate({ requestBody: roomData, }); } setRoom(roomInitialState); setIsEditing(false); setEditRoomId(""); setNameError(""); refetch(); onClose(); } catch (err) { if ( err instanceof ApiError && err.status === 400 && (err.body as any).detail == "Room name is not unique" ) { setNameError( "This room name is already taken. Please choose a different name.", ); } else { setNameError("An error occurred. Please try again."); } } }; const handleEditRoom = (roomId, roomData) => { setRoom({ name: roomData.name, zulipAutoPost: roomData.zulip_auto_post, zulipStream: roomData.zulip_stream, zulipTopic: roomData.zulip_topic, isLocked: roomData.is_locked, roomMode: roomData.room_mode, recordingType: roomData.recording_type, recordingTrigger: roomData.recording_trigger, }); setEditRoomId(roomId); setIsEditing(true); setNameError(""); onOpen(); }; const handleDeleteRoom = async (roomId: string) => { try { await api?.v1RoomsDelete({ roomId, }); refetch(); } catch (err) { console.error(err); } }; const handleRoomChange = (e) => { let { name, value, type, checked } = e.target; if (name === "name") { value = value .replace(/[^a-zA-Z0-9\s-]/g, "") .replace(/\s+/g, "-") .toLowerCase(); setNameError(""); } setRoom({ ...room, [name]: type === "checkbox" ? checked : value, }); }; if (loading && !response) return ( ); return ( <> Rooms {isEditing ? "Edit Room" : "Add Room"} Room name No spaces or special characters allowed {nameError && {nameError}} Locked room Room size rt.value === room.recordingTrigger, )?.label, value: room.recordingTrigger, }} onChange={(newValue) => setRoom({ ...room, recordingTrigger: newValue!.value, }) } /> Automatically post transcription to Zulip Zulip stream setRoom({ ...room, zulipTopic: newValue!.value, }) } isDisabled={!room.zulipAutoPost} /> {response?.items && response.items.length > 0 ? ( response.items.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 rooms found )} ); }