feat: migrate from chakra 2 to chakra 3 (#500)

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

* fix: various fixes

* feat: migrate to Chakra UI v3 - update theme, fix deprecated props

- Add whiteAlpha color palette with semantic tokens
- Update button recipe with fontWeight 600 and hover states
- Move Poppins font from theme to HTML tag className
- Fix deprecated props: isDisabled→disabled, align→alignItems/textAlign
- Remove button.css as styles are now handled by Chakra v3

* fix: complete Chakra UI v3 deprecated prop migrations

- Replace all isDisabled with disabled
- Replace all isChecked with checked
- Replace all isLoading with loading
- Replace all isOpen with open
- Replace all noOfLines with lineClamp
- Replace all align with alignItems on Flex/Stack components
- Replace all justify with justifyContent on Flex/Stack components
- Update temporary Select components to use new prop names
- Update REFACTOR2.md with completion status

* fix: add value prop to Menu.Item for proper hover states in Chakra v3

* fix: update browse page components for Chakra UI v3 compatibility

- Fix FilterSidebar status filter styling and prop usage
- Update Pagination component to use new Chakra v3 props and structure
- Refactor TranscriptTable to use modern Chakra patterns
- Clean up browse page layout and props
- Remove unused import from transcripts API view
- Enhance theme with additional semantic color tokens

* fix: polish browse page UI for Chakra v3

- Add rounded corners to FilterSidebar
- Adjust responsive breakpoints from md to lg for table/card view
- Add consistent font weights to table headers
- Improve card view typography and spacing
- Fix padding and margins for better mobile experience
- Remove unused table recipe from theme

* fix: padding

* fix: rework transcript page

* fix: more tidy layout for topic

* fix: share and privacy using chakra3 select

* fix: fix share and privacy select, now working, with closing dialog

* fix: complete Chakra UI v3 migration for share components and fix all TypeScript errors

- Refactor shareZulip.tsx to integrate modal content directly
- Replace react-select-search with Chakra UI v3 Select components using collection pattern
- Convert all Checkbox components to use v3 composable structure (Checkbox.Root, etc.)
- Fix Card components to use Card.Root and Card.Body
- Replace deprecated textColor prop with color prop
- Update Menu components to use v3 namespace pattern (Menu.Root, Menu.Trigger, etc.)
- Remove unused AlertDialog imports
- Fix useDisclosure hook changes (isOpen -> open)
- Replace UnorderedList with List.Root and ListItem with List.Item
- Fix Skeleton components by removing isLoaded prop and using conditional rendering
- Update Button variants to valid v3 options
- Fix Spinner props (remove thickness, speed, emptyColor)
- Update toast API to use custom toaster component
- Fix Progress components and FormControl to Field.Root
- Update Alert to use compound component pattern
- Remove shareModal.tsx file after integration

* fix: bring back topic list

* fix: normalize menu item

* fix: migrate rooms page to Chakra UI v3 pattern

- Updated layout to match browse page with Flex container and proper spacing
- Migrated add/edit room modal from custom HTML to Chakra UI v3 Dialog component
- Replaced all Select components with Chakra UI v3 Select using createListCollection
- Replaced FormControl/FormLabel/FormHelperText with Field.Root/Field.Label/Field.HelperText
- Removed inline styles and used Chakra props (mr={2} instead of style={{ marginRight: "8px" }})
- Fixed TypeScript interfaces removing OptionBase extension
- Fixed theme.ts accordion anatomy import issue

* refactor: convert rooms list to table view with responsive design

- Create RoomTable component for desktop view showing room details in columns
- Create RoomCards component for mobile/tablet responsive view
- Refactor RoomList to use table/card components based on screen size
- Display Zulip configuration, room size, and recording settings in table
- Remove unused RoomItem component
- Import Room type from API for proper typing

* refactor: extract RoomActionsMenu component to eliminate duplication

- Create RoomActionsMenu component for consistent room action menus
- Update RoomCards and RoomTable to use the new shared component
- Remove duplicated menu code from both components

* feat: add icons to TranscriptActionsMenu for consistency

- Add FaTrash icon for Delete action with red color
- Add FaArrowsRotate icon for Reprocess action
- Matches the pattern established in RoomActionsMenu

* refactor: update icons from Font Awesome to Lucide React

- Replace FaEllipsisVertical with LuMenu in menu triggers
- Replace FaLink with LuLink for copy URL buttons
- Replace FaPencil with LuPen for edit actions
- Replace FaTrash with LuTrash for delete actions
- Replace FaArrowsRotate with LuRotateCw for reprocess action
- Consistent icon library usage across all components

* refactor: little pass on the icons

* fix: lu icon

* fix: primary for button

* fix: recording page with mic selection

* fix: also fix duration

* fix: use combobox for share zulip

* fix: use proper theming for button, variant was not recognized

* fix: room actions menu

* fix: remove other variant primary left.
This commit is contained in:
2025-07-21 16:16:12 -06:00
committed by GitHub
parent d77b5611f8
commit 901a239952
66 changed files with 3061 additions and 2437 deletions

View File

@@ -0,0 +1,61 @@
import { Box, Text, Accordion, Flex } from "@chakra-ui/react";
import { formatTime } from "../../../../lib/time";
import { Topic } from "../../webSocketTypes";
import { TopicSegment } from "./TopicSegment";
interface TopicItemProps {
topic: Topic;
isActive: boolean;
getSpeakerName: (speakerNumber: number) => string | undefined;
}
export function TopicItem({ topic, isActive, getSpeakerName }: TopicItemProps) {
return (
<Accordion.Item value={topic.id} id={`topic-${topic.id}`}>
<Accordion.ItemTrigger
background={isActive ? "gray.50" : "white"}
display="flex"
alignItems="start"
justifyContent="space-between"
>
<Flex
display="flex"
justifyContent="center"
alignItems="center"
height="24px"
width="24px"
>
<Accordion.ItemIndicator />
</Flex>
<Box flex="1">{topic.title} </Box>
<Text as="span" color="gray.500" fontSize="xs" pr={1}>
{formatTime(topic.timestamp)}
</Text>
</Accordion.ItemTrigger>
<Accordion.ItemContent>
<Accordion.ItemBody p={4}>
{isActive && (
<>
{topic.segments ? (
<>
{topic.segments.map((segment, index: number) => (
<TopicSegment
key={index}
segment={segment}
speakerName={
getSpeakerName(segment.speaker) ||
`Speaker ${segment.speaker}`
}
/>
))}
</>
) : (
<>{topic.transcript}</>
)}
</>
)}
</Accordion.ItemBody>
</Accordion.ItemContent>
</Accordion.Item>
);
}

View File

@@ -0,0 +1,190 @@
import React, { useState, useEffect } from "react";
import ScrollToBottom from "../../scrollToBottom";
import { Topic } from "../../webSocketTypes";
import useParticipants from "../../useParticipants";
import { Box, Flex, Text, Accordion } from "@chakra-ui/react";
import { featureEnabled } from "../../../../domainContext";
import { TopicItem } from "./TopicItem";
type TopicListProps = {
topics: Topic[];
useActiveTopic: [
Topic | null,
React.Dispatch<React.SetStateAction<Topic | null>>,
];
autoscroll: boolean;
transcriptId: string;
status: string;
currentTranscriptText: any;
};
export function TopicList({
topics,
useActiveTopic,
autoscroll,
transcriptId,
status,
currentTranscriptText,
}: TopicListProps) {
const [activeTopic, setActiveTopic] = useActiveTopic;
const [autoscrollEnabled, setAutoscrollEnabled] = useState<boolean>(true);
const participants = useParticipants(transcriptId);
const scrollToTopic = () => {
const topicDiv = document.getElementById(`topic-${activeTopic?.id}`);
setTimeout(() => {
topicDiv?.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "nearest",
});
}, 200);
};
useEffect(() => {
if (activeTopic && autoscroll) scrollToTopic();
}, [activeTopic, autoscroll]);
// scroll top is not rounded, heights are, so exact match won't work.
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
const toggleScroll = (element) => {
const bottom =
Math.abs(
element.scrollHeight - element.clientHeight - element.scrollTop,
) < 2 || element.scrollHeight == element.clientHeight;
if (!bottom && autoscrollEnabled) {
setAutoscrollEnabled(false);
} else if (bottom && !autoscrollEnabled) {
setAutoscrollEnabled(true);
}
};
const handleScroll = (e) => {
toggleScroll(e.target);
};
useEffect(() => {
if (autoscroll) {
const topicsDiv = document.getElementById("scroll-div");
topicsDiv && toggleScroll(topicsDiv);
}
}, [activeTopic, autoscroll]);
useEffect(() => {
if (autoscroll && autoscrollEnabled) scrollToBottom();
}, [topics.length, currentTranscriptText]);
const scrollToBottom = () => {
const topicsDiv = document.getElementById("scroll-div");
if (topicsDiv) topicsDiv.scrollTop = topicsDiv.scrollHeight;
};
const getSpeakerName = (speakerNumber: number) => {
if (!participants.response) return;
return (
participants.response.find(
(participant) => participant.speaker == speakerNumber,
)?.name || `Speaker ${speakerNumber}`
);
};
const requireLogin = featureEnabled("requireLogin");
useEffect(() => {
if (autoscroll) {
setActiveTopic(topics[topics.length - 1]);
}
}, [topics, autoscroll]);
return (
<Flex
position={"relative"}
w={"100%"}
h={"95%"}
flexDirection={"column"}
justify={"center"}
align={"center"}
flexShrink={0}
>
{autoscroll && (
<ScrollToBottom
visible={!autoscrollEnabled}
handleScrollBottom={scrollToBottom}
/>
)}
<Box
id="scroll-div"
overflowY={"auto"}
h={"100%"}
onScroll={handleScroll}
width="full"
>
{topics.length > 0 && (
<Accordion.Root
multiple={false}
collapsible={true}
value={activeTopic ? [activeTopic.id] : []}
onValueChange={(details) => {
const selectedTopicId = details.value[0];
const selectedTopic = selectedTopicId
? topics.find((t) => t.id === selectedTopicId)
: null;
setActiveTopic(selectedTopic || null);
}}
>
{topics.map((topic) => (
<TopicItem
key={topic.id}
topic={topic}
isActive={activeTopic?.id === topic.id}
getSpeakerName={getSpeakerName}
/>
))}
</Accordion.Root>
)}
{status == "recording" && (
<Box textAlign={"center"}>
<Text>{currentTranscriptText}</Text>
</Box>
)}
{(status == "recording" || status == "idle") &&
currentTranscriptText.length == 0 &&
topics.length == 0 && (
<Box textAlign={"center"} color="gray">
<Text>
Full discussion transcript will appear here after you start
recording.
</Text>
<Text>
It may take up to 5 minutes of conversation to first appear.
</Text>
</Box>
)}
{status == "processing" && (
<Box textAlign={"center"} color="gray">
<Text>We are processing the recording, please wait.</Text>
{!requireLogin && (
<span>
Please do not navigate away from the page during this time.
</span>
)}
</Box>
)}
{status == "ended" && topics.length == 0 && (
<Box textAlign={"center"} color="gray">
<Text>Recording has ended without topics being found.</Text>
</Box>
)}
{status == "error" && (
<Box textAlign={"center"} color="gray">
<Text>There was an error processing your recording</Text>
</Box>
)}
</Box>
</Flex>
);
}

