Igor/mathieu/frontend openapi react query (#597)

* small typing

* typing fixes

---------

Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
This commit is contained in:
Igor Monadical
2025-09-02 11:49:00 -04:00
committed by GitHub
parent 0df1b224f2
commit ca75a4c95e
14 changed files with 67 additions and 74 deletions

View File

@@ -47,7 +47,7 @@ export default function FilterSidebar({
key={room.id} key={room.id}
as={NextLink} as={NextLink}
href="#" href="#"
onClick={() => onFilterChange("room" as SourceKind, room.id)} onClick={() => onFilterChange("room", room.id)}
color={ color={
selectedSourceKind === "room" && selectedRoomId === room.id selectedSourceKind === "room" && selectedRoomId === room.id
? "blue.500" ? "blue.500"

View File

@@ -203,7 +203,11 @@ export default function TranscriptBrowser() {
const [urlSourceKind, setUrlSourceKind] = useQueryState( const [urlSourceKind, setUrlSourceKind] = useQueryState(
"source", "source",
parseAsStringLiteral(["room", "live", "file"] as const).withOptions({ parseAsStringLiteral([
"room",
"live",
"file",
] as const satisfies SourceKind[]).withOptions({
shallow: false, shallow: false,
}), }),
); );

View File

@@ -93,33 +93,26 @@ export default function RoomsList() {
const createRoomMutation = useRoomCreate(); const createRoomMutation = useRoomCreate();
const updateRoomMutation = useRoomUpdate(); const updateRoomMutation = useRoomUpdate();
const deleteRoomMutation = useRoomDelete(); const deleteRoomMutation = useRoomDelete();
const { data: streams = [] } = useZulipStreams() as { data: any[] }; const { data: streams = [] } = useZulipStreams();
const { data: topics = [] } = useZulipTopics(selectedStreamId) as { const { data: topics = [] } = useZulipTopics(selectedStreamId);
data: Topic[];
};
interface Topic {
name: string;
}
// Update selected stream ID when zulip stream changes // Update selected stream ID when zulip stream changes
useEffect(() => { useEffect(() => {
if (room.zulipStream && streams.length > 0) { if (room.zulipStream && streams.length > 0) {
const selectedStream = streams.find( const selectedStream = streams.find((s) => s.name === room.zulipStream);
(s: any) => s.name === room.zulipStream, if (selectedStream !== undefined) {
); setSelectedStreamId(selectedStream.stream_id);
if (selectedStream) {
setSelectedStreamId((selectedStream as any).stream_id);
} }
} else { } else {
setSelectedStreamId(null); setSelectedStreamId(null);
} }
}, [room.zulipStream, streams]); }, [room.zulipStream, streams]);
const streamOptions: SelectOption[] = streams.map((stream: any) => { const streamOptions: SelectOption[] = streams.map((stream) => {
return { label: stream.name, value: stream.name }; return { label: stream.name, value: stream.name };
}); });
const topicOptions: SelectOption[] = topics.map((topic: any) => ({ const topicOptions: SelectOption[] = topics.map((topic) => ({
label: topic.name, label: topic.name,
value: topic.name, value: topic.name,
})); }));

View File

@@ -11,6 +11,12 @@ type RoomList = {
refetch: () => void; refetch: () => void;
}; };
type ValidationError = components["schemas"]["ValidationError"];
const formatValidationErrors = (errors: ValidationError[]) => {
return errors.map((error) => error.msg).join(", ");
};
// Wrapper to maintain backward compatibility // Wrapper to maintain backward compatibility
const useRoomList = (page: PaginationPage): RoomList => { const useRoomList = (page: PaginationPage): RoomList => {
const { data, isLoading, error, refetch } = useRoomsList(page); const { data, isLoading, error, refetch } = useRoomsList(page);
@@ -18,7 +24,11 @@ const useRoomList = (page: PaginationPage): RoomList => {
return { return {
response: data || null, response: data || null,
loading: isLoading, loading: isLoading,
error: error as Error | null, error: error
? new Error(
error.detail ? formatValidationErrors(error.detail) : undefined,
)
: null,
refetch, refetch,
}; };
}; };

View File

@@ -86,7 +86,7 @@ export default function TranscriptDetails(details: TranscriptDetails) {
useActiveTopic={useActiveTopic} useActiveTopic={useActiveTopic}
waveform={waveform.waveform} waveform={waveform.waveform}
media={mp3.media} media={mp3.media}
mediaDuration={transcript.response?.duration} mediaDuration={transcript.response?.duration || null}
/> />
) : !mp3.loading && (waveform.error || mp3.error) ? ( ) : !mp3.loading && (waveform.error || mp3.error) ? (
<Box p={4} bg="red.100" borderRadius="md"> <Box p={4} bg="red.100" borderRadius="md">

View File

@@ -20,7 +20,7 @@ type PlayerProps = {
]; ];
waveform: AudioWaveform; waveform: AudioWaveform;
media: HTMLMediaElement; media: HTMLMediaElement;
mediaDuration: number; mediaDuration: number | null;
}; };
export default function Player(props: PlayerProps) { export default function Player(props: PlayerProps) {
@@ -52,7 +52,9 @@ export default function Player(props: PlayerProps) {
container: waveformRef.current, container: waveformRef.current,
peaks: [props.waveform.data], peaks: [props.waveform.data],
height: "auto", height: "auto",
duration: Math.floor(props.mediaDuration / 1000), duration: props.mediaDuration
? Math.floor(props.mediaDuration / 1000)
: undefined,
media: props.media, media: props.media,
...waveSurferStyles.playerSettings, ...waveSurferStyles.playerSettings,

View File

@@ -76,7 +76,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
}); });
setShareMode( setShareMode(
shareOptionsData.find( shareOptionsData.find(
(option) => option.value === (updatedTranscript as any).share_mode, (option) => option.value === updatedTranscript.share_mode,
) || shareOptionsData[0], ) || shareOptionsData[0],
); );
} catch (err) { } catch (err) {

View File

@@ -15,7 +15,6 @@ import {
Checkbox, Checkbox,
Combobox, Combobox,
Spinner, Spinner,
Portal,
useFilter, useFilter,
useListCollection, useListCollection,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
@@ -37,10 +36,6 @@ interface Stream {
name: string; name: string;
} }
interface Topic {
name: string;
}
export default function ShareZulip(props: ShareZulipProps & BoxProps) { export default function ShareZulip(props: ShareZulipProps & BoxProps) {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [stream, setStream] = useState<string | undefined>(undefined); const [stream, setStream] = useState<string | undefined>(undefined);
@@ -49,26 +44,23 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
const [includeTopics, setIncludeTopics] = useState(false); const [includeTopics, setIncludeTopics] = useState(false);
// React Query hooks // React Query hooks
const { data: streams = [], isLoading: isLoadingStreams } = const { data: streams = [], isLoading: isLoadingStreams } = useZulipStreams();
useZulipStreams() as { data: Stream[]; isLoading: boolean }; const { data: topics = [] } = useZulipTopics(selectedStreamId);
const { data: topics = [] } = useZulipTopics(selectedStreamId) as {
data: Topic[];
};
const postToZulipMutation = useTranscriptPostToZulip(); const postToZulipMutation = useTranscriptPostToZulip();
const { contains } = useFilter({ sensitivity: "base" }); const { contains } = useFilter({ sensitivity: "base" });
const streamItems = useMemo(() => { const streamItems = useMemo(() => {
return (streams || []).map((stream: Stream) => ({ return streams.map((stream: Stream) => ({
label: stream.name, label: stream.name,
value: stream.name, value: stream.name,
})); }));
}, [streams]); }, [streams]);
const topicItems = useMemo(() => { const topicItems = useMemo(() => {
return (topics || []).map((topic: Topic) => ({ return topics.map(({ name }) => ({
label: topic.name, label: name,
value: topic.name, value: name,
})); }));
}, [topics]); }, [topics]);

View File

@@ -41,7 +41,7 @@ const useParticipants = (transcriptId: string): UseParticipants => {
loading: false, loading: false,
response: null, response: null,
refetch, refetch,
} as ErrorParticipants & { refetch: () => void }; } satisfies ErrorParticipants & { refetch: () => void };
} }
if (loading || !response) { if (loading || !response) {
@@ -50,7 +50,7 @@ const useParticipants = (transcriptId: string): UseParticipants => {
loading: true, loading: true,
error: null, error: null,
refetch, refetch,
} as LoadingParticipants & { refetch: () => void }; } satisfies LoadingParticipants & { refetch: () => void };
} }
return { return {
@@ -58,7 +58,7 @@ const useParticipants = (transcriptId: string): UseParticipants => {
loading: false, loading: false,
error: null, error: null,
refetch, refetch,
} as SuccessParticipants & { refetch: () => void }; } satisfies SuccessParticipants & { refetch: () => void };
}; };
export default useParticipants; export default useParticipants;

View File

@@ -42,14 +42,13 @@ const useTopicWithWords = (
topicId || null, topicId || null,
); );
// Type-safe return based on state
if (error) { if (error) {
return { return {
error: error as Error, error: error as Error,
loading: false, loading: false,
response: null, response: null,
refetch, refetch,
} as ErrorTopicWithWords & { refetch: () => void }; } satisfies ErrorTopicWithWords & { refetch: () => void };
} }
if (loading || !response) { if (loading || !response) {
@@ -58,7 +57,7 @@ const useTopicWithWords = (
loading: true, loading: true,
error: false, error: false,
refetch, refetch,
} as LoadingTopicWithWords & { refetch: () => void }; } satisfies LoadingTopicWithWords & { refetch: () => void };
} }
return { return {
@@ -66,7 +65,7 @@ const useTopicWithWords = (
loading: false, loading: false,
error: null, error: null,
refetch, refetch,
} as SuccessTopicWithWords & { refetch: () => void }; } satisfies SuccessTopicWithWords & { refetch: () => void };
}; };
export default useTopicWithWords; export default useTopicWithWords;

