mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-23 05:39:05 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { TopicList } from "./TopicList";
|
||||
export { TopicItem } from "./TopicItem";
|
||||
export { TopicSegment } from "./TopicSegment";
|
||||
Reference in New Issue
Block a user