View File

@@ -0,0 +1,39 @@
import { Text } from "@chakra-ui/react";
import { formatTime } from "../../../../lib/time";
import { generateHighContrastColor } from "../../../../lib/utils";
interface TopicSegmentProps {
segment: {
start: number;
speaker: number;
text: string;
};
speakerName: string;
}
export function TopicSegment({ segment, speakerName }: TopicSegmentProps) {
return (
<Text
className="text-left text-slate-500 text-sm md:text-base"
pb={2}
lineHeight="1.3"
>
<Text as="span" color="gray.500" fontFamily="monospace" fontSize="sm">
[{formatTime(segment.start)}]
</Text>
<Text
as="span"
fontWeight="bold"
fontSize="sm"
color={generateHighContrastColor(
`Speaker ${segment.speaker}`,
[96, 165, 250],
)}
>
{" "}
{speakerName}:
</Text>{" "}
<span>{segment.text}</span>
</Text>
);
}

View File

@@ -0,0 +1,3 @@
export { TopicList } from "./TopicList";
export { TopicItem } from "./TopicItem";
export { TopicSegment } from "./TopicSegment";

View File

@@ -11,11 +11,10 @@ import {
Button,
Flex,
Text,
UnorderedList,
List,
Input,
Kbd,
Spinner,
ListItem,
Grid,
} from "@chakra-ui/react";
@@ -351,7 +350,7 @@ const ParticipantList = ({
/>
<Button
onClick={doAction}
colorScheme="blue"
colorPalette="blue"
disabled={!action || anyLoading}
>
{!anyLoading ? (
@@ -371,14 +370,14 @@ const ParticipantList = ({
</Flex>
{participants.response && (
<UnorderedList
<List.Root
mx="0"
mb={{ base: 2, md: 4 }}
maxH="100%"
overflow="scroll"
>
{participants.response.map((participant: Participant) => (
<ListItem
<List.Item
onClick={selectParticipant(participant)}
cursor="pointer"
className={
@@ -410,7 +409,7 @@ const ParticipantList = ({
!loading && (
<Button
onClick={mergeSpeaker(selectedText, participant)}
colorScheme="blue"
colorPalette="blue"
ml="2"
size="sm"
>
@@ -435,7 +434,7 @@ const ParticipantList = ({
{selectedTextIsTimeSlice(selectedText) && !loading && (
<Button
onClick={assignTo(participant)}
colorScheme="blue"
colorPalette="blue"
ml="2"
size="sm"
>
@@ -460,16 +459,16 @@ const ParticipantList = ({
<Button
onClick={deleteParticipant(participant.id)}
colorScheme="blue"
colorPalette="blue"
ml="2"
size="sm"
>
Delete
</Button>
</Box>
</ListItem>
</List.Item>
))}
</UnorderedList>
</List.Root>
)}
</Grid>
</Box>

View File

@@ -11,7 +11,7 @@ import {
SkeletonCircle,
Flex,
} from "@chakra-ui/react";
import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons";
import { ChevronLeft, ChevronRight } from "lucide-react";
type TopicHeader = {
stateCurrentTopic: [
@@ -91,73 +91,62 @@ export default function TopicHeader({
justifyContent="space-between"
{...chakraProps}
>
<SkeletonCircle
isLoaded={isLoaded}
h={isLoaded ? "auto" : "40px"}
w={isLoaded ? "auto" : "40px"}
mb="2"
fadeDuration={1}
>
{isLoaded ? (
<Circle
as="button"
onClick={onPrev}
disabled={!canGoPrevious}
onClick={canGoPrevious ? onPrev : undefined}
size="40px"
border="1px"
color={canGoPrevious ? "inherit" : "gray"}
borderColor={canGoNext ? "body-text" : "gray"}
cursor={canGoPrevious ? "pointer" : "not-allowed"}
opacity={canGoPrevious ? 1 : 0.5}
>
{canGoPrevious ? (
<Kbd>
<ChevronLeftIcon />
<ChevronLeft size={16} />
</Kbd>
) : (
<ChevronLeftIcon />
<ChevronLeft size={16} />
)}
</Circle>
</SkeletonCircle>
<Skeleton
isLoaded={isLoaded}
h={isLoaded ? "auto" : "40px"}
mb="2"
fadeDuration={1}
flexGrow={1}
mx={6}
>
<Flex wrap="nowrap" justifyContent="center">
<Heading size="lg" textAlign="center" noOfLines={1}>
) : (
<SkeletonCircle h="40px" w="40px" mb="2" />
)}
{isLoaded ? (
<Flex wrap="nowrap" justifyContent="center" flexGrow={1} mx={6}>
<Heading size="lg" textAlign="center" lineClamp={1}>
{currentTopic?.title}{" "}
</Heading>
<Heading size="lg" ml="3">
{(number || 0) + 1}/{total}
</Heading>
</Flex>
</Skeleton>
<SkeletonCircle
isLoaded={isLoaded}
h={isLoaded ? "auto" : "40px"}
w={isLoaded ? "auto" : "40px"}
mb="2"
fadeDuration={1}
>
) : (
<Skeleton h="40px" mb="2" flexGrow={1} mx={6} />
)}
{isLoaded ? (
<Circle
as="button"
onClick={onNext}
disabled={!canGoNext}
onClick={canGoNext ? onNext : undefined}
size="40px"
border="1px"
color={canGoNext ? "inherit" : "gray"}
borderColor={canGoNext ? "body-text" : "gray"}
cursor={canGoNext ? "pointer" : "not-allowed"}
opacity={canGoNext ? 1 : 0.5}
>
{canGoNext ? (
<Kbd>
<ChevronRightIcon />
<ChevronRight size={16} />
</Kbd>
) : (
<ChevronRightIcon />
<ChevronRight size={16} />
)}
</Circle>
</SkeletonCircle>
) : (
<SkeletonCircle h="40px" w="40px" mb="2" />
)}
</Box>
);
}

View File

@@ -199,61 +199,54 @@ const TopicPlayer = ({
</Text>
);
}
return (
<Skeleton
isLoaded={isLoaded}
h={isLoaded ? "auto" : "40px"}
fadeDuration={1}
w={isLoaded ? "auto" : "container.md"}
margin="auto"
{...chakraProps}
>
<Wrap spacing="4" justify="center" align="center">
<WrapItem>
<SoundWaveCss playing={isPlaying} />
<Text fontSize="sm" pt="1" pl="2">
{showTime}
</Text>
</WrapItem>
<WrapItem>
<Button onClick={playTopic} colorScheme="blue">
Play from start
</Button>
</WrapItem>
<WrapItem>
{!isPlaying ? (
<Button
onClick={playCurrent}
ref={playButton}
id="playButton"
colorScheme="blue"
w="120px"
>
<Kbd color="blue.600">Space</Kbd>&nbsp;Play
</Button>
) : (
<Button
onClick={pause}
ref={playButton}
id="playButton"
colorScheme="blue"
w="120px"
>
<Kbd color="blue.600">Space</Kbd>&nbsp;Pause
</Button>
)}
</WrapItem>
<WrapItem visibility={selectedTime ? "visible" : "hidden"}>
return isLoaded ? (
<Wrap gap="4" justify="center" align="center">
<WrapItem>
<SoundWaveCss playing={isPlaying} />
<Text fontSize="sm" pt="1" pl="2">
{showTime}
</Text>
</WrapItem>
<WrapItem>
<Button onClick={playTopic} colorPalette="blue">
Play from start
</Button>
</WrapItem>
<WrapItem>
{!isPlaying ? (
<Button
disabled={!selectedTime}
onClick={playSelection}
colorScheme="blue"
onClick={playCurrent}
ref={playButton}
id="playButton"
colorPalette="blue"
w="120px"
>
<Kbd color="blue.600">,</Kbd>&nbsp;Play selection
<Kbd color="blue.600">Space</Kbd>&nbsp;Play
</Button>
</WrapItem>
</Wrap>
</Skeleton>
) : (
<Button
onClick={pause}
ref={playButton}
id="playButton"
colorPalette="blue"
w="120px"
>
<Kbd color="blue.600">Space</Kbd>&nbsp;Pause
</Button>
)}
</WrapItem>
<WrapItem visibility={selectedTime ? "visible" : "hidden"}>
<Button
disabled={!selectedTime}
onClick={playSelection}
colorPalette="blue"
>
<Kbd color="blue.600">,</Kbd>&nbsp;Play selection
</Button>
</WrapItem>
</Wrap>
) : (
<Skeleton h="40px" w="container.md" margin="auto" {...chakraProps} />
);
};

View File

@@ -16,7 +16,7 @@ import {
Textarea,
Spacer,
} from "@chakra-ui/react";
import { FaPen } from "react-icons/fa";
import { LuPen } from "react-icons/lu";
import { useError } from "../../../(errors)/errorContext";
import ShareAndPrivacy from "../shareAndPrivacy";
@@ -108,29 +108,26 @@ export default function FinalSummary(props: FinalSummaryProps) {
right="0"
>
{isEditMode && (
<>
<Flex gap={2} align="center" w="full">
<Heading size={{ base: "md" }}>Summary</Heading>
<Spacer />
<Button
onClick={onDiscardClick}
colorScheme="gray"
variant={"text"}
>
Discard
<Button onClick={onDiscardClick} variant="ghost">
Cancel
</Button>
<Button onClick={onSaveClick} colorScheme="blue">
Save
</Button>
</>
<Button onClick={onSaveClick}>Save</Button>
</Flex>
)}
{!isEditMode && (
<>
<Spacer />
<IconButton
icon={<FaPen />}
aria-label="Edit Summary"
onClick={onEditClick}
/>
size="sm"
variant="subtle"
>
<LuPen />
</IconButton>
<ShareAndPrivacy
finalSummaryRef={finalSummaryRef}
transcriptResponse={props.transcriptResponse}

View File

@@ -4,10 +4,9 @@ import useTranscript from "../useTranscript";
import useTopics from "../useTopics";
import useWaveform from "../useWaveform";
import useMp3 from "../useMp3";
import { TopicList } from "../topicList";
import { TopicList } from "./_components/TopicList";
import { Topic } from "../webSocketTypes";
import React, { useEffect, useState } from "react";
import "../../../styles/button.css";
import FinalSummary from "./finalSummary";
import TranscriptTitle from "../transcriptTitle";
import Player from "../player";
@@ -104,7 +103,8 @@ export default function TranscriptDetails(details: TranscriptDetails) {
base: "auto minmax(0, 1fr) minmax(0, 1fr)",
md: "auto minmax(0, 1fr)",
}}
gap={2}
gap={4}
gridRowGap={2}
padding={4}
paddingBottom={0}
background="gray.bg"

View File

@@ -1,10 +1,9 @@
"use client";
import { useEffect, useState } from "react";
import Recorder from "../../recorder";
import { TopicList } from "../../topicList";
import { TopicList } from "../_components/TopicList";
import useTranscript from "../../useTranscript";
import { useWebSockets } from "../../useWebSockets";
import "../../../../styles/button.css";
import { Topic } from "../../webSocketTypes";
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
import { useRouter } from "next/navigation";
@@ -105,7 +104,7 @@ const TranscriptRecord = (details: TranscriptDetails) => {
</Box>
<Box w={{ md: "50%" }} h={{ base: "20%", md: "full" }}>
{!transcriptStarted ? (
<Box textAlign={"center"} textColor="gray">
<Box textAlign={"center"} color="gray">
<Text>
Live transcript will appear here shortly after you'll start
recording.

View File

@@ -1,179 +0,0 @@
import React, { useContext, useState, useEffect } from "react";
import SelectSearch from "react-select-search";
import { GetTranscript, GetTranscriptTopic } from "../../../api";
import "react-select-search/style.css";
import { DomainContext } from "../../../domainContext";
import useApi from "../../../lib/useApi";
type ShareModalProps = {
show: boolean;
setShow: (show: boolean) => void;
transcript: GetTranscript | null;
topics: GetTranscriptTopic[] | null;
};
interface Stream {
stream_id: number;
name: string;
}
interface Topic {
name: string;
}
interface SelectSearchOption {
name: string;
value: string;
}
const ShareModal = (props: ShareModalProps) => {
const [stream, setStream] = useState<string | undefined>(undefined);
const [topic, setTopic] = useState<string | undefined>(undefined);
const [includeTopics, setIncludeTopics] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [streams, setStreams] = useState<Stream[]>([]);
const [topics, setTopics] = useState<Topic[]>([]);
const api = useApi();
useEffect(() => {
const fetchZulipStreams = async () => {
if (!api) return;
try {
const response = await api.v1ZulipGetStreams();
setStreams(response);
setIsLoading(false);
} catch (error) {
console.error("Error fetching Zulip streams:", error);
}
};
fetchZulipStreams();
}, [!api]);
useEffect(() => {
const fetchZulipTopics = async () => {
if (!api || !stream) return;
try {
const selectedStream = streams.find((s) => s.name === stream);
if (selectedStream) {
const response = await api.v1ZulipGetTopics({
streamId: selectedStream.stream_id,
});
setTopics(response);
}
} catch (error) {
console.error("Error fetching Zulip topics:", error);
}
};
fetchZulipTopics();
}, [stream, streams, api]);
const handleSendToZulip = async () => {
if (!api || !props.transcript) return;
if (stream && topic) {
try {
await api.v1TranscriptPostToZulip({
transcriptId: props.transcript.id,
stream,
topic,
includeTopics,
});
} catch (error) {
console.log(error);
}
}
};
if (props.show && isLoading) {
return <div>Loading...</div>;
}
const streamOptions: SelectSearchOption[] = streams.map((stream) => ({
name: stream.name,
value: stream.name,
}));
const topicOptions: SelectSearchOption[] = topics.map((topic) => ({
name: topic.name,
value: topic.name,
}));
return (
<div className="absolute">
{props.show && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 w-96 shadow-lg rounded-md bg-white">
<div className="mt-3 text-center">
<h3 className="font-bold text-xl">Send to Zulip</h3>
{/* Checkbox for 'Include Topics' */}
<div className="mt-4 text-left ml-5">
<label className="flex items-center">
<input
type="checkbox"
className="form-checkbox rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
checked={includeTopics}
onChange={(e) => setIncludeTopics(e.target.checked)}
/>
<span className="ml-2">Include topics</span>
</label>
</div>
<div className="flex items-center mt-4">
<span className="mr-2">#</span>
<SelectSearch
search={true}
options={streamOptions}
value={stream}
onChange={(val) => {
setTopic(undefined); // Reset topic when stream changes
setStream(val.toString());
}}
placeholder="Pick a stream"
/>
</div>
{stream && (
<div className="flex items-center mt-4">
<span className="mr-2 invisible">#</span>
<SelectSearch
search={true}
options={topicOptions}
value={topic}
onChange={(val) => setTopic(val.toString())}
placeholder="Pick a topic"
/>
</div>
)}
<button
className={`bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded py-2 px-4 mr-3 ${
!stream || !topic ? "opacity-50 cursor-not-allowed" : ""
}`}
disabled={!stream || !topic}
onClick={() => {
handleSendToZulip();
props.setShow(false);
}}
>
Send to Zulip
</button>
<button
className="bg-red-500 hover:bg-red-700 focus-visible:bg-red-700 text-white rounded py-2 px-4 mt-4"
onClick={() => props.setShow(false)}
>
Close
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default ShareModal;

View File

@@ -2,7 +2,6 @@
import { useEffect, useState } from "react";
import useTranscript from "../../useTranscript";
import { useWebSockets } from "../../useWebSockets";
import "../../../../styles/button.css";
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
import { useRouter } from "next/navigation";
import useMp3 from "../../useMp3";
@@ -62,18 +61,14 @@ const TranscriptUpload = (details: TranscriptUpload) => {
<>
<VStack
align={"left"}
w="full"
h="full"
mb={4}
background="gray.bg"
border={"2px solid"}
borderColor={"gray.bg"}
borderRadius={8}
p="4"
pt={4}
mx="auto"
w={{ base: "full", md: "container.xl" }}
>
<Heading size={"lg"}>Upload meeting</Heading>
<Center h={"full"} w="full">
<VStack spacing={10}>
<VStack gap={10} bg="gray.100" p={10} borderRadius="md" maxW="500px">
{status && status == "idle" && (
<>
<Text>
@@ -94,7 +89,6 @@ const TranscriptUpload = (details: TranscriptUpload) => {
processed.
</Text>
<Button
colorScheme="blue"
onClick={() => {
router.push("/browse");
}}