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 };