diff --git a/www/app/(app)/browse/_components/FilterSidebar.tsx b/www/app/(app)/browse/_components/FilterSidebar.tsx index 6a440a10..8aa77095 100644 --- a/www/app/(app)/browse/_components/FilterSidebar.tsx +++ b/www/app/(app)/browse/_components/FilterSidebar.tsx @@ -47,7 +47,7 @@ export default function FilterSidebar({ key={room.id} as={NextLink} href="#" - onClick={() => onFilterChange("room" as SourceKind, room.id)} + onClick={() => onFilterChange("room", room.id)} color={ selectedSourceKind === "room" && selectedRoomId === room.id ? "blue.500" diff --git a/www/app/(app)/browse/page.tsx b/www/app/(app)/browse/page.tsx index ca63d0a6..21e364bb 100644 --- a/www/app/(app)/browse/page.tsx +++ b/www/app/(app)/browse/page.tsx @@ -203,7 +203,11 @@ export default function TranscriptBrowser() { const [urlSourceKind, setUrlSourceKind] = useQueryState( "source", - parseAsStringLiteral(["room", "live", "file"] as const).withOptions({ + parseAsStringLiteral([ + "room", + "live", + "file", + ] as const satisfies SourceKind[]).withOptions({ shallow: false, }), ); diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index 3d96ffeb..1631e689 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -93,33 +93,26 @@ export default function RoomsList() { const createRoomMutation = useRoomCreate(); const updateRoomMutation = useRoomUpdate(); const deleteRoomMutation = useRoomDelete(); - const { data: streams = [] } = useZulipStreams() as { data: any[] }; - const { data: topics = [] } = useZulipTopics(selectedStreamId) as { - data: Topic[]; - }; - interface Topic { - name: string; - } + const { data: streams = [] } = useZulipStreams(); + const { data: topics = [] } = useZulipTopics(selectedStreamId); // Update selected stream ID when zulip stream changes useEffect(() => { if (room.zulipStream && streams.length > 0) { - const selectedStream = streams.find( - (s: any) => s.name === room.zulipStream, - ); - if (selectedStream) { - setSelectedStreamId((selectedStream as any).stream_id); + const selectedStream = streams.find((s) => s.name === room.zulipStream); + if (selectedStream !== undefined) { + setSelectedStreamId(selectedStream.stream_id); } } else { setSelectedStreamId(null); } }, [room.zulipStream, streams]); - const streamOptions: SelectOption[] = streams.map((stream: any) => { + const streamOptions: SelectOption[] = streams.map((stream) => { return { label: stream.name, value: stream.name }; }); - const topicOptions: SelectOption[] = topics.map((topic: any) => ({ + const topicOptions: SelectOption[] = topics.map((topic) => ({ label: topic.name, value: topic.name, })); diff --git a/www/app/(app)/rooms/useRoomList.tsx b/www/app/(app)/rooms/useRoomList.tsx index fb655b9b..4cf364c1 100644 --- a/www/app/(app)/rooms/useRoomList.tsx +++ b/www/app/(app)/rooms/useRoomList.tsx @@ -11,6 +11,12 @@ type RoomList = { refetch: () => void; }; +type ValidationError = components["schemas"]["ValidationError"]; + +const formatValidationErrors = (errors: ValidationError[]) => { + return errors.map((error) => error.msg).join(", "); +}; + // Wrapper to maintain backward compatibility const useRoomList = (page: PaginationPage): RoomList => { const { data, isLoading, error, refetch } = useRoomsList(page); @@ -18,7 +24,11 @@ const useRoomList = (page: PaginationPage): RoomList => { return { response: data || null, loading: isLoading, - error: error as Error | null, + error: error + ? new Error( + error.detail ? formatValidationErrors(error.detail) : undefined, + ) + : null, refetch, }; }; diff --git a/www/app/(app)/transcripts/[transcriptId]/page.tsx b/www/app/(app)/transcripts/[transcriptId]/page.tsx index 3e55f5cb..ce48e951 100644 --- a/www/app/(app)/transcripts/[transcriptId]/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/page.tsx @@ -86,7 +86,7 @@ export default function TranscriptDetails(details: TranscriptDetails) { useActiveTopic={useActiveTopic} waveform={waveform.waveform} media={mp3.media} - mediaDuration={transcript.response?.duration} + mediaDuration={transcript.response?.duration || null} /> ) : !mp3.loading && (waveform.error || mp3.error) ? ( diff --git a/www/app/(app)/transcripts/player.tsx b/www/app/(app)/transcripts/player.tsx index 4e1c0a7c..10b02aa1 100644 --- a/www/app/(app)/transcripts/player.tsx +++ b/www/app/(app)/transcripts/player.tsx @@ -20,7 +20,7 @@ type PlayerProps = { ]; waveform: AudioWaveform; media: HTMLMediaElement; - mediaDuration: number; + mediaDuration: number | null; }; export default function Player(props: PlayerProps) { @@ -52,7 +52,9 @@ export default function Player(props: PlayerProps) { container: waveformRef.current, peaks: [props.waveform.data], height: "auto", - duration: Math.floor(props.mediaDuration / 1000), + duration: props.mediaDuration + ? Math.floor(props.mediaDuration / 1000) + : undefined, media: props.media, ...waveSurferStyles.playerSettings, diff --git a/www/app/(app)/transcripts/shareAndPrivacy.tsx b/www/app/(app)/transcripts/shareAndPrivacy.tsx index 6304f057..43a61753 100644 --- a/www/app/(app)/transcripts/shareAndPrivacy.tsx +++ b/www/app/(app)/transcripts/shareAndPrivacy.tsx @@ -76,7 +76,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) { }); setShareMode( shareOptionsData.find( - (option) => option.value === (updatedTranscript as any).share_mode, + (option) => option.value === updatedTranscript.share_mode, ) || shareOptionsData[0], ); } catch (err) { diff --git a/www/app/(app)/transcripts/shareZulip.tsx b/www/app/(app)/transcripts/shareZulip.tsx index 2293bf27..3c3b1ea4 100644 --- a/www/app/(app)/transcripts/shareZulip.tsx +++ b/www/app/(app)/transcripts/shareZulip.tsx @@ -15,7 +15,6 @@ import { Checkbox, Combobox, Spinner, - Portal, useFilter, useListCollection, } from "@chakra-ui/react"; @@ -37,10 +36,6 @@ interface Stream { name: string; } -interface Topic { - name: string; -} - export default function ShareZulip(props: ShareZulipProps & BoxProps) { const [showModal, setShowModal] = useState(false); const [stream, setStream] = useState(undefined); @@ -49,26 +44,23 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) { const [includeTopics, setIncludeTopics] = useState(false); // React Query hooks - const { data: streams = [], isLoading: isLoadingStreams } = - useZulipStreams() as { data: Stream[]; isLoading: boolean }; - const { data: topics = [] } = useZulipTopics(selectedStreamId) as { - data: Topic[]; - }; + const { data: streams = [], isLoading: isLoadingStreams } = useZulipStreams(); + const { data: topics = [] } = useZulipTopics(selectedStreamId); const postToZulipMutation = useTranscriptPostToZulip(); const { contains } = useFilter({ sensitivity: "base" }); const streamItems = useMemo(() => { - return (streams || []).map((stream: Stream) => ({ + return streams.map((stream: Stream) => ({ label: stream.name, value: stream.name, })); }, [streams]); const topicItems = useMemo(() => { - return (topics || []).map((topic: Topic) => ({ - label: topic.name, - value: topic.name, + return topics.map(({ name }) => ({ + label: name, + value: name, })); }, [topics]); diff --git a/www/app/(app)/transcripts/useParticipants.ts b/www/app/(app)/transcripts/useParticipants.ts index f45970c4..a3674597 100644 --- a/www/app/(app)/transcripts/useParticipants.ts +++ b/www/app/(app)/transcripts/useParticipants.ts @@ -41,7 +41,7 @@ const useParticipants = (transcriptId: string): UseParticipants => { loading: false, response: null, refetch, - } as ErrorParticipants & { refetch: () => void }; + } satisfies ErrorParticipants & { refetch: () => void }; } if (loading || !response) { @@ -50,7 +50,7 @@ const useParticipants = (transcriptId: string): UseParticipants => { loading: true, error: null, refetch, - } as LoadingParticipants & { refetch: () => void }; + } satisfies LoadingParticipants & { refetch: () => void }; } return { @@ -58,7 +58,7 @@ const useParticipants = (transcriptId: string): UseParticipants => { loading: false, error: null, refetch, - } as SuccessParticipants & { refetch: () => void }; + } satisfies SuccessParticipants & { refetch: () => void }; }; export default useParticipants; diff --git a/www/app/(app)/transcripts/useTopicWithWords.ts b/www/app/(app)/transcripts/useTopicWithWords.ts index d5177f6f..31e184cc 100644 --- a/www/app/(app)/transcripts/useTopicWithWords.ts +++ b/www/app/(app)/transcripts/useTopicWithWords.ts @@ -42,14 +42,13 @@ const useTopicWithWords = ( topicId || null, ); - // Type-safe return based on state if (error) { return { error: error as Error, loading: false, response: null, refetch, - } as ErrorTopicWithWords & { refetch: () => void }; + } satisfies ErrorTopicWithWords & { refetch: () => void }; } if (loading || !response) { @@ -58,7 +57,7 @@ const useTopicWithWords = ( loading: true, error: false, refetch, - } as LoadingTopicWithWords & { refetch: () => void }; + } satisfies LoadingTopicWithWords & { refetch: () => void }; } return { @@ -66,7 +65,7 @@ const useTopicWithWords = ( loading: false, error: null, refetch, - } as SuccessTopicWithWords & { refetch: () => void }; + } satisfies SuccessTopicWithWords & { refetch: () => void }; }; export default useTopicWithWords; diff --git a/www/app/(app)/transcripts/useTranscript.ts b/www/app/(app)/transcripts/useTranscript.ts index 9d4a18e1..3e56fb9e 100644 --- a/www/app/(app)/transcripts/useTranscript.ts +++ b/www/app/(app)/transcripts/useTranscript.ts @@ -59,7 +59,7 @@ const useTranscript = ( } return { - response: data as GetTranscript, + response: data, loading: false, error: null, reload: refetch, diff --git a/www/app/(app)/transcripts/useWebRTC.ts b/www/app/(app)/transcripts/useWebRTC.ts index 077f26c0..89a2a946 100644 --- a/www/app/(app)/transcripts/useWebRTC.ts +++ b/www/app/(app)/transcripts/useWebRTC.ts @@ -11,7 +11,7 @@ const useWebRTC = ( ): Peer => { const [peer, setPeer] = useState(null); const { setError } = useError(); - const webRTCMutation = useTranscriptWebRTC(); + const { mutateAsync: mutateWebRtcTranscriptAsync } = useTranscriptWebRTC(); useEffect(() => { if (!stream || !transcriptId) { @@ -41,7 +41,7 @@ const useWebRTC = ( }; try { - const answer = await webRTCMutation.mutateAsync({ + const answer = await mutateWebRtcTranscriptAsync({ params: { path: { transcript_id: transcriptId, @@ -69,7 +69,7 @@ const useWebRTC = ( return () => { p.destroy(); }; - }, [stream, transcriptId, webRTCMutation]); + }, [stream, transcriptId, mutateWebRtcTranscriptAsync]); return peer; }; diff --git a/www/app/lib/apiHooks.ts b/www/app/lib/apiHooks.ts index 965c6dfc..519512a8 100644 --- a/www/app/lib/apiHooks.ts +++ b/www/app/lib/apiHooks.ts @@ -3,7 +3,7 @@ import { $api } from "./apiClient"; import { useError } from "../(errors)/errorContext"; import { useQueryClient } from "@tanstack/react-query"; -import type { paths } from "../reflector-api"; +import type { components, paths } from "../reflector-api"; import useAuthReady from "./useAuthReady"; // FIXME: React Query caching issues with cross-tab synchronization @@ -23,7 +23,6 @@ import useAuthReady from "./useAuthReady"; const STALE_TIME = 500; export function useRoomsList(page: number = 1) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( @@ -41,16 +40,17 @@ export function useRoomsList(page: number = 1) { ); } +type SourceKind = components["schemas"]["SourceKind"]; + export function useTranscriptsSearch( q: string = "", options: { limit?: number; offset?: number; room_id?: string; - source_kind?: string; + source_kind?: SourceKind; } = {}, ) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( @@ -63,7 +63,7 @@ export function useTranscriptsSearch( limit: options.limit, offset: options.offset, room_id: options.room_id, - source_kind: options.source_kind as any, + source_kind: options.source_kind, }, }, }, @@ -101,7 +101,6 @@ export function useTranscriptProcess() { } export function useTranscriptGet(transcriptId: string | null) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( @@ -170,12 +169,11 @@ export function useRoomDelete() { } export function useZulipStreams() { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", - "/v1/zulip/streams" as any, + "/v1/zulip/streams", {}, { enabled: isAuthReady, @@ -185,15 +183,20 @@ export function useZulipStreams() { } export function useZulipTopics(streamId: number | null) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); - + const enabled = !!streamId && isAuthReady; return $api.useQuery( "get", - streamId ? (`/v1/zulip/streams/${streamId}/topics` as any) : null, - {}, + "/v1/zulip/streams/{stream_id}/topics", { - enabled: !!streamId && isAuthReady, + params: { + path: { + stream_id: enabled ? streamId : 0, + }, + }, + }, + { + enabled, staleTime: STALE_TIME, }, ); @@ -223,15 +226,11 @@ export function useTranscriptPostToZulip() { const { setError } = useError(); // @ts-ignore - Zulip endpoint not in OpenAPI spec - return $api.useMutation( - "post", - "/v1/transcripts/{transcript_id}/zulip" as any, - { - onError: (error) => { - setError(error as Error, "There was an error posting to Zulip"); - }, + return $api.useMutation("post", "/v1/transcripts/{transcript_id}/zulip", { + onError: (error) => { + setError(error as Error, "There was an error posting to Zulip"); }, - ); + }); } export function useTranscriptUploadAudio() { @@ -263,7 +262,6 @@ export function useTranscriptUploadAudio() { } export function useTranscriptWaveform(transcriptId: string | null) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( @@ -282,7 +280,6 @@ export function useTranscriptWaveform(transcriptId: string | null) { } export function useTranscriptMP3(transcriptId: string | null) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( @@ -301,7 +298,6 @@ export function useTranscriptMP3(transcriptId: string | null) { } export function useTranscriptTopics(transcriptId: string | null) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( @@ -320,7 +316,6 @@ export function useTranscriptTopics(transcriptId: string | null) { } export function useTranscriptTopicsWithWords(transcriptId: string | null) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( @@ -342,7 +337,6 @@ export function useTranscriptTopicsWithWordsPerSpeaker( transcriptId: string | null, topicId: string | null, ) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( @@ -364,7 +358,6 @@ export function useTranscriptTopicsWithWordsPerSpeaker( } export function useTranscriptParticipants(transcriptId: string | null) { - const { setError } = useError(); const { isAuthReady } = useAuthReady(); return $api.useQuery( diff --git a/www/app/lib/useSessionStatus.ts b/www/app/lib/useSessionStatus.ts index 5629c025..a56691b2 100644 --- a/www/app/lib/useSessionStatus.ts +++ b/www/app/lib/useSessionStatus.ts @@ -6,7 +6,7 @@ import { Session } from "next-auth"; export default function useSessionStatus() { const { status: naStatus } = useNextAuthSession(); - const [status, setStatus] = useState("loading"); + const [status, setStatus] = useState("loading"); useEffect(() => { if (naStatus !== "loading" && naStatus !== status) {