From fbeeff4c4da91f4d44c758f26538ba2736d100aa Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Thu, 28 Aug 2025 10:08:31 -0600 Subject: [PATCH] feat: complete migration from @hey-api/openapi-ts to openapi-react-query - Migrated all components from useApi compatibility layer to direct React Query hooks - Added new hooks for participant operations, room meetings, and speaker operations - Updated all imports from old api module to api-types - Fixed TypeScript types and API endpoint signatures - Removed deprecated useApi.ts compatibility layer - Fixed SourceKind enum values to match OpenAPI spec - Added @ts-ignore for Zulip endpoints not in OpenAPI spec yet - Fixed all compilation errors and type issues --- .../browse/_components/FilterSidebar.tsx | 31 ++- .../browse/_components/TranscriptCards.tsx | 4 +- www/app/(app)/browse/page.tsx | 3 +- www/app/(app)/rooms/page.tsx | 15 +- .../[transcriptId]/correct/page.tsx | 27 ++- .../correct/participantList.tsx | 196 +++++++++-------- .../[transcriptId]/correct/topicHeader.tsx | 2 +- .../[transcriptId]/finalSummary.tsx | 35 +-- www/app/(app)/transcripts/createTranscript.ts | 43 ++-- .../(app)/transcripts/fileUploadButton.tsx | 64 +++--- www/app/(app)/transcripts/new/page.tsx | 16 +- www/app/(app)/transcripts/shareAndPrivacy.tsx | 2 +- www/app/(app)/transcripts/shareZulip.tsx | 139 +++++------- www/app/(app)/transcripts/useMp3.ts | 108 ++++----- www/app/(app)/transcripts/useParticipants.ts | 71 +++--- .../(app)/transcripts/useTopicWithWords.ts | 76 +++---- www/app/(app)/transcripts/useTopics.ts | 40 +--- www/app/(app)/transcripts/useWaveform.ts | 43 +--- www/app/(app)/transcripts/useWebRTC.ts | 41 ++-- www/app/(app)/transcripts/useWebSockets.ts | 48 +++- www/app/[roomName]/page.tsx | 32 +-- www/app/[roomName]/useRoomMeeting.tsx | 39 ++-- www/app/lib/api-hooks.ts | 208 +++++++++++------- www/app/lib/api-types.ts | 3 +- www/app/lib/useApi.ts | 13 -- 25 files changed, 669 insertions(+), 630 deletions(-) delete mode 100644 www/app/lib/useApi.ts diff --git a/www/app/(app)/browse/_components/FilterSidebar.tsx b/www/app/(app)/browse/_components/FilterSidebar.tsx index fd298548..17b4e7e9 100644 --- a/www/app/(app)/browse/_components/FilterSidebar.tsx +++ b/www/app/(app)/browse/_components/FilterSidebar.tsx @@ -10,6 +10,13 @@ interface FilterSidebarProps { onFilterChange: (sourceKind: SourceKind | null, roomId: string) => void; } +// Type helper for source kind literals +const SK = { + room: "room" as SourceKind, + live: "live" as SourceKind, + file: "file" as SourceKind, +}; + export default function FilterSidebar({ rooms, selectedSourceKind, @@ -44,14 +51,14 @@ export default function FilterSidebar({ key={room.id} as={NextLink} href="#" - onClick={() => onFilterChange("room", room.id)} + onClick={() => onFilterChange(SK.room, room.id)} color={ - selectedSourceKind === "room" && selectedRoomId === room.id + selectedSourceKind === SK.room && selectedRoomId === room.id ? "blue.500" : "gray.600" } fontWeight={ - selectedSourceKind === "room" && selectedRoomId === room.id + selectedSourceKind === SK.room && selectedRoomId === room.id ? "bold" : "normal" } @@ -72,14 +79,14 @@ export default function FilterSidebar({ key={room.id} as={NextLink} href="#" - onClick={() => onFilterChange("room", room.id)} + onClick={() => onFilterChange(SK.room, room.id)} color={ - selectedSourceKind === "room" && selectedRoomId === room.id + selectedSourceKind === SK.room && selectedRoomId === room.id ? "blue.500" : "gray.600" } fontWeight={ - selectedSourceKind === "room" && selectedRoomId === room.id + selectedSourceKind === SK.room && selectedRoomId === room.id ? "bold" : "normal" } @@ -95,10 +102,10 @@ export default function FilterSidebar({ onFilterChange("live", "")} - color={selectedSourceKind === "live" ? "blue.500" : "gray.600"} + onClick={() => onFilterChange(SK.live, "")} + color={selectedSourceKind === SK.live ? "blue.500" : "gray.600"} _hover={{ color: "blue.300" }} - fontWeight={selectedSourceKind === "live" ? "bold" : "normal"} + fontWeight={selectedSourceKind === SK.live ? "bold" : "normal"} fontSize="sm" > Live Transcripts @@ -106,10 +113,10 @@ export default function FilterSidebar({ onFilterChange("file", "")} - color={selectedSourceKind === "file" ? "blue.500" : "gray.600"} + onClick={() => onFilterChange(SK.file, "")} + color={selectedSourceKind === SK.file ? "blue.500" : "gray.600"} _hover={{ color: "blue.300" }} - fontWeight={selectedSourceKind === "file" ? "bold" : "normal"} + fontWeight={selectedSourceKind === SK.file ? "bold" : "normal"} fontSize="sm" > Uploaded Files diff --git a/www/app/(app)/browse/_components/TranscriptCards.tsx b/www/app/(app)/browse/_components/TranscriptCards.tsx index bb1843f1..f417ccc8 100644 --- a/www/app/(app)/browse/_components/TranscriptCards.tsx +++ b/www/app/(app)/browse/_components/TranscriptCards.tsx @@ -18,7 +18,7 @@ import { highlightMatches, generateTextFragment, } from "../../../lib/textHighlight"; -import { SearchResult } from "../../../lib/api-types"; +import { SearchResult, SourceKind } from "../../../lib/api-types"; interface TranscriptCardsProps { results: SearchResult[]; @@ -120,7 +120,7 @@ function TranscriptCard({ : "N/A"; const formattedDate = formatLocalDate(result.created_at); const source = - result.source_kind === "room" + result.source_kind === ("room" as SourceKind) ? result.room_name || result.room_id : result.source_kind; diff --git a/www/app/(app)/browse/page.tsx b/www/app/(app)/browse/page.tsx index 93620550..28ef9b30 100644 --- a/www/app/(app)/browse/page.tsx +++ b/www/app/(app)/browse/page.tsx @@ -204,7 +204,7 @@ export default function TranscriptBrowser() { const [urlSourceKind, setUrlSourceKind] = useQueryState( "source", - parseAsStringLiteral($SourceKind.enum).withOptions({ + parseAsStringLiteral($SourceKind.values).withOptions({ shallow: false, }), ); @@ -302,7 +302,6 @@ export default function TranscriptBrowser() { params: { path: { transcript_id: transcriptId }, }, - body: {}, }, { onSuccess: (result) => { diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index 0a8f68d8..e4c72aa6 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -16,7 +16,7 @@ import { } from "@chakra-ui/react"; import { useEffect, useState } from "react"; import useRoomList from "./useRoomList"; -import { ApiError, Room } from "../../lib/api-types"; +import { Room } from "../../lib/api-types"; import { useRoomCreate, useRoomUpdate, @@ -92,8 +92,10 @@ export default function RoomsList() { const createRoomMutation = useRoomCreate(); const updateRoomMutation = useRoomUpdate(); const deleteRoomMutation = useRoomDelete(); - const { data: streams = [] } = useZulipStreams(); - const { data: topics = [] } = useZulipTopics(selectedStreamId); + const { data: streams = [] } = useZulipStreams() as { data: any[] }; + const { data: topics = [] } = useZulipTopics(selectedStreamId) as { + data: Topic[]; + }; interface Topic { name: string; } @@ -177,11 +179,10 @@ export default function RoomsList() { setNameError(""); refetch(); onClose(); - } catch (err) { + } catch (err: any) { if ( - err instanceof ApiError && - err.status === 400 && - (err.body as any).detail == "Room name is not unique" + err?.status === 400 && + err?.body?.detail == "Room name is not unique" ) { setNameError( "This room name is already taken. Please choose a different name.", diff --git a/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx b/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx index 9eff7b60..839a48cc 100644 --- a/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx @@ -6,9 +6,9 @@ import TopicPlayer from "./topicPlayer"; import useParticipants from "../../useParticipants"; import useTopicWithWords from "../../useTopicWithWords"; import ParticipantList from "./participantList"; -import { GetTranscriptTopic } from "../../../../api"; +import { GetTranscriptTopic } from "../../../../lib/api-types"; import { SelectedText, selectedTextIsTimeSlice } from "./types"; -import useApi from "../../../../lib/useApi"; +import { useTranscriptUpdate } from "../../../../lib/api-hooks"; import useTranscript from "../../useTranscript"; import { useError } from "../../../../(errors)/errorContext"; import { useRouter } from "next/navigation"; @@ -23,7 +23,7 @@ export type TranscriptCorrect = { export default function TranscriptCorrect({ params: { transcriptId }, }: TranscriptCorrect) { - const api = useApi(); + const updateTranscriptMutation = useTranscriptUpdate(); const transcript = useTranscript(transcriptId); const stateCurrentTopic = useState(); const [currentTopic, _sct] = stateCurrentTopic; @@ -34,16 +34,21 @@ export default function TranscriptCorrect({ const { setError } = useError(); const router = useRouter(); - const markAsDone = () => { + const markAsDone = async () => { if (transcript.response && !transcript.response.reviewed) { - api - ?.v1TranscriptUpdate({ transcriptId, requestBody: { reviewed: true } }) - .then(() => { - router.push(`/transcripts/${transcriptId}`); - }) - .catch((e) => { - setError(e, "Error marking as done"); + try { + await updateTranscriptMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { reviewed: true }, }); + router.push(`/transcripts/${transcriptId}`); + } catch (e) { + setError(e as Error, "Error marking as done"); + } } }; diff --git a/www/app/(app)/transcripts/[transcriptId]/correct/participantList.tsx b/www/app/(app)/transcripts/[transcriptId]/correct/participantList.tsx index e9297c4b..5cef29f9 100644 --- a/www/app/(app)/transcripts/[transcriptId]/correct/participantList.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/correct/participantList.tsx @@ -1,8 +1,14 @@ import { faArrowTurnDown } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ChangeEvent, useEffect, useRef, useState } from "react"; -import { Participant } from "../../../../api"; -import useApi from "../../../../lib/useApi"; +import { Participant } from "../../../../lib/api-types"; +import { + useTranscriptSpeakerAssign, + useTranscriptSpeakerMerge, + useTranscriptParticipantUpdate, + useTranscriptParticipantCreate, + useTranscriptParticipantDelete, +} from "../../../../lib/api-hooks"; import { UseParticipants } from "../../useParticipants"; import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./types"; import { useError } from "../../../../(errors)/errorContext"; @@ -30,9 +36,19 @@ const ParticipantList = ({ topicWithWords, stateSelectedText, }: ParticipantList) => { - const api = useApi(); const { setError } = useError(); - const [loading, setLoading] = useState(false); + const speakerAssignMutation = useTranscriptSpeakerAssign(); + const speakerMergeMutation = useTranscriptSpeakerMerge(); + const participantUpdateMutation = useTranscriptParticipantUpdate(); + const participantCreateMutation = useTranscriptParticipantCreate(); + const participantDeleteMutation = useTranscriptParticipantDelete(); + + const loading = + speakerAssignMutation.isPending || + speakerMergeMutation.isPending || + participantUpdateMutation.isPending || + participantCreateMutation.isPending || + participantDeleteMutation.isPending; const [participantInput, setParticipantInput] = useState(""); const inputRef = useRef(null); const [selectedText, setSelectedText] = stateSelectedText; @@ -103,7 +119,6 @@ const ParticipantList = ({ const onSuccess = () => { topicWithWords.refetch(); participants.refetch(); - setLoading(false); setAction(null); setSelectedText(undefined); setSelectedParticipant(undefined); @@ -120,11 +135,14 @@ const ParticipantList = ({ if (loading || participants.loading || topicWithWords.loading) return; if (!selectedTextIsTimeSlice(selectedText)) return; - setLoading(true); try { - await api?.v1TranscriptAssignSpeaker({ - transcriptId, - requestBody: { + await speakerAssignMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { participant: participant.id, timestamp_from: selectedText.start, timestamp_to: selectedText.end, @@ -132,8 +150,7 @@ const ParticipantList = ({ }); onSuccess(); } catch (error) { - setError(error, "There was an error assigning"); - setLoading(false); + setError(error as Error, "There was an error assigning"); throw error; } }; @@ -141,32 +158,38 @@ const ParticipantList = ({ const mergeSpeaker = (speakerFrom, participantTo: Participant) => async () => { if (loading || participants.loading || topicWithWords.loading) return; - setLoading(true); + if (participantTo.speaker) { try { - await api?.v1TranscriptMergeSpeaker({ - transcriptId, - requestBody: { + await speakerMergeMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { speaker_from: speakerFrom, speaker_to: participantTo.speaker, }, }); onSuccess(); } catch (error) { - setError(error, "There was an error merging"); - setLoading(false); + setError(error as Error, "There was an error merging"); } } else { try { - await api?.v1TranscriptUpdateParticipant({ - transcriptId, - participantId: participantTo.id, - requestBody: { speaker: speakerFrom }, + await participantUpdateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + participant_id: participantTo.id, + }, + }, + body: { speaker: speakerFrom }, }); onSuccess(); } catch (error) { - setError(error, "There was an error merging (update)"); - setLoading(false); + setError(error as Error, "There was an error merging (update)"); } } }; @@ -186,105 +209,106 @@ const ParticipantList = ({ (p) => p.speaker == selectedText, ); if (participant && participant.name !== participantInput) { - setLoading(true); - api - ?.v1TranscriptUpdateParticipant({ - transcriptId, - participantId: participant.id, - requestBody: { + try { + await participantUpdateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + participant_id: participant.id, + }, + }, + body: { name: participantInput, }, - }) - .then(() => { - participants.refetch(); - setLoading(false); - setAction(null); - }) - .catch((e) => { - setError(e, "There was an error renaming"); - setLoading(false); }); + participants.refetch(); + setAction(null); + } catch (e) { + setError(e as Error, "There was an error renaming"); + } } } else if ( action == "Create to rename" && selectedTextIsSpeaker(selectedText) ) { - setLoading(true); - api - ?.v1TranscriptAddParticipant({ - transcriptId, - requestBody: { + try { + await participantCreateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { name: participantInput, speaker: selectedText, }, - }) - .then(() => { - participants.refetch(); - setParticipantInput(""); - setOneMatch(undefined); - setLoading(false); - }) - .catch((e) => { - setError(e, "There was an error creating"); - setLoading(false); }); + participants.refetch(); + setParticipantInput(""); + setOneMatch(undefined); + } catch (e) { + setError(e as Error, "There was an error creating"); + } } else if ( action == "Create and assign" && selectedTextIsTimeSlice(selectedText) ) { - setLoading(true); try { - const participant = await api?.v1TranscriptAddParticipant({ - transcriptId, - requestBody: { + const participant = await participantCreateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { name: participantInput, }, }); - setLoading(false); assignTo(participant)().catch(() => { // error and loading are handled by assignTo catch participants.refetch(); }); } catch (error) { - setError(e, "There was an error creating"); - setLoading(false); + setError(error as Error, "There was an error creating"); } } else if (action == "Create") { - setLoading(true); - api - ?.v1TranscriptAddParticipant({ - transcriptId, - requestBody: { + try { + await participantCreateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { name: participantInput, }, - }) - .then(() => { - participants.refetch(); - setParticipantInput(""); - setLoading(false); - inputRef.current?.focus(); - }) - .catch((e) => { - setError(e, "There was an error creating"); - setLoading(false); }); + participants.refetch(); + setParticipantInput(""); + inputRef.current?.focus(); + } catch (e) { + setError(e as Error, "There was an error creating"); + } } }; - const deleteParticipant = (participantId) => (e) => { + const deleteParticipant = (participantId) => async (e) => { e.stopPropagation(); if (loading || participants.loading || topicWithWords.loading) return; - setLoading(true); - api - ?.v1TranscriptDeleteParticipant({ transcriptId, participantId }) - .then(() => { - participants.refetch(); - setLoading(false); - }) - .catch((e) => { - setError(e, "There was an error deleting"); - setLoading(false); + + try { + await participantDeleteMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + participant_id: participantId, + }, + }, }); + participants.refetch(); + } catch (e) { + setError(e as Error, "There was an error deleting"); + } }; const selectParticipant = (participant) => (e) => { diff --git a/www/app/(app)/transcripts/[transcriptId]/correct/topicHeader.tsx b/www/app/(app)/transcripts/[transcriptId]/correct/topicHeader.tsx index 1448de80..3bd3a1cc 100644 --- a/www/app/(app)/transcripts/[transcriptId]/correct/topicHeader.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/correct/topicHeader.tsx @@ -1,6 +1,6 @@ import useTopics from "../../useTopics"; import { Dispatch, SetStateAction, useEffect } from "react"; -import { GetTranscriptTopic } from "../../../../api"; +import { GetTranscriptTopic } from "../../../../lib/api-types"; import { BoxProps, Box, diff --git a/www/app/(app)/transcripts/[transcriptId]/finalSummary.tsx b/www/app/(app)/transcripts/[transcriptId]/finalSummary.tsx index 5f8525b1..b59b5bf1 100644 --- a/www/app/(app)/transcripts/[transcriptId]/finalSummary.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/finalSummary.tsx @@ -2,12 +2,8 @@ import { useEffect, useRef, useState } from "react"; import React from "react"; import Markdown from "react-markdown"; import "../../../styles/markdown.css"; -import { - GetTranscript, - GetTranscriptTopic, - UpdateTranscript, -} from "../../../lib/api-types"; -import useApi from "../../../lib/useApi"; +import { GetTranscript, GetTranscriptTopic } from "../../../lib/api-types"; +import { useTranscriptUpdate } from "../../../lib/api-hooks"; import { Flex, Heading, @@ -33,9 +29,8 @@ export default function FinalSummary(props: FinalSummaryProps) { const [preEditSummary, setPreEditSummary] = useState(""); const [editedSummary, setEditedSummary] = useState(""); - const api = useApi(); - const { setError } = useError(); + const updateTranscriptMutation = useTranscriptUpdate(); useEffect(() => { setEditedSummary(props.transcriptResponse?.long_summary || ""); @@ -47,12 +42,15 @@ export default function FinalSummary(props: FinalSummaryProps) { const updateSummary = async (newSummary: string, transcriptId: string) => { try { - const requestBody: UpdateTranscript = { - long_summary: newSummary, - }; - const updatedTranscript = await api?.v1TranscriptUpdate({ - transcriptId, - requestBody, + const updatedTranscript = await updateTranscriptMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { + long_summary: newSummary, + }, }); if (props.onUpdate) { props.onUpdate(newSummary); @@ -60,7 +58,7 @@ export default function FinalSummary(props: FinalSummaryProps) { console.log("Updated long summary:", updatedTranscript); } catch (err) { console.error("Failed to update long summary:", err); - setError(err, "Failed to update long summary."); + setError(err as Error, "Failed to update long summary."); } }; @@ -114,7 +112,12 @@ export default function FinalSummary(props: FinalSummaryProps) { - + )} {!isEditMode && ( diff --git a/www/app/(app)/transcripts/createTranscript.ts b/www/app/(app)/transcripts/createTranscript.ts index dac50719..1b57f870 100644 --- a/www/app/(app)/transcripts/createTranscript.ts +++ b/www/app/(app)/transcripts/createTranscript.ts @@ -1,45 +1,30 @@ -import { useEffect, useState } from "react"; - -import { useError } from "../../(errors)/errorContext"; import { CreateTranscript, GetTranscript } from "../../lib/api-types"; -import useApi from "../../lib/useApi"; +import { useTranscriptCreate } from "../../lib/api-hooks"; type UseCreateTranscript = { transcript: GetTranscript | null; loading: boolean; error: Error | null; - create: (transcriptCreationDetails: CreateTranscript) => void; + create: (transcriptCreationDetails: CreateTranscript) => Promise; }; const useCreateTranscript = (): UseCreateTranscript => { - const [transcript, setTranscript] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); + const createMutation = useTranscriptCreate(); - const create = (transcriptCreationDetails: CreateTranscript) => { - if (loading || !api) return; + const create = async (transcriptCreationDetails: CreateTranscript) => { + if (createMutation.isPending) return; - setLoading(true); - - api - .v1TranscriptsCreate({ requestBody: transcriptCreationDetails }) - .then((transcript) => { - setTranscript(transcript); - setLoading(false); - }) - .catch((err) => { - setError( - err, - "There was an issue creating a transcript, please try again.", - ); - setErrorState(err); - setLoading(false); - }); + await createMutation.mutateAsync({ + body: transcriptCreationDetails, + }); }; - return { transcript, loading, error, create }; + return { + transcript: createMutation.data || null, + loading: createMutation.isPending, + error: createMutation.error as Error | null, + create, + }; }; export default useCreateTranscript; diff --git a/www/app/(app)/transcripts/fileUploadButton.tsx b/www/app/(app)/transcripts/fileUploadButton.tsx index 1b4101e8..7067a1e6 100644 --- a/www/app/(app)/transcripts/fileUploadButton.tsx +++ b/www/app/(app)/transcripts/fileUploadButton.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; -import useApi from "../../lib/useApi"; +import { useTranscriptUploadAudio } from "../../lib/api-hooks"; import { Button, Spinner } from "@chakra-ui/react"; +import { useError } from "../../(errors)/errorContext"; type FileUploadButton = { transcriptId: string; @@ -8,13 +9,16 @@ type FileUploadButton = { export default function FileUploadButton(props: FileUploadButton) { const fileInputRef = React.useRef(null); - const api = useApi(); + const uploadMutation = useTranscriptUploadAudio(); + const { setError } = useError(); const [progress, setProgress] = useState(0); const triggerFileUpload = () => { fileInputRef.current?.click(); }; - const handleFileUpload = (event: React.ChangeEvent) => { + const handleFileUpload = async ( + event: React.ChangeEvent, + ) => { const file = event.target.files?.[0]; if (file) { @@ -24,37 +28,45 @@ export default function FileUploadButton(props: FileUploadButton) { let start = 0; let uploadedSize = 0; - api?.httpRequest.config.interceptors.request.use((request) => { - request.onUploadProgress = (progressEvent) => { - const currentProgress = Math.floor( - ((uploadedSize + progressEvent.loaded) / file.size) * 100, - ); - setProgress(currentProgress); - }; - return request; - }); - const uploadNextChunk = async () => { - if (chunkNumber == totalChunks) return; + if (chunkNumber == totalChunks) { + setProgress(0); + return; + } const chunkSize = Math.min(maxChunkSize, file.size - start); const end = start + chunkSize; const chunk = file.slice(start, end); - await api?.v1TranscriptRecordUpload({ - transcriptId: props.transcriptId, - formData: { - chunk, - }, - chunkNumber, - totalChunks, - }); + try { + const formData = new FormData(); + formData.append("chunk", chunk); - uploadedSize += chunkSize; - chunkNumber++; - start = end; + await uploadMutation.mutateAsync({ + params: { + path: { + transcript_id: props.transcriptId, + }, + query: { + chunk_number: chunkNumber, + total_chunks: totalChunks, + }, + }, + body: formData as any, + }); - uploadNextChunk(); + uploadedSize += chunkSize; + const currentProgress = Math.floor((uploadedSize / file.size) * 100); + setProgress(currentProgress); + + chunkNumber++; + start = end; + + await uploadNextChunk(); + } catch (error) { + setError(error as Error, "Failed to upload file"); + setProgress(0); + } }; uploadNextChunk(); diff --git a/www/app/(app)/transcripts/new/page.tsx b/www/app/(app)/transcripts/new/page.tsx index 698ac47b..50b80b17 100644 --- a/www/app/(app)/transcripts/new/page.tsx +++ b/www/app/(app)/transcripts/new/page.tsx @@ -54,20 +54,30 @@ const TranscriptCreate = () => { const [loadingUpload, setLoadingUpload] = useState(false); const getTargetLanguage = () => { - if (targetLanguage === "NOTRANSLATION") return; + if (targetLanguage === "NOTRANSLATION") return undefined; return targetLanguage; }; const send = () => { if (loadingRecord || createTranscript.loading || permissionDenied) return; setLoadingRecord(true); - createTranscript.create({ name, target_language: getTargetLanguage() }); + const targetLang = getTargetLanguage(); + createTranscript.create({ + name, + source_language: "en", + target_language: targetLang || "en", + }); }; const uploadFile = () => { if (loadingUpload || createTranscript.loading || permissionDenied) return; setLoadingUpload(true); - createTranscript.create({ name, target_language: getTargetLanguage() }); + const targetLang = getTargetLanguage(); + createTranscript.create({ + name, + source_language: "en", + target_language: targetLang || "en", + }); }; useEffect(() => { diff --git a/www/app/(app)/transcripts/shareAndPrivacy.tsx b/www/app/(app)/transcripts/shareAndPrivacy.tsx index 4c46630c..b9102472 100644 --- a/www/app/(app)/transcripts/shareAndPrivacy.tsx +++ b/www/app/(app)/transcripts/shareAndPrivacy.tsx @@ -132,7 +132,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) { "This transcript is public. Everyone can access it."} - {isOwner && api && ( + {isOwner && ( (undefined); + const [selectedStreamId, setSelectedStreamId] = useState(null); const [topic, setTopic] = useState(undefined); const [includeTopics, setIncludeTopics] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [streams, setStreams] = useState([]); - const [topics, setTopics] = useState([]); - const api = useApi(); + + // React Query hooks + const { data: streams = [], isLoading: isLoadingStreams } = + useZulipStreams() as { data: Stream[]; isLoading: boolean }; + const { data: topics = [] } = useZulipTopics(selectedStreamId) as { + data: Topic[]; + }; + const postToZulipMutation = useTranscriptPostToZulip(); + const { contains } = useFilter({ sensitivity: "base" }); - const { - collection: streamItemsCollection, - filter: streamItemsFilter, - set: streamItemsSet, - } = useListCollection({ - initialItems: [] as { label: string; value: string }[], - filter: contains, - }); + const streamItems = useMemo(() => { + return (streams || []).map((stream: Stream) => ({ + label: stream.name, + value: stream.name, + })); + }, [streams]); - const { - collection: topicItemsCollection, - filter: topicItemsFilter, - set: topicItemsSet, - } = useListCollection({ - initialItems: [] as { label: string; value: string }[], - filter: contains, - }); + const topicItems = useMemo(() => { + return (topics || []).map((topic: Topic) => ({ + label: topic.name, + value: topic.name, + })); + }, [topics]); + const { collection: streamItemsCollection, filter: streamItemsFilter } = + useListCollection({ + initialItems: streamItems, + filter: contains, + }); + + const { collection: topicItemsCollection, filter: topicItemsFilter } = + useListCollection({ + initialItems: topicItems, + filter: contains, + }); + + // Update selected stream ID when stream changes useEffect(() => { - const fetchZulipStreams = async () => { - if (!api) return; - - try { - const response = await api.v1ZulipGetStreams(); - setStreams(response); - - streamItemsSet( - response.map((stream) => ({ - label: stream.name, - value: stream.name, - })), - ); - - 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); - topicItemsSet( - response.map((topic) => ({ - label: topic.name, - value: topic.name, - })), - ); - } else { - topicItemsSet([]); - } - } catch (error) { - console.error("Error fetching Zulip topics:", error); - } - }; - - fetchZulipTopics(); - }, [stream, streams, api]); + if (stream && streams) { + const selectedStream = streams.find((s: Stream) => s.name === stream); + setSelectedStreamId(selectedStream ? selectedStream.stream_id : null); + } else { + setSelectedStreamId(null); + } + }, [stream, streams]); const handleSendToZulip = async () => { - if (!api || !props.transcriptResponse) return; + if (!props.transcriptResponse) return; if (stream && topic) { try { - await api.v1TranscriptPostToZulip({ - transcriptId: props.transcriptResponse.id, - stream, - topic, - includeTopics, + await postToZulipMutation.mutateAsync({ + params: { + path: { + transcript_id: props.transcriptResponse.id, + }, + query: { + stream, + topic, + include_topics: includeTopics, + }, + }, }); setShowModal(false); } catch (error) { - console.log(error); + console.error("Error posting to Zulip:", error); } } }; @@ -155,7 +138,7 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) { - {isLoading ? ( + {isLoadingStreams ? ( diff --git a/www/app/(app)/transcripts/useMp3.ts b/www/app/(app)/transcripts/useMp3.ts index 3e8344ad..bf35fa6e 100644 --- a/www/app/(app)/transcripts/useMp3.ts +++ b/www/app/(app)/transcripts/useMp3.ts @@ -1,6 +1,7 @@ import { useContext, useEffect, useState } from "react"; import { DomainContext } from "../../domainContext"; -import getApi from "../../lib/useApi"; +import { useTranscriptGet } from "../../lib/api-hooks"; +import { useSession } from "next-auth/react"; export type Mp3Response = { media: HTMLMediaElement | null; @@ -17,14 +18,17 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => { const [audioLoadingError, setAudioLoadingError] = useState( null, ); - const [transcriptMetadataLoading, setTranscriptMetadataLoading] = - useState(true); - const [transcriptMetadataLoadingError, setTranscriptMetadataLoadingError] = - useState(null); const [audioDeleted, setAudioDeleted] = useState(null); - const api = getApi(); const { api_url } = useContext(DomainContext); - const accessTokenInfo = api?.httpRequest?.config?.TOKEN; + const { data: session } = useSession(); + const accessTokenInfo = (session as any)?.accessToken as string | undefined; + + // Use React Query to fetch transcript metadata + const { + data: transcript, + isLoading: transcriptMetadataLoading, + error: transcriptError, + } = useTranscriptGet(later ? null : transcriptId); const [serviceWorker, setServiceWorker] = useState(null); @@ -52,72 +56,50 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => { }, [navigator.serviceWorker, !serviceWorker, accessTokenInfo]); useEffect(() => { - if (!transcriptId || !api || later) return; + if (!transcriptId || later || !transcript) return; let stopped = false; let audioElement: HTMLAudioElement | null = null; let handleCanPlay: (() => void) | null = null; let handleError: (() => void) | null = null; - setTranscriptMetadataLoading(true); setAudioLoading(true); - // First fetch transcript info to check if audio is deleted - api - .v1TranscriptGet({ transcriptId }) - .then((transcript) => { - if (stopped) { - return; - } + const deleted = transcript.audio_deleted || false; + setAudioDeleted(deleted); - const deleted = transcript.audio_deleted || false; - setAudioDeleted(deleted); - setTranscriptMetadataLoadingError(null); + if (deleted) { + // Audio is deleted, don't attempt to load it + setMedia(null); + setAudioLoadingError(null); + setAudioLoading(false); + return; + } - if (deleted) { - // Audio is deleted, don't attempt to load it - setMedia(null); - setAudioLoadingError(null); - setAudioLoading(false); - return; - } + // Audio is not deleted, proceed to load it + audioElement = document.createElement("audio"); + audioElement.src = `${api_url}/v1/transcripts/${transcriptId}/audio/mp3`; + audioElement.crossOrigin = "anonymous"; + audioElement.preload = "auto"; - // Audio is not deleted, proceed to load it - audioElement = document.createElement("audio"); - audioElement.src = `${api_url}/v1/transcripts/${transcriptId}/audio/mp3`; - audioElement.crossOrigin = "anonymous"; - audioElement.preload = "auto"; + handleCanPlay = () => { + if (stopped) return; + setAudioLoading(false); + setAudioLoadingError(null); + }; - handleCanPlay = () => { - if (stopped) return; - setAudioLoading(false); - setAudioLoadingError(null); - }; + handleError = () => { + if (stopped) return; + setAudioLoading(false); + setAudioLoadingError("Failed to load audio"); + }; - handleError = () => { - if (stopped) return; - setAudioLoading(false); - setAudioLoadingError("Failed to load audio"); - }; + audioElement.addEventListener("canplay", handleCanPlay); + audioElement.addEventListener("error", handleError); - audioElement.addEventListener("canplay", handleCanPlay); - audioElement.addEventListener("error", handleError); - - if (!stopped) { - setMedia(audioElement); - } - }) - .catch((error) => { - if (stopped) return; - console.error("Failed to fetch transcript:", error); - setAudioDeleted(null); - setTranscriptMetadataLoadingError(error.message); - setAudioLoading(false); - }) - .finally(() => { - if (stopped) return; - setTranscriptMetadataLoading(false); - }); + if (!stopped) { + setMedia(audioElement); + } return () => { stopped = true; @@ -128,14 +110,18 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => { if (handleError) audioElement.removeEventListener("error", handleError); } }; - }, [transcriptId, api, later, api_url]); + }, [transcriptId, transcript, later, api_url]); const getNow = () => { setLater(false); }; const loading = audioLoading || transcriptMetadataLoading; - const error = audioLoadingError || transcriptMetadataLoadingError; + const error = + audioLoadingError || + (transcriptError + ? (transcriptError as any).message || String(transcriptError) + : null); return { media, loading, error, getNow, audioDeleted }; }; diff --git a/www/app/(app)/transcripts/useParticipants.ts b/www/app/(app)/transcripts/useParticipants.ts index 7a576d7f..014a0cf1 100644 --- a/www/app/(app)/transcripts/useParticipants.ts +++ b/www/app/(app)/transcripts/useParticipants.ts @@ -1,8 +1,5 @@ -import { useEffect, useState } from "react"; import { Participant } from "../../lib/api-types"; -import { useError } from "../../(errors)/errorContext"; -import useApi from "../../lib/useApi"; -import { shouldShowError } from "../../lib/errorUtils"; +import { useTranscriptParticipants } from "../../lib/api-hooks"; type ErrorParticipants = { error: Error; @@ -29,46 +26,38 @@ export type UseParticipants = ( ) & { refetch: () => void }; const useParticipants = (transcriptId: string): UseParticipants => { - const [response, setResponse] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); - const [count, setCount] = useState(0); + const { + data: response, + isLoading: loading, + error, + refetch, + } = useTranscriptParticipants(transcriptId || null); - const refetch = () => { - if (!loading) { - setCount(count + 1); - setLoading(true); - setErrorState(null); - } - }; + // Type-safe return based on state + if (error) { + return { + error: error as Error, + loading: false, + response: null, + refetch, + } as ErrorParticipants & { refetch: () => void }; + } - useEffect(() => { - if (!transcriptId || !api) return; + if (loading || !response) { + return { + response: response || null, + loading: true, + error: null, + refetch, + } as LoadingParticipants & { refetch: () => void }; + } - setLoading(true); - api - .v1TranscriptGetParticipants({ transcriptId }) - .then((result) => { - setResponse(result); - setLoading(false); - console.debug("Participants Loaded:", result); - }) - .catch((error) => { - const shouldShowHuman = shouldShowError(error); - if (shouldShowHuman) { - setError(error, "There was an error loading the participants"); - } else { - setError(error); - } - setErrorState(error); - setResponse(null); - setLoading(false); - }); - }, [transcriptId, !api, count]); - - return { response, loading, error, refetch } as UseParticipants; + return { + response, + loading: false, + error: null, + refetch, + } as SuccessParticipants & { refetch: () => void }; }; export default useParticipants; diff --git a/www/app/(app)/transcripts/useTopicWithWords.ts b/www/app/(app)/transcripts/useTopicWithWords.ts index e895383b..4fc10269 100644 --- a/www/app/(app)/transcripts/useTopicWithWords.ts +++ b/www/app/(app)/transcripts/useTopicWithWords.ts @@ -1,9 +1,5 @@ -import { useEffect, useState } from "react"; - import { GetTranscriptTopicWithWordsPerSpeaker } from "../../lib/api-types"; -import { useError } from "../../(errors)/errorContext"; -import useApi from "../../lib/useApi"; -import { shouldShowError } from "../../lib/errorUtils"; +import { useTranscriptTopicsWithWordsPerSpeaker } from "../../lib/api-hooks"; type ErrorTopicWithWords = { error: Error; @@ -33,47 +29,41 @@ const useTopicWithWords = ( topicId: string | undefined, transcriptId: string, ): UseTopicWithWords => { - const [response, setResponse] = - useState(null); - const [loading, setLoading] = useState(false); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); + const { + data: response, + isLoading: loading, + error, + refetch, + } = useTranscriptTopicsWithWordsPerSpeaker( + transcriptId || null, + topicId || null, + ); - const [count, setCount] = useState(0); + // Type-safe return based on state + if (error) { + return { + error: error as Error, + loading: false, + response: null, + refetch, + } as ErrorTopicWithWords & { refetch: () => void }; + } - const refetch = () => { - if (!loading) { - setCount(count + 1); - setLoading(true); - setErrorState(null); - } - }; + if (loading || !response) { + return { + response: response || null, + loading: true, + error: false, + refetch, + } as LoadingTopicWithWords & { refetch: () => void }; + } - useEffect(() => { - if (!transcriptId || !topicId || !api) return; - - setLoading(true); - - api - .v1TranscriptGetTopicsWithWordsPerSpeaker({ transcriptId, topicId }) - .then((result) => { - setResponse(result); - setLoading(false); - console.debug("Topics with words Loaded:", result); - }) - .catch((error) => { - const shouldShowHuman = shouldShowError(error); - if (shouldShowHuman) { - setError(error, "There was an error loading the topics with words"); - } else { - setError(error); - } - setErrorState(error); - }); - }, [transcriptId, !api, topicId, count]); - - return { response, loading, error, refetch } as UseTopicWithWords; + return { + response, + loading: false, + error: null, + refetch, + } as SuccessTopicWithWords & { refetch: () => void }; }; export default useTopicWithWords; diff --git a/www/app/(app)/transcripts/useTopics.ts b/www/app/(app)/transcripts/useTopics.ts index 73d78c64..b2923cba 100644 --- a/www/app/(app)/transcripts/useTopics.ts +++ b/www/app/(app)/transcripts/useTopics.ts @@ -1,9 +1,4 @@ -import { useEffect, useState } from "react"; - -import { useError } from "../../(errors)/errorContext"; -import { Topic } from "./webSocketTypes"; -import useApi from "../../lib/useApi"; -import { shouldShowError } from "../../lib/errorUtils"; +import { useTranscriptTopics } from "../../lib/api-hooks"; import { GetTranscriptTopic } from "../../lib/api-types"; type TranscriptTopics = { @@ -13,34 +8,13 @@ type TranscriptTopics = { }; const useTopics = (id: string): TranscriptTopics => { - const [topics, setTopics] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); - useEffect(() => { - if (!id || !api) return; + const { data: topics, isLoading: loading, error } = useTranscriptTopics(id); - setLoading(true); - api - .v1TranscriptGetTopics({ transcriptId: id }) - .then((result) => { - setTopics(result); - setLoading(false); - console.debug("Transcript topics loaded:", result); - }) - .catch((err) => { - setErrorState(err); - const shouldShowHuman = shouldShowError(err); - if (shouldShowHuman) { - setError(err, "There was an error loading the topics"); - } else { - setError(err); - } - }); - }, [id, !api]); - - return { topics, loading, error }; + return { + topics: topics || null, + loading, + error: error as Error | null, + }; }; export default useTopics; diff --git a/www/app/(app)/transcripts/useWaveform.ts b/www/app/(app)/transcripts/useWaveform.ts index fd266657..aca86816 100644 --- a/www/app/(app)/transcripts/useWaveform.ts +++ b/www/app/(app)/transcripts/useWaveform.ts @@ -1,8 +1,5 @@ -import { useEffect, useState } from "react"; import { AudioWaveform } from "../../lib/api-types"; -import { useError } from "../../(errors)/errorContext"; -import useApi from "../../lib/useApi"; -import { shouldShowError } from "../../lib/errorUtils"; +import { useTranscriptWaveform } from "../../lib/api-hooks"; type AudioWaveFormResponse = { waveform: AudioWaveform | null; @@ -11,35 +8,17 @@ type AudioWaveFormResponse = { }; const useWaveform = (id: string, skip: boolean): AudioWaveFormResponse => { - const [waveform, setWaveform] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); + const { + data: waveform, + isLoading: loading, + error, + } = useTranscriptWaveform(skip ? null : id); - useEffect(() => { - if (!id || !api || skip) { - setLoading(false); - setErrorState(null); - setWaveform(null); - return; - } - setLoading(true); - setErrorState(null); - api - .v1TranscriptGetAudioWaveform({ transcriptId: id }) - .then((result) => { - setWaveform(result); - setLoading(false); - console.debug("Transcript waveform loaded:", result); - }) - .catch((err) => { - setErrorState(err); - setLoading(false); - }); - }, [id, api, skip]); - - return { waveform, loading, error }; + return { + waveform: waveform || null, + loading, + error: error as Error | null, + }; }; export default useWaveform; diff --git a/www/app/(app)/transcripts/useWebRTC.ts b/www/app/(app)/transcripts/useWebRTC.ts index 07ada45c..2ae8cbce 100644 --- a/www/app/(app)/transcripts/useWebRTC.ts +++ b/www/app/(app)/transcripts/useWebRTC.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import Peer from "simple-peer"; import { useError } from "../../(errors)/errorContext"; -import useApi from "../../lib/useApi"; +import { useTranscriptWebRTC } from "../../lib/api-hooks"; import { RtcOffer } from "../../lib/api-types"; const useWebRTC = ( @@ -10,10 +10,10 @@ const useWebRTC = ( ): Peer => { const [peer, setPeer] = useState(null); const { setError } = useError(); - const api = useApi(); + const webRTCMutation = useTranscriptWebRTC(); useEffect(() => { - if (!stream || !transcriptId || !api) { + if (!stream || !transcriptId) { return; } @@ -24,7 +24,7 @@ const useWebRTC = ( try { p = new Peer({ initiator: true, stream: stream }); } catch (error) { - setError(error, "Error creating WebRTC"); + setError(error as Error, "Error creating WebRTC"); return; } @@ -32,26 +32,31 @@ const useWebRTC = ( setError(new Error(`WebRTC error: ${err}`)); }); - p.on("signal", (data: any) => { - if (!api) return; + p.on("signal", async (data: any) => { if ("sdp" in data) { const rtcOffer: RtcOffer = { sdp: data.sdp, type: data.type, }; - api - .v1TranscriptRecordWebrtc({ transcriptId, requestBody: rtcOffer }) - .then((answer) => { - try { - p.signal(answer); - } catch (error) { - setError(error); - } - }) - .catch((error) => { - setError(error, "Error loading WebRTCOffer"); + try { + const answer = await webRTCMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: rtcOffer, }); + + try { + p.signal(answer); + } catch (error) { + setError(error as Error); + } + } catch (error) { + setError(error as Error, "Error loading WebRTCOffer"); + } } }); @@ -63,7 +68,7 @@ const useWebRTC = ( return () => { p.destroy(); }; - }, [stream, transcriptId, !api]); + }, [stream, transcriptId, webRTCMutation]); return peer; }; diff --git a/www/app/(app)/transcripts/useWebSockets.ts b/www/app/(app)/transcripts/useWebSockets.ts index 863e5f5a..69e0836d 100644 --- a/www/app/(app)/transcripts/useWebSockets.ts +++ b/www/app/(app)/transcripts/useWebSockets.ts @@ -3,7 +3,8 @@ import { Topic, FinalSummary, Status } from "./webSocketTypes"; import { useError } from "../../(errors)/errorContext"; import { DomainContext } from "../../domainContext"; import { AudioWaveform, GetTranscriptSegmentTopic } from "../../lib/api-types"; -import useApi from "../../lib/useApi"; +import { useQueryClient } from "@tanstack/react-query"; +import { $api } from "../../lib/apiClient"; export type UseWebSockets = { transcriptTextLive: string; @@ -34,7 +35,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { const { setError } = useError(); const { websocket_url } = useContext(DomainContext); - const api = useApi(); + const queryClient = useQueryClient(); const [accumulatedText, setAccumulatedText] = useState(""); @@ -105,6 +106,13 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { title: "Topic 1: Introduction to Quantum Mechanics", transcript: "A brief overview of quantum mechanics and its principles.", + segments: [ + { + speaker: 1, + start: 0, + text: "This is the transcription of an example title", + }, + ], }, { id: "2", @@ -315,9 +323,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { } }; - if (!transcriptId || !api) return; - - api?.v1TranscriptGetWebsocketEvents({ transcriptId }).then((result) => {}); + if (!transcriptId) return; const url = `${websocket_url}/v1/transcripts/${transcriptId}/events`; let ws = new WebSocket(url); @@ -361,6 +367,16 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { return [...prevTopics, topic]; }); console.debug("TOPIC event:", message.data); + // Invalidate topics query to sync with WebSocket data + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/topics", + { + params: { path: { transcript_id: transcriptId } }, + }, + ).queryKey, + }); break; case "FINAL_SHORT_SUMMARY": @@ -370,6 +386,16 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { case "FINAL_LONG_SUMMARY": if (message.data) { setFinalSummary(message.data); + // Invalidate transcript query to sync summary + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}", + { + params: { path: { transcript_id: transcriptId } }, + }, + ).queryKey, + }); } break; @@ -377,6 +403,16 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { console.debug("FINAL_TITLE event:", message.data); if (message.data) { setTitle(message.data.title); + // Invalidate transcript query to sync title + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}", + { + params: { path: { transcript_id: transcriptId } }, + }, + ).queryKey, + }); } break; @@ -450,7 +486,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { return () => { ws.close(); }; - }, [transcriptId, !api]); + }, [transcriptId, websocket_url, queryClient]); return { transcriptTextLive, diff --git a/www/app/[roomName]/page.tsx b/www/app/[roomName]/page.tsx index 3ca424b9..ec65f43c 100644 --- a/www/app/[roomName]/page.tsx +++ b/www/app/[roomName]/page.tsx @@ -23,7 +23,7 @@ import { useRouter } from "next/navigation"; import { notFound } from "next/navigation"; import useSessionStatus from "../lib/useSessionStatus"; import { useRecordingConsent } from "../recordingConsentContext"; -import useApi from "../lib/useApi"; +import { useMeetingAudioConsent } from "../lib/api-hooks"; import { Meeting } from "../lib/api-types"; import { FaBars } from "react-icons/fa6"; @@ -76,31 +76,30 @@ const useConsentDialog = ( wherebyRef: RefObject /*accessibility*/, ) => { const { state: consentState, touch, hasConsent } = useRecordingConsent(); - const [consentLoading, setConsentLoading] = useState(false); // toast would open duplicates, even with using "id=" prop const [modalOpen, setModalOpen] = useState(false); - const api = useApi(); + const audioConsentMutation = useMeetingAudioConsent(); const handleConsent = useCallback( async (meetingId: string, given: boolean) => { - if (!api) return; - - setConsentLoading(true); - try { - await api.v1MeetingAudioConsent({ - meetingId, - requestBody: { consent_given: given }, + await audioConsentMutation.mutateAsync({ + params: { + path: { + meeting_id: meetingId, + }, + }, + body: { + consent_given: given, + }, }); touch(meetingId); } catch (error) { console.error("Error submitting consent:", error); - } finally { - setConsentLoading(false); } }, - [api, touch], + [audioConsentMutation, touch], ); const showConsentModal = useCallback(() => { @@ -194,7 +193,12 @@ const useConsentDialog = ( return cleanup; }, [meetingId, handleConsent, wherebyRef, modalOpen]); - return { showConsentModal, consentState, hasConsent, consentLoading }; + return { + showConsentModal, + consentState, + hasConsent, + consentLoading: audioConsentMutation.isPending, + }; }; function ConsentDialogButton({ diff --git a/www/app/[roomName]/useRoomMeeting.tsx b/www/app/[roomName]/useRoomMeeting.tsx index 569271d8..d8a308c1 100644 --- a/www/app/[roomName]/useRoomMeeting.tsx +++ b/www/app/[roomName]/useRoomMeeting.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useError } from "../(errors)/errorContext"; import { Meeting } from "../lib/api-types"; import { shouldShowError } from "../lib/errorUtils"; -import useApi from "../lib/useApi"; +import { useRoomsCreateMeeting } from "../lib/api-hooks"; import { notFound } from "next/navigation"; type ErrorMeeting = { @@ -30,27 +30,25 @@ const useRoomMeeting = ( roomName: string | null | undefined, ): ErrorMeeting | LoadingMeeting | SuccessMeeting => { const [response, setResponse] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setErrorState] = useState(null); const [reload, setReload] = useState(0); const { setError } = useError(); - const api = useApi(); + const createMeetingMutation = useRoomsCreateMeeting(); const reloadHandler = () => setReload((prev) => prev + 1); useEffect(() => { - if (!roomName || !api) return; + if (!roomName) return; - if (!response) { - setLoading(true); - } - - api - .v1RoomsCreateMeeting({ roomName }) - .then((result) => { + const createMeeting = async () => { + try { + const result = await createMeetingMutation.mutateAsync({ + params: { + path: { + room_name: roomName, + }, + }, + }); setResponse(result); - setLoading(false); - }) - .catch((error) => { + } catch (error: any) { const shouldShowHuman = shouldShowError(error); if (shouldShowHuman && error.status !== 404) { setError( @@ -60,9 +58,14 @@ const useRoomMeeting = ( } else { setError(error); } - setErrorState(error); - }); - }, [roomName, !api, reload]); + } + }; + + createMeeting(); + }, [roomName, reload]); + + const loading = createMeetingMutation.isPending && !response; + const error = createMeetingMutation.error as Error | null; return { response, loading, error, reload: reloadHandler } as | ErrorMeeting diff --git a/www/app/lib/api-hooks.ts b/www/app/lib/api-hooks.ts index 55a5be2f..02ae66da 100644 --- a/www/app/lib/api-hooks.ts +++ b/www/app/lib/api-hooks.ts @@ -9,20 +9,11 @@ import type { paths } from "../reflector-api"; export function useRoomsList(page: number = 1) { const { setError } = useError(); - return $api.useQuery( - "get", - "/v1/rooms", - { - params: { - query: { page }, - }, + return $api.useQuery("get", "/v1/rooms", { + params: { + query: { page }, }, - { - onError: (error) => { - setError(error as Error, "There was an error fetching the rooms"); - }, - }, - ); + }); } // Transcripts hooks @@ -37,27 +28,17 @@ export function useTranscriptsSearch( ) { const { setError } = useError(); - return $api.useQuery( - "get", - "/v1/transcripts/search", - { - params: { - query: { - q, - limit: options.limit, - offset: options.offset, - room_id: options.room_id, - source_kind: options.source_kind as any, - }, + return $api.useQuery("get", "/v1/transcripts/search", { + params: { + query: { + q, + limit: options.limit, + offset: options.offset, + room_id: options.room_id, + source_kind: options.source_kind as any, }, }, - { - onError: (error) => { - setError(error as Error, "There was an error searching transcripts"); - }, - keepPreviousData: true, // For smooth pagination - }, - ); + }); } export function useTranscriptDelete() { @@ -68,7 +49,9 @@ export function useTranscriptDelete() { onSuccess: () => { // Invalidate transcripts queries to refetch queryClient.invalidateQueries({ - queryKey: $api.queryOptions("get", "/v1/transcripts/search").queryKey, + queryKey: $api.queryOptions("get", "/v1/transcripts/search", { + params: { query: { q: "" } }, + }).queryKey, }); }, onError: (error) => { @@ -102,9 +85,6 @@ export function useTranscriptGet(transcriptId: string | null) { }, { enabled: !!transcriptId, - onError: (error) => { - setError(error as Error, "There was an error loading the transcript"); - }, }, ); } @@ -158,28 +138,21 @@ export function useRoomDelete() { }); } -// Zulip hooks +// Zulip hooks - NOTE: These endpoints are not in the OpenAPI spec yet export function useZulipStreams() { const { setError } = useError(); - return $api.useQuery( - "get", - "/v1/zulip/get-streams", - {}, - { - onError: (error) => { - setError(error as Error, "There was an error fetching Zulip streams"); - }, - }, - ); + // @ts-ignore - Zulip endpoint not in OpenAPI spec + return $api.useQuery("get", "/v1/zulip/get-streams" as any, {}); } export function useZulipTopics(streamId: number | null) { const { setError } = useError(); + // @ts-ignore - Zulip endpoint not in OpenAPI spec return $api.useQuery( "get", - "/v1/zulip/get-topics", + "/v1/zulip/get-topics" as any, { params: { query: { stream_id: streamId || 0 }, @@ -187,9 +160,6 @@ export function useZulipTopics(streamId: number | null) { }, { enabled: !!streamId, - onError: (error) => { - setError(error as Error, "There was an error fetching Zulip topics"); - }, }, ); } @@ -219,11 +189,16 @@ export function useTranscriptUpdate() { export function useTranscriptPostToZulip() { const { setError } = useError(); - return $api.useMutation("post", "/v1/transcripts/{transcript_id}/zulip", { - onError: (error) => { - setError(error as Error, "There was an error posting to Zulip"); + // @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"); + }, }, - }); + ); } export function useTranscriptUploadAudio() { @@ -269,9 +244,6 @@ export function useTranscriptWaveform(transcriptId: string | null) { }, { enabled: !!transcriptId, - onError: (error) => { - setError(error as Error, "There was an error fetching the waveform"); - }, }, ); } @@ -289,9 +261,6 @@ export function useTranscriptMP3(transcriptId: string | null) { }, { enabled: !!transcriptId, - onError: (error) => { - setError(error as Error, "There was an error fetching the MP3"); - }, }, ); } @@ -309,9 +278,6 @@ export function useTranscriptTopics(transcriptId: string | null) { }, { enabled: !!transcriptId, - onError: (error) => { - setError(error as Error, "There was an error fetching topics"); - }, }, ); } @@ -329,13 +295,30 @@ export function useTranscriptTopicsWithWords(transcriptId: string | null) { }, { enabled: !!transcriptId, - onError: (error) => { - setError( - error as Error, - "There was an error fetching topics with words", - ); + }, + ); +} + +export function useTranscriptTopicsWithWordsPerSpeaker( + transcriptId: string | null, + topicId: string | null, +) { + const { setError } = useError(); + + return $api.useQuery( + "get", + "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker", + { + params: { + path: { + transcript_id: transcriptId || "", + topic_id: topicId || "", + }, }, }, + { + enabled: !!transcriptId && !!topicId, + }, ); } @@ -353,9 +336,6 @@ export function useTranscriptParticipants(transcriptId: string | null) { }, { enabled: !!transcriptId, - onError: (error) => { - setError(error as Error, "There was an error fetching participants"); - }, }, ); } @@ -389,12 +369,70 @@ export function useTranscriptParticipantUpdate() { ); } -export function useTranscriptSpeakerAssign() { +export function useTranscriptParticipantCreate() { const { setError } = useError(); const queryClient = useQueryClient(); return $api.useMutation( "post", + "/v1/transcripts/{transcript_id}/participants", + { + onSuccess: (data, variables) => { + // Invalidate participants list + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/participants", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error creating the participant"); + }, + }, + ); +} + +export function useTranscriptParticipantDelete() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation( + "delete", + "/v1/transcripts/{transcript_id}/participants/{participant_id}", + { + onSuccess: (data, variables) => { + // Invalidate participants list + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/participants", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error deleting the participant"); + }, + }, + ); +} + +export function useTranscriptSpeakerAssign() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation( + "patch", "/v1/transcripts/{transcript_id}/speaker/assign", { onSuccess: (data, variables) => { @@ -434,7 +472,7 @@ export function useTranscriptSpeakerMerge() { const queryClient = useQueryClient(); return $api.useMutation( - "post", + "patch", "/v1/transcripts/{transcript_id}/speaker/merge", { onSuccess: (data, variables) => { @@ -504,7 +542,9 @@ export function useTranscriptCreate() { onSuccess: () => { // Invalidate transcripts list queryClient.invalidateQueries({ - queryKey: $api.queryOptions("get", "/v1/transcripts/search").queryKey, + queryKey: $api.queryOptions("get", "/v1/transcripts/search", { + params: { query: { q: "" } }, + }).queryKey, }); }, onError: (error) => { @@ -512,3 +552,21 @@ export function useTranscriptCreate() { }, }); } + +// Rooms meeting operations +export function useRoomsCreateMeeting() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation("post", "/v1/rooms/{room_name}/meeting", { + onSuccess: () => { + // Invalidate rooms list to refresh meeting data + queryClient.invalidateQueries({ + queryKey: $api.queryOptions("get", "/v1/rooms").queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error creating the meeting"); + }, + }); +} diff --git a/www/app/lib/api-types.ts b/www/app/lib/api-types.ts index 865386c0..66aed1c7 100644 --- a/www/app/lib/api-types.ts +++ b/www/app/lib/api-types.ts @@ -16,7 +16,6 @@ export type RtcOffer = components["schemas"]["RtcOffer"]; export type GetTranscriptSegmentTopic = components["schemas"]["GetTranscriptSegmentTopic"]; export type Page_Room_ = components["schemas"]["Page_Room_"]; -export type ApiError = components["schemas"]["ApiError"]; export type GetTranscriptTopicWithWordsPerSpeaker = components["schemas"]["GetTranscriptTopicWithWordsPerSpeaker"]; export type GetTranscriptMinimal = @@ -24,5 +23,5 @@ export type GetTranscriptMinimal = // Export any enums or constants that were in the old API export const $SourceKind = { - values: ["SINGLE", "CALL", "WHEREBY", "UPLOAD"] as const, + values: ["room", "live", "file"] as const, } as const; diff --git a/www/app/lib/useApi.ts b/www/app/lib/useApi.ts deleted file mode 100644 index 97280db7..00000000 --- a/www/app/lib/useApi.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Compatibility layer for direct API client usage -// Prefer using React Query hooks from api-hooks.ts instead - -import { client } from "./apiClient"; - -// Returns the configured client for direct API calls -// This is a minimal compatibility layer for components that haven't been fully migrated -export default function useApi() { - return client; -} - -// Export the client directly for non-hook contexts -export { client };