View File

@@ -59,7 +59,7 @@ const useTranscript = (
} }
return { return {
response: data as GetTranscript, response: data,
loading: false, loading: false,
error: null, error: null,
reload: refetch, reload: refetch,

View File

@@ -11,7 +11,7 @@ const useWebRTC = (
): Peer => { ): Peer => {
const [peer, setPeer] = useState<Peer | null>(null); const [peer, setPeer] = useState<Peer | null>(null);
const { setError } = useError(); const { setError } = useError();
const webRTCMutation = useTranscriptWebRTC(); const { mutateAsync: mutateWebRtcTranscriptAsync } = useTranscriptWebRTC();
useEffect(() => { useEffect(() => {
if (!stream || !transcriptId) { if (!stream || !transcriptId) {
@@ -41,7 +41,7 @@ const useWebRTC = (
}; };
try { try {
const answer = await webRTCMutation.mutateAsync({ const answer = await mutateWebRtcTranscriptAsync({
params: { params: {
path: { path: {
transcript_id: transcriptId, transcript_id: transcriptId,
@@ -69,7 +69,7 @@ const useWebRTC = (
return () => { return () => {
p.destroy(); p.destroy();
}; };
}, [stream, transcriptId, webRTCMutation]); }, [stream, transcriptId, mutateWebRtcTranscriptAsync]);
return peer; return peer;
}; };

View File

@@ -3,7 +3,7 @@
import { $api } from "./apiClient"; import { $api } from "./apiClient";
import { useError } from "../(errors)/errorContext"; import { useError } from "../(errors)/errorContext";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import type { paths } from "../reflector-api"; import type { components, paths } from "../reflector-api";
import useAuthReady from "./useAuthReady"; import useAuthReady from "./useAuthReady";
// FIXME: React Query caching issues with cross-tab synchronization // FIXME: React Query caching issues with cross-tab synchronization
@@ -23,7 +23,6 @@ import useAuthReady from "./useAuthReady";
const STALE_TIME = 500; const STALE_TIME = 500;
export function useRoomsList(page: number = 1) { export function useRoomsList(page: number = 1) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(
@@ -41,16 +40,17 @@ export function useRoomsList(page: number = 1) {
); );
} }
type SourceKind = components["schemas"]["SourceKind"];
export function useTranscriptsSearch( export function useTranscriptsSearch(
q: string = "", q: string = "",
options: { options: {
limit?: number; limit?: number;
offset?: number; offset?: number;
room_id?: string; room_id?: string;
source_kind?: string; source_kind?: SourceKind;
} = {}, } = {},
) { ) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(
@@ -63,7 +63,7 @@ export function useTranscriptsSearch(
limit: options.limit, limit: options.limit,
offset: options.offset, offset: options.offset,
room_id: options.room_id, 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) { export function useTranscriptGet(transcriptId: string | null) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(
@@ -170,12 +169,11 @@ export function useRoomDelete() {
} }
export function useZulipStreams() { export function useZulipStreams() {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(
"get", "get",
"/v1/zulip/streams" as any, "/v1/zulip/streams",
{}, {},
{ {
enabled: isAuthReady, enabled: isAuthReady,
@@ -185,15 +183,20 @@ export function useZulipStreams() {
} }
export function useZulipTopics(streamId: number | null) { export function useZulipTopics(streamId: number | null) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
const enabled = !!streamId && isAuthReady;
return $api.useQuery( return $api.useQuery(
"get", "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, staleTime: STALE_TIME,
}, },
); );
@@ -223,15 +226,11 @@ export function useTranscriptPostToZulip() {
const { setError } = useError(); const { setError } = useError();
// @ts-ignore - Zulip endpoint not in OpenAPI spec // @ts-ignore - Zulip endpoint not in OpenAPI spec
return $api.useMutation( return $api.useMutation("post", "/v1/transcripts/{transcript_id}/zulip", {
"post", onError: (error) => {
"/v1/transcripts/{transcript_id}/zulip" as any, setError(error as Error, "There was an error posting to Zulip");
{
onError: (error) => {
setError(error as Error, "There was an error posting to Zulip");
},
}, },
); });
} }
export function useTranscriptUploadAudio() { export function useTranscriptUploadAudio() {
@@ -263,7 +262,6 @@ export function useTranscriptUploadAudio() {
} }
export function useTranscriptWaveform(transcriptId: string | null) { export function useTranscriptWaveform(transcriptId: string | null) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(
@@ -282,7 +280,6 @@ export function useTranscriptWaveform(transcriptId: string | null) {
} }
export function useTranscriptMP3(transcriptId: string | null) { export function useTranscriptMP3(transcriptId: string | null) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(
@@ -301,7 +298,6 @@ export function useTranscriptMP3(transcriptId: string | null) {
} }
export function useTranscriptTopics(transcriptId: string | null) { export function useTranscriptTopics(transcriptId: string | null) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(
@@ -320,7 +316,6 @@ export function useTranscriptTopics(transcriptId: string | null) {
} }
export function useTranscriptTopicsWithWords(transcriptId: string | null) { export function useTranscriptTopicsWithWords(transcriptId: string | null) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(
@@ -342,7 +337,6 @@ export function useTranscriptTopicsWithWordsPerSpeaker(
transcriptId: string | null, transcriptId: string | null,
topicId: string | null, topicId: string | null,
) { ) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(
@@ -364,7 +358,6 @@ export function useTranscriptTopicsWithWordsPerSpeaker(
} }
export function useTranscriptParticipants(transcriptId: string | null) { export function useTranscriptParticipants(transcriptId: string | null) {
const { setError } = useError();
const { isAuthReady } = useAuthReady(); const { isAuthReady } = useAuthReady();
return $api.useQuery( return $api.useQuery(

View File

@@ -6,7 +6,7 @@ import { Session } from "next-auth";
export default function useSessionStatus() { export default function useSessionStatus() {
const { status: naStatus } = useNextAuthSession(); const { status: naStatus } = useNextAuthSession();
const [status, setStatus] = useState("loading"); const [status, setStatus] = useState<typeof naStatus>("loading");
useEffect(() => { useEffect(() => {
if (naStatus !== "loading" && naStatus !== status) { if (naStatus !== "loading" && naStatus !== status) {