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
This commit is contained in:
2025-08-28 10:08:31 -06:00
parent 55f83cf5f4
commit fbeeff4c4d
25 changed files with 669 additions and 630 deletions

View File

@@ -10,6 +10,13 @@ interface FilterSidebarProps {
onFilterChange: (sourceKind: SourceKind | null, roomId: string) => void; 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({ export default function FilterSidebar({
rooms, rooms,
selectedSourceKind, selectedSourceKind,
@@ -44,14 +51,14 @@ export default function FilterSidebar({
key={room.id} key={room.id}
as={NextLink} as={NextLink}
href="#" href="#"
onClick={() => onFilterChange("room", room.id)} onClick={() => onFilterChange(SK.room, room.id)}
color={ color={
selectedSourceKind === "room" && selectedRoomId === room.id selectedSourceKind === SK.room && selectedRoomId === room.id
? "blue.500" ? "blue.500"
: "gray.600" : "gray.600"
} }
fontWeight={ fontWeight={
selectedSourceKind === "room" && selectedRoomId === room.id selectedSourceKind === SK.room && selectedRoomId === room.id
? "bold" ? "bold"
: "normal" : "normal"
} }
@@ -72,14 +79,14 @@ export default function FilterSidebar({
key={room.id} key={room.id}
as={NextLink} as={NextLink}
href="#" href="#"
onClick={() => onFilterChange("room", room.id)} onClick={() => onFilterChange(SK.room, room.id)}
color={ color={
selectedSourceKind === "room" && selectedRoomId === room.id selectedSourceKind === SK.room && selectedRoomId === room.id
? "blue.500" ? "blue.500"
: "gray.600" : "gray.600"
} }
fontWeight={ fontWeight={
selectedSourceKind === "room" && selectedRoomId === room.id selectedSourceKind === SK.room && selectedRoomId === room.id
? "bold" ? "bold"
: "normal" : "normal"
} }
@@ -95,10 +102,10 @@ export default function FilterSidebar({
<Link <Link
as={NextLink} as={NextLink}
href="#" href="#"
onClick={() => onFilterChange("live", "")} onClick={() => onFilterChange(SK.live, "")}
color={selectedSourceKind === "live" ? "blue.500" : "gray.600"} color={selectedSourceKind === SK.live ? "blue.500" : "gray.600"}
_hover={{ color: "blue.300" }} _hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === "live" ? "bold" : "normal"} fontWeight={selectedSourceKind === SK.live ? "bold" : "normal"}
fontSize="sm" fontSize="sm"
> >
Live Transcripts Live Transcripts
@@ -106,10 +113,10 @@ export default function FilterSidebar({
<Link <Link
as={NextLink} as={NextLink}
href="#" href="#"
onClick={() => onFilterChange("file", "")} onClick={() => onFilterChange(SK.file, "")}
color={selectedSourceKind === "file" ? "blue.500" : "gray.600"} color={selectedSourceKind === SK.file ? "blue.500" : "gray.600"}
_hover={{ color: "blue.300" }} _hover={{ color: "blue.300" }}
fontWeight={selectedSourceKind === "file" ? "bold" : "normal"} fontWeight={selectedSourceKind === SK.file ? "bold" : "normal"}
fontSize="sm" fontSize="sm"
> >
Uploaded Files Uploaded Files

View File

@@ -18,7 +18,7 @@ import {
highlightMatches, highlightMatches,
generateTextFragment, generateTextFragment,
} from "../../../lib/textHighlight"; } from "../../../lib/textHighlight";
import { SearchResult } from "../../../lib/api-types"; import { SearchResult, SourceKind } from "../../../lib/api-types";
interface TranscriptCardsProps { interface TranscriptCardsProps {
results: SearchResult[]; results: SearchResult[];
@@ -120,7 +120,7 @@ function TranscriptCard({
: "N/A"; : "N/A";
const formattedDate = formatLocalDate(result.created_at); const formattedDate = formatLocalDate(result.created_at);
const source = const source =
result.source_kind === "room" result.source_kind === ("room" as SourceKind)
? result.room_name || result.room_id ? result.room_name || result.room_id
: result.source_kind; : result.source_kind;

View File

@@ -204,7 +204,7 @@ export default function TranscriptBrowser() {
const [urlSourceKind, setUrlSourceKind] = useQueryState( const [urlSourceKind, setUrlSourceKind] = useQueryState(
"source", "source",
parseAsStringLiteral($SourceKind.enum).withOptions({ parseAsStringLiteral($SourceKind.values).withOptions({
shallow: false, shallow: false,
}), }),
); );
@@ -302,7 +302,6 @@ export default function TranscriptBrowser() {
params: { params: {
path: { transcript_id: transcriptId }, path: { transcript_id: transcriptId },
}, },
body: {},
}, },
{ {
onSuccess: (result) => { onSuccess: (result) => {

View File

@@ -16,7 +16,7 @@ import {
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useRoomList from "./useRoomList"; import useRoomList from "./useRoomList";
import { ApiError, Room } from "../../lib/api-types"; import { Room } from "../../lib/api-types";
import { import {
useRoomCreate, useRoomCreate,
useRoomUpdate, useRoomUpdate,
@@ -92,8 +92,10 @@ export default function RoomsList() {
const createRoomMutation = useRoomCreate(); const createRoomMutation = useRoomCreate();
const updateRoomMutation = useRoomUpdate(); const updateRoomMutation = useRoomUpdate();
const deleteRoomMutation = useRoomDelete(); const deleteRoomMutation = useRoomDelete();
const { data: streams = [] } = useZulipStreams(); const { data: streams = [] } = useZulipStreams() as { data: any[] };
const { data: topics = [] } = useZulipTopics(selectedStreamId); const { data: topics = [] } = useZulipTopics(selectedStreamId) as {
data: Topic[];
};
interface Topic { interface Topic {
name: string; name: string;
} }
@@ -177,11 +179,10 @@ export default function RoomsList() {
setNameError(""); setNameError("");
refetch(); refetch();
onClose(); onClose();
} catch (err) { } catch (err: any) {
if ( if (
err instanceof ApiError && err?.status === 400 &&
err.status === 400 && err?.body?.detail == "Room name is not unique"
(err.body as any).detail == "Room name is not unique"
) { ) {
setNameError( setNameError(
"This room name is already taken. Please choose a different name.", "This room name is already taken. Please choose a different name.",

View File

@@ -6,9 +6,9 @@ import TopicPlayer from "./topicPlayer";
import useParticipants from "../../useParticipants"; import useParticipants from "../../useParticipants";
import useTopicWithWords from "../../useTopicWithWords"; import useTopicWithWords from "../../useTopicWithWords";
import ParticipantList from "./participantList"; import ParticipantList from "./participantList";
import { GetTranscriptTopic } from "../../../../api"; import { GetTranscriptTopic } from "../../../../lib/api-types";
import { SelectedText, selectedTextIsTimeSlice } from "./types"; import { SelectedText, selectedTextIsTimeSlice } from "./types";
import useApi from "../../../../lib/useApi"; import { useTranscriptUpdate } from "../../../../lib/api-hooks";
import useTranscript from "../../useTranscript"; import useTranscript from "../../useTranscript";
import { useError } from "../../../../(errors)/errorContext"; import { useError } from "../../../../(errors)/errorContext";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -23,7 +23,7 @@ export type TranscriptCorrect = {
export default function TranscriptCorrect({ export default function TranscriptCorrect({
params: { transcriptId }, params: { transcriptId },
}: TranscriptCorrect) { }: TranscriptCorrect) {
const api = useApi(); const updateTranscriptMutation = useTranscriptUpdate();
const transcript = useTranscript(transcriptId); const transcript = useTranscript(transcriptId);
const stateCurrentTopic = useState<GetTranscriptTopic>(); const stateCurrentTopic = useState<GetTranscriptTopic>();
const [currentTopic, _sct] = stateCurrentTopic; const [currentTopic, _sct] = stateCurrentTopic;
@@ -34,16 +34,21 @@ export default function TranscriptCorrect({
const { setError } = useError(); const { setError } = useError();
const router = useRouter(); const router = useRouter();
const markAsDone = () => { const markAsDone = async () => {
if (transcript.response && !transcript.response.reviewed) { if (transcript.response && !transcript.response.reviewed) {
api try {
?.v1TranscriptUpdate({ transcriptId, requestBody: { reviewed: true } }) await updateTranscriptMutation.mutateAsync({
.then(() => { params: {
router.push(`/transcripts/${transcriptId}`); path: {
}) transcript_id: transcriptId,
.catch((e) => { },
setError(e, "Error marking as done"); },
body: { reviewed: true },
}); });
router.push(`/transcripts/${transcriptId}`);
} catch (e) {
setError(e as Error, "Error marking as done");
}
} }
}; };

View File

@@ -1,8 +1,14 @@
import { faArrowTurnDown } from "@fortawesome/free-solid-svg-icons"; import { faArrowTurnDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ChangeEvent, useEffect, useRef, useState } from "react"; import { ChangeEvent, useEffect, useRef, useState } from "react";
import { Participant } from "../../../../api"; import { Participant } from "../../../../lib/api-types";
import useApi from "../../../../lib/useApi"; import {
useTranscriptSpeakerAssign,
useTranscriptSpeakerMerge,
useTranscriptParticipantUpdate,
useTranscriptParticipantCreate,
useTranscriptParticipantDelete,
} from "../../../../lib/api-hooks";
import { UseParticipants } from "../../useParticipants"; import { UseParticipants } from "../../useParticipants";
import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./types"; import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./types";
import { useError } from "../../../../(errors)/errorContext"; import { useError } from "../../../../(errors)/errorContext";
@@ -30,9 +36,19 @@ const ParticipantList = ({
topicWithWords, topicWithWords,
stateSelectedText, stateSelectedText,
}: ParticipantList) => { }: ParticipantList) => {
const api = useApi();
const { setError } = useError(); 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 [participantInput, setParticipantInput] = useState("");
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [selectedText, setSelectedText] = stateSelectedText; const [selectedText, setSelectedText] = stateSelectedText;
@@ -103,7 +119,6 @@ const ParticipantList = ({
const onSuccess = () => { const onSuccess = () => {
topicWithWords.refetch(); topicWithWords.refetch();
participants.refetch(); participants.refetch();
setLoading(false);
setAction(null); setAction(null);
setSelectedText(undefined); setSelectedText(undefined);
setSelectedParticipant(undefined); setSelectedParticipant(undefined);
@@ -120,11 +135,14 @@ const ParticipantList = ({
if (loading || participants.loading || topicWithWords.loading) return; if (loading || participants.loading || topicWithWords.loading) return;
if (!selectedTextIsTimeSlice(selectedText)) return; if (!selectedTextIsTimeSlice(selectedText)) return;
setLoading(true);
try { try {
await api?.v1TranscriptAssignSpeaker({ await speakerAssignMutation.mutateAsync({
transcriptId, params: {
requestBody: { path: {
transcript_id: transcriptId,
},
},
body: {
participant: participant.id, participant: participant.id,
timestamp_from: selectedText.start, timestamp_from: selectedText.start,
timestamp_to: selectedText.end, timestamp_to: selectedText.end,
@@ -132,8 +150,7 @@ const ParticipantList = ({
}); });
onSuccess(); onSuccess();
} catch (error) { } catch (error) {
setError(error, "There was an error assigning"); setError(error as Error, "There was an error assigning");
setLoading(false);
throw error; throw error;
} }
}; };
@@ -141,32 +158,38 @@ const ParticipantList = ({
const mergeSpeaker = const mergeSpeaker =
(speakerFrom, participantTo: Participant) => async () => { (speakerFrom, participantTo: Participant) => async () => {
if (loading || participants.loading || topicWithWords.loading) return; if (loading || participants.loading || topicWithWords.loading) return;
setLoading(true);
if (participantTo.speaker) { if (participantTo.speaker) {
try { try {
await api?.v1TranscriptMergeSpeaker({ await speakerMergeMutation.mutateAsync({
transcriptId, params: {
requestBody: { path: {
transcript_id: transcriptId,
},
},
body: {
speaker_from: speakerFrom, speaker_from: speakerFrom,
speaker_to: participantTo.speaker, speaker_to: participantTo.speaker,
}, },
}); });
onSuccess(); onSuccess();
} catch (error) { } catch (error) {
setError(error, "There was an error merging"); setError(error as Error, "There was an error merging");
setLoading(false);
} }
} else { } else {
try { try {
await api?.v1TranscriptUpdateParticipant({ await participantUpdateMutation.mutateAsync({
transcriptId, params: {
participantId: participantTo.id, path: {
requestBody: { speaker: speakerFrom }, transcript_id: transcriptId,
participant_id: participantTo.id,
},
},
body: { speaker: speakerFrom },
}); });
onSuccess(); onSuccess();
} catch (error) { } catch (error) {
setError(error, "There was an error merging (update)"); setError(error as Error, "There was an error merging (update)");
setLoading(false);
} }
} }
}; };
@@ -186,105 +209,106 @@ const ParticipantList = ({
(p) => p.speaker == selectedText, (p) => p.speaker == selectedText,
); );
if (participant && participant.name !== participantInput) { if (participant && participant.name !== participantInput) {
setLoading(true); try {
api await participantUpdateMutation.mutateAsync({
?.v1TranscriptUpdateParticipant({ params: {
transcriptId, path: {
participantId: participant.id, transcript_id: transcriptId,
requestBody: { participant_id: participant.id,
},
},
body: {
name: participantInput, 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 ( } else if (
action == "Create to rename" && action == "Create to rename" &&
selectedTextIsSpeaker(selectedText) selectedTextIsSpeaker(selectedText)
) { ) {
setLoading(true); try {
api await participantCreateMutation.mutateAsync({
?.v1TranscriptAddParticipant({ params: {
transcriptId, path: {
requestBody: { transcript_id: transcriptId,
},
},
body: {
name: participantInput, name: participantInput,
speaker: selectedText, speaker: selectedText,
}, },
}) });
.then(() => {
participants.refetch(); participants.refetch();
setParticipantInput(""); setParticipantInput("");
setOneMatch(undefined); setOneMatch(undefined);
setLoading(false); } catch (e) {
}) setError(e as Error, "There was an error creating");
.catch((e) => { }
setError(e, "There was an error creating");
setLoading(false);
});
} else if ( } else if (
action == "Create and assign" && action == "Create and assign" &&
selectedTextIsTimeSlice(selectedText) selectedTextIsTimeSlice(selectedText)
) { ) {
setLoading(true);
try { try {
const participant = await api?.v1TranscriptAddParticipant({ const participant = await participantCreateMutation.mutateAsync({
transcriptId, params: {
requestBody: { path: {
transcript_id: transcriptId,
},
},
body: {
name: participantInput, name: participantInput,
}, },
}); });
setLoading(false);
assignTo(participant)().catch(() => { assignTo(participant)().catch(() => {
// error and loading are handled by assignTo catch // error and loading are handled by assignTo catch
participants.refetch(); participants.refetch();
}); });
} catch (error) { } catch (error) {
setError(e, "There was an error creating"); setError(error as Error, "There was an error creating");
setLoading(false);
} }
} else if (action == "Create") { } else if (action == "Create") {
setLoading(true); try {
api await participantCreateMutation.mutateAsync({
?.v1TranscriptAddParticipant({ params: {
transcriptId, path: {
requestBody: { transcript_id: transcriptId,
},
},
body: {
name: participantInput, name: participantInput,
}, },
}) });
.then(() => {
participants.refetch(); participants.refetch();
setParticipantInput(""); setParticipantInput("");
setLoading(false);
inputRef.current?.focus(); inputRef.current?.focus();
}) } catch (e) {
.catch((e) => { setError(e as Error, "There was an error creating");
setError(e, "There was an error creating"); }
setLoading(false);
});
} }
}; };
const deleteParticipant = (participantId) => (e) => { const deleteParticipant = (participantId) => async (e) => {
e.stopPropagation(); e.stopPropagation();
if (loading || participants.loading || topicWithWords.loading) return; if (loading || participants.loading || topicWithWords.loading) return;
setLoading(true);
api try {
?.v1TranscriptDeleteParticipant({ transcriptId, participantId }) await participantDeleteMutation.mutateAsync({
.then(() => { params: {
participants.refetch(); path: {
setLoading(false); transcript_id: transcriptId,
}) participant_id: participantId,
.catch((e) => { },
setError(e, "There was an error deleting"); },
setLoading(false);
}); });
participants.refetch();
} catch (e) {
setError(e as Error, "There was an error deleting");
}
}; };
const selectParticipant = (participant) => (e) => { const selectParticipant = (participant) => (e) => {

View File

@@ -1,6 +1,6 @@
import useTopics from "../../useTopics"; import useTopics from "../../useTopics";
import { Dispatch, SetStateAction, useEffect } from "react"; import { Dispatch, SetStateAction, useEffect } from "react";
import { GetTranscriptTopic } from "../../../../api"; import { GetTranscriptTopic } from "../../../../lib/api-types";
import { import {
BoxProps, BoxProps,
Box, Box,

View File

@@ -2,12 +2,8 @@ import { useEffect, useRef, useState } from "react";
import React from "react"; import React from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import "../../../styles/markdown.css"; import "../../../styles/markdown.css";
import { import { GetTranscript, GetTranscriptTopic } from "../../../lib/api-types";
GetTranscript, import { useTranscriptUpdate } from "../../../lib/api-hooks";
GetTranscriptTopic,
UpdateTranscript,
} from "../../../lib/api-types";
import useApi from "../../../lib/useApi";
import { import {
Flex, Flex,
Heading, Heading,
@@ -33,9 +29,8 @@ export default function FinalSummary(props: FinalSummaryProps) {
const [preEditSummary, setPreEditSummary] = useState(""); const [preEditSummary, setPreEditSummary] = useState("");
const [editedSummary, setEditedSummary] = useState(""); const [editedSummary, setEditedSummary] = useState("");
const api = useApi();
const { setError } = useError(); const { setError } = useError();
const updateTranscriptMutation = useTranscriptUpdate();
useEffect(() => { useEffect(() => {
setEditedSummary(props.transcriptResponse?.long_summary || ""); setEditedSummary(props.transcriptResponse?.long_summary || "");
@@ -47,12 +42,15 @@ export default function FinalSummary(props: FinalSummaryProps) {
const updateSummary = async (newSummary: string, transcriptId: string) => { const updateSummary = async (newSummary: string, transcriptId: string) => {
try { try {
const requestBody: UpdateTranscript = { const updatedTranscript = await updateTranscriptMutation.mutateAsync({
params: {
path: {
transcript_id: transcriptId,
},
},
body: {
long_summary: newSummary, long_summary: newSummary,
}; },
const updatedTranscript = await api?.v1TranscriptUpdate({
transcriptId,
requestBody,
}); });
if (props.onUpdate) { if (props.onUpdate) {
props.onUpdate(newSummary); props.onUpdate(newSummary);
@@ -60,7 +58,7 @@ export default function FinalSummary(props: FinalSummaryProps) {
console.log("Updated long summary:", updatedTranscript); console.log("Updated long summary:", updatedTranscript);
} catch (err) { } catch (err) {
console.error("Failed to update long summary:", 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) {
<Button onClick={onDiscardClick} variant="ghost"> <Button onClick={onDiscardClick} variant="ghost">
Cancel Cancel
</Button> </Button>
<Button onClick={onSaveClick}>Save</Button> <Button
onClick={onSaveClick}
disabled={updateTranscriptMutation.isPending}
>
Save
</Button>
</Flex> </Flex>
)} )}
{!isEditMode && ( {!isEditMode && (

View File

@@ -1,45 +1,30 @@
import { useEffect, useState } from "react";
import { useError } from "../../(errors)/errorContext";
import { CreateTranscript, GetTranscript } from "../../lib/api-types"; import { CreateTranscript, GetTranscript } from "../../lib/api-types";
import useApi from "../../lib/useApi"; import { useTranscriptCreate } from "../../lib/api-hooks";
type UseCreateTranscript = { type UseCreateTranscript = {
transcript: GetTranscript | null; transcript: GetTranscript | null;
loading: boolean; loading: boolean;
error: Error | null; error: Error | null;
create: (transcriptCreationDetails: CreateTranscript) => void; create: (transcriptCreationDetails: CreateTranscript) => Promise<void>;
}; };
const useCreateTranscript = (): UseCreateTranscript => { const useCreateTranscript = (): UseCreateTranscript => {
const [transcript, setTranscript] = useState<GetTranscript | null>(null); const createMutation = useTranscriptCreate();
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = useApi();
const create = (transcriptCreationDetails: CreateTranscript) => { const create = async (transcriptCreationDetails: CreateTranscript) => {
if (loading || !api) return; if (createMutation.isPending) return;
setLoading(true); await createMutation.mutateAsync({
body: transcriptCreationDetails,
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);
}); });
}; };
return { transcript, loading, error, create }; return {
transcript: createMutation.data || null,
loading: createMutation.isPending,
error: createMutation.error as Error | null,
create,
};
}; };
export default useCreateTranscript; export default useCreateTranscript;

View File

@@ -1,6 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import useApi from "../../lib/useApi"; import { useTranscriptUploadAudio } from "../../lib/api-hooks";
import { Button, Spinner } from "@chakra-ui/react"; import { Button, Spinner } from "@chakra-ui/react";
import { useError } from "../../(errors)/errorContext";
type FileUploadButton = { type FileUploadButton = {
transcriptId: string; transcriptId: string;
@@ -8,13 +9,16 @@ type FileUploadButton = {
export default function FileUploadButton(props: FileUploadButton) { export default function FileUploadButton(props: FileUploadButton) {
const fileInputRef = React.useRef<HTMLInputElement>(null); const fileInputRef = React.useRef<HTMLInputElement>(null);
const api = useApi(); const uploadMutation = useTranscriptUploadAudio();
const { setError } = useError();
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const triggerFileUpload = () => { const triggerFileUpload = () => {
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileUpload = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (file) { if (file) {
@@ -24,37 +28,45 @@ export default function FileUploadButton(props: FileUploadButton) {
let start = 0; let start = 0;
let uploadedSize = 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 () => { const uploadNextChunk = async () => {
if (chunkNumber == totalChunks) return; if (chunkNumber == totalChunks) {
setProgress(0);
return;
}
const chunkSize = Math.min(maxChunkSize, file.size - start); const chunkSize = Math.min(maxChunkSize, file.size - start);
const end = start + chunkSize; const end = start + chunkSize;
const chunk = file.slice(start, end); const chunk = file.slice(start, end);
await api?.v1TranscriptRecordUpload({ try {
transcriptId: props.transcriptId, const formData = new FormData();
formData: { formData.append("chunk", chunk);
chunk,
await uploadMutation.mutateAsync({
params: {
path: {
transcript_id: props.transcriptId,
}, },
chunkNumber, query: {
totalChunks, chunk_number: chunkNumber,
total_chunks: totalChunks,
},
},
body: formData as any,
}); });
uploadedSize += chunkSize; uploadedSize += chunkSize;
const currentProgress = Math.floor((uploadedSize / file.size) * 100);
setProgress(currentProgress);
chunkNumber++; chunkNumber++;
start = end; start = end;
uploadNextChunk(); await uploadNextChunk();
} catch (error) {
setError(error as Error, "Failed to upload file");
setProgress(0);
}
}; };
uploadNextChunk(); uploadNextChunk();

View File

@@ -54,20 +54,30 @@ const TranscriptCreate = () => {
const [loadingUpload, setLoadingUpload] = useState(false); const [loadingUpload, setLoadingUpload] = useState(false);
const getTargetLanguage = () => { const getTargetLanguage = () => {
if (targetLanguage === "NOTRANSLATION") return; if (targetLanguage === "NOTRANSLATION") return undefined;
return targetLanguage; return targetLanguage;
}; };
const send = () => { const send = () => {
if (loadingRecord || createTranscript.loading || permissionDenied) return; if (loadingRecord || createTranscript.loading || permissionDenied) return;
setLoadingRecord(true); setLoadingRecord(true);
createTranscript.create({ name, target_language: getTargetLanguage() }); const targetLang = getTargetLanguage();
createTranscript.create({
name,
source_language: "en",
target_language: targetLang || "en",
});
}; };
const uploadFile = () => { const uploadFile = () => {
if (loadingUpload || createTranscript.loading || permissionDenied) return; if (loadingUpload || createTranscript.loading || permissionDenied) return;
setLoadingUpload(true); setLoadingUpload(true);
createTranscript.create({ name, target_language: getTargetLanguage() }); const targetLang = getTargetLanguage();
createTranscript.create({
name,
source_language: "en",
target_language: targetLang || "en",
});
}; };
useEffect(() => { useEffect(() => {

View File

@@ -132,7 +132,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
"This transcript is public. Everyone can access it."} "This transcript is public. Everyone can access it."}
</Text> </Text>
{isOwner && api && ( {isOwner && (
<Select.Root <Select.Root
key={shareMode.value} key={shareMode.value}
value={[shareMode.value || ""]} value={[shareMode.value || ""]}

View File

@@ -17,7 +17,11 @@ import {
useListCollection, useListCollection,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { TbBrandZulip } from "react-icons/tb"; import { TbBrandZulip } from "react-icons/tb";
import useApi from "../../lib/useApi"; import {
useZulipStreams,
useZulipTopics,
useTranscriptPostToZulip,
} from "../../lib/api-hooks";
type ShareZulipProps = { type ShareZulipProps = {
transcriptResponse: GetTranscript; transcriptResponse: GetTranscript;
@@ -37,97 +41,76 @@ interface Topic {
export default function ShareZulip(props: ShareZulipProps & BoxProps) { export default function ShareZulip(props: ShareZulipProps & BoxProps) {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [stream, setStream] = useState<string | undefined>(undefined); const [stream, setStream] = useState<string | undefined>(undefined);
const [selectedStreamId, setSelectedStreamId] = useState<number | null>(null);
const [topic, setTopic] = useState<string | undefined>(undefined); const [topic, setTopic] = useState<string | undefined>(undefined);
const [includeTopics, setIncludeTopics] = useState(false); const [includeTopics, setIncludeTopics] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [streams, setStreams] = useState<Stream[]>([]); // React Query hooks
const [topics, setTopics] = useState<Topic[]>([]); const { data: streams = [], isLoading: isLoadingStreams } =
const api = useApi(); useZulipStreams() as { data: Stream[]; isLoading: boolean };
const { data: topics = [] } = useZulipTopics(selectedStreamId) as {
data: Topic[];
};
const postToZulipMutation = useTranscriptPostToZulip();
const { contains } = useFilter({ sensitivity: "base" }); const { contains } = useFilter({ sensitivity: "base" });
const { const streamItems = useMemo(() => {
collection: streamItemsCollection, return (streams || []).map((stream: Stream) => ({
filter: streamItemsFilter,
set: streamItemsSet,
} = useListCollection({
initialItems: [] as { label: string; value: string }[],
filter: contains,
});
const {
collection: topicItemsCollection,
filter: topicItemsFilter,
set: topicItemsSet,
} = useListCollection({
initialItems: [] as { label: string; value: string }[],
filter: contains,
});
useEffect(() => {
const fetchZulipStreams = async () => {
if (!api) return;
try {
const response = await api.v1ZulipGetStreams();
setStreams(response);
streamItemsSet(
response.map((stream) => ({
label: stream.name, label: stream.name,
value: stream.name, value: stream.name,
})), }));
); }, [streams]);
setIsLoading(false); const topicItems = useMemo(() => {
} catch (error) { return (topics || []).map((topic: Topic) => ({
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, label: topic.name,
value: topic.name, value: topic.name,
})), }));
); }, [topics]);
} else {
topicItemsSet([]);
}
} catch (error) {
console.error("Error fetching Zulip topics:", error);
}
};
fetchZulipTopics(); const { collection: streamItemsCollection, filter: streamItemsFilter } =
}, [stream, streams, api]); useListCollection({
initialItems: streamItems,
filter: contains,
});
const { collection: topicItemsCollection, filter: topicItemsFilter } =
useListCollection({
initialItems: topicItems,
filter: contains,
});
// Update selected stream ID when stream changes
useEffect(() => {
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 () => { const handleSendToZulip = async () => {
if (!api || !props.transcriptResponse) return; if (!props.transcriptResponse) return;
if (stream && topic) { if (stream && topic) {
try { try {
await api.v1TranscriptPostToZulip({ await postToZulipMutation.mutateAsync({
transcriptId: props.transcriptResponse.id, params: {
path: {
transcript_id: props.transcriptResponse.id,
},
query: {
stream, stream,
topic, topic,
includeTopics, include_topics: includeTopics,
},
},
}); });
setShowModal(false); setShowModal(false);
} catch (error) { } catch (error) {
console.log(error); console.error("Error posting to Zulip:", error);
} }
} }
}; };
@@ -155,7 +138,7 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
</Dialog.CloseTrigger> </Dialog.CloseTrigger>
</Dialog.Header> </Dialog.Header>
<Dialog.Body> <Dialog.Body>
{isLoading ? ( {isLoadingStreams ? (
<Flex justify="center" py={8}> <Flex justify="center" py={8}>
<Spinner /> <Spinner />
</Flex> </Flex>

View File

@@ -1,6 +1,7 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { DomainContext } from "../../domainContext"; import { DomainContext } from "../../domainContext";
import getApi from "../../lib/useApi"; import { useTranscriptGet } from "../../lib/api-hooks";
import { useSession } from "next-auth/react";
export type Mp3Response = { export type Mp3Response = {
media: HTMLMediaElement | null; media: HTMLMediaElement | null;
@@ -17,14 +18,17 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
const [audioLoadingError, setAudioLoadingError] = useState<null | string>( const [audioLoadingError, setAudioLoadingError] = useState<null | string>(
null, null,
); );
const [transcriptMetadataLoading, setTranscriptMetadataLoading] =
useState<boolean>(true);
const [transcriptMetadataLoadingError, setTranscriptMetadataLoadingError] =
useState<string | null>(null);
const [audioDeleted, setAudioDeleted] = useState<boolean | null>(null); const [audioDeleted, setAudioDeleted] = useState<boolean | null>(null);
const api = getApi();
const { api_url } = useContext(DomainContext); 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] = const [serviceWorker, setServiceWorker] =
useState<ServiceWorkerRegistration | null>(null); useState<ServiceWorkerRegistration | null>(null);
@@ -52,27 +56,17 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
}, [navigator.serviceWorker, !serviceWorker, accessTokenInfo]); }, [navigator.serviceWorker, !serviceWorker, accessTokenInfo]);
useEffect(() => { useEffect(() => {
if (!transcriptId || !api || later) return; if (!transcriptId || later || !transcript) return;
let stopped = false; let stopped = false;
let audioElement: HTMLAudioElement | null = null; let audioElement: HTMLAudioElement | null = null;
let handleCanPlay: (() => void) | null = null; let handleCanPlay: (() => void) | null = null;
let handleError: (() => void) | null = null; let handleError: (() => void) | null = null;
setTranscriptMetadataLoading(true);
setAudioLoading(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; const deleted = transcript.audio_deleted || false;
setAudioDeleted(deleted); setAudioDeleted(deleted);
setTranscriptMetadataLoadingError(null);
if (deleted) { if (deleted) {
// Audio is deleted, don't attempt to load it // Audio is deleted, don't attempt to load it
@@ -106,18 +100,6 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
if (!stopped) { if (!stopped) {
setMedia(audioElement); 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);
});
return () => { return () => {
stopped = true; stopped = true;
@@ -128,14 +110,18 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
if (handleError) audioElement.removeEventListener("error", handleError); if (handleError) audioElement.removeEventListener("error", handleError);
} }
}; };
}, [transcriptId, api, later, api_url]); }, [transcriptId, transcript, later, api_url]);
const getNow = () => { const getNow = () => {
setLater(false); setLater(false);
}; };
const loading = audioLoading || transcriptMetadataLoading; 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 }; return { media, loading, error, getNow, audioDeleted };
}; };

View File

@@ -1,8 +1,5 @@
import { useEffect, useState } from "react";
import { Participant } from "../../lib/api-types"; import { Participant } from "../../lib/api-types";
import { useError } from "../../(errors)/errorContext"; import { useTranscriptParticipants } from "../../lib/api-hooks";
import useApi from "../../lib/useApi";
import { shouldShowError } from "../../lib/errorUtils";
type ErrorParticipants = { type ErrorParticipants = {
error: Error; error: Error;
@@ -29,46 +26,38 @@ export type UseParticipants = (
) & { refetch: () => void }; ) & { refetch: () => void };
const useParticipants = (transcriptId: string): UseParticipants => { const useParticipants = (transcriptId: string): UseParticipants => {
const [response, setResponse] = useState<Participant[] | null>(null); const {
const [loading, setLoading] = useState<boolean>(true); data: response,
const [error, setErrorState] = useState<Error | null>(null); isLoading: loading,
const { setError } = useError(); error,
const api = useApi(); refetch,
const [count, setCount] = useState(0); } = useTranscriptParticipants(transcriptId || null);
const refetch = () => { // Type-safe return based on state
if (!loading) { if (error) {
setCount(count + 1); return {
setLoading(true); error: error as Error,
setErrorState(null); loading: false,
response: null,
refetch,
} as ErrorParticipants & { refetch: () => void };
} }
};
useEffect(() => { if (loading || !response) {
if (!transcriptId || !api) return; return {
response: response || null,
setLoading(true); loading: true,
api error: null,
.v1TranscriptGetParticipants({ transcriptId }) refetch,
.then((result) => { } as LoadingParticipants & { refetch: () => void };
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; export default useParticipants;

View File

@@ -1,9 +1,5 @@
import { useEffect, useState } from "react";
import { GetTranscriptTopicWithWordsPerSpeaker } from "../../lib/api-types"; import { GetTranscriptTopicWithWordsPerSpeaker } from "../../lib/api-types";
import { useError } from "../../(errors)/errorContext"; import { useTranscriptTopicsWithWordsPerSpeaker } from "../../lib/api-hooks";
import useApi from "../../lib/useApi";
import { shouldShowError } from "../../lib/errorUtils";
type ErrorTopicWithWords = { type ErrorTopicWithWords = {
error: Error; error: Error;
@@ -33,47 +29,41 @@ const useTopicWithWords = (
topicId: string | undefined, topicId: string | undefined,
transcriptId: string, transcriptId: string,
): UseTopicWithWords => { ): UseTopicWithWords => {
const [response, setResponse] = const {
useState<GetTranscriptTopicWithWordsPerSpeaker | null>(null); data: response,
const [loading, setLoading] = useState<boolean>(false); isLoading: loading,
const [error, setErrorState] = useState<Error | null>(null); error,
const { setError } = useError(); refetch,
const api = useApi(); } = useTranscriptTopicsWithWordsPerSpeaker(
transcriptId || null,
topicId || null,
);
const [count, setCount] = useState(0); // Type-safe return based on state
if (error) {
const refetch = () => { return {
if (!loading) { error: error as Error,
setCount(count + 1); loading: false,
setLoading(true); response: null,
setErrorState(null); refetch,
} as ErrorTopicWithWords & { refetch: () => void };
} }
};
useEffect(() => { if (loading || !response) {
if (!transcriptId || !topicId || !api) return; return {
response: response || null,
setLoading(true); loading: true,
error: false,
api refetch,
.v1TranscriptGetTopicsWithWordsPerSpeaker({ transcriptId, topicId }) } as LoadingTopicWithWords & { refetch: () => void };
.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; export default useTopicWithWords;

View File

@@ -1,9 +1,4 @@
import { useEffect, useState } from "react"; import { useTranscriptTopics } from "../../lib/api-hooks";
import { useError } from "../../(errors)/errorContext";
import { Topic } from "./webSocketTypes";
import useApi from "../../lib/useApi";
import { shouldShowError } from "../../lib/errorUtils";
import { GetTranscriptTopic } from "../../lib/api-types"; import { GetTranscriptTopic } from "../../lib/api-types";
type TranscriptTopics = { type TranscriptTopics = {
@@ -13,34 +8,13 @@ type TranscriptTopics = {
}; };
const useTopics = (id: string): TranscriptTopics => { const useTopics = (id: string): TranscriptTopics => {
const [topics, setTopics] = useState<Topic[] | null>(null); const { data: topics, isLoading: loading, error } = useTranscriptTopics(id);
const [loading, setLoading] = useState<boolean>(true);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = useApi();
useEffect(() => {
if (!id || !api) return;
setLoading(true); return {
api topics: topics || null,
.v1TranscriptGetTopics({ transcriptId: id }) loading,
.then((result) => { error: error as Error | null,
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 };
}; };
export default useTopics; export default useTopics;

View File

@@ -1,8 +1,5 @@
import { useEffect, useState } from "react";
import { AudioWaveform } from "../../lib/api-types"; import { AudioWaveform } from "../../lib/api-types";
import { useError } from "../../(errors)/errorContext"; import { useTranscriptWaveform } from "../../lib/api-hooks";
import useApi from "../../lib/useApi";
import { shouldShowError } from "../../lib/errorUtils";
type AudioWaveFormResponse = { type AudioWaveFormResponse = {
waveform: AudioWaveform | null; waveform: AudioWaveform | null;
@@ -11,35 +8,17 @@ type AudioWaveFormResponse = {
}; };
const useWaveform = (id: string, skip: boolean): AudioWaveFormResponse => { const useWaveform = (id: string, skip: boolean): AudioWaveFormResponse => {
const [waveform, setWaveform] = useState<AudioWaveform | null>(null); const {
const [loading, setLoading] = useState<boolean>(false); data: waveform,
const [error, setErrorState] = useState<Error | null>(null); isLoading: loading,
const { setError } = useError(); error,
const api = useApi(); } = useTranscriptWaveform(skip ? null : id);
useEffect(() => { return {
if (!id || !api || skip) { waveform: waveform || null,
setLoading(false); loading,
setErrorState(null); error: error as Error | 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 };
}; };
export default useWaveform; export default useWaveform;

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Peer from "simple-peer"; import Peer from "simple-peer";
import { useError } from "../../(errors)/errorContext"; import { useError } from "../../(errors)/errorContext";
import useApi from "../../lib/useApi"; import { useTranscriptWebRTC } from "../../lib/api-hooks";
import { RtcOffer } from "../../lib/api-types"; import { RtcOffer } from "../../lib/api-types";
const useWebRTC = ( const useWebRTC = (
@@ -10,10 +10,10 @@ const useWebRTC = (
): Peer => { ): Peer => {
const [peer, setPeer] = useState<Peer | null>(null); const [peer, setPeer] = useState<Peer | null>(null);
const { setError } = useError(); const { setError } = useError();
const api = useApi(); const webRTCMutation = useTranscriptWebRTC();
useEffect(() => { useEffect(() => {
if (!stream || !transcriptId || !api) { if (!stream || !transcriptId) {
return; return;
} }
@@ -24,7 +24,7 @@ const useWebRTC = (
try { try {
p = new Peer({ initiator: true, stream: stream }); p = new Peer({ initiator: true, stream: stream });
} catch (error) { } catch (error) {
setError(error, "Error creating WebRTC"); setError(error as Error, "Error creating WebRTC");
return; return;
} }
@@ -32,26 +32,31 @@ const useWebRTC = (
setError(new Error(`WebRTC error: ${err}`)); setError(new Error(`WebRTC error: ${err}`));
}); });
p.on("signal", (data: any) => { p.on("signal", async (data: any) => {
if (!api) return;
if ("sdp" in data) { if ("sdp" in data) {
const rtcOffer: RtcOffer = { const rtcOffer: RtcOffer = {
sdp: data.sdp, sdp: data.sdp,
type: data.type, type: data.type,
}; };
api try {
.v1TranscriptRecordWebrtc({ transcriptId, requestBody: rtcOffer }) const answer = await webRTCMutation.mutateAsync({
.then((answer) => { params: {
path: {
transcript_id: transcriptId,
},
},
body: rtcOffer,
});
try { try {
p.signal(answer); p.signal(answer);
} catch (error) { } catch (error) {
setError(error); setError(error as Error);
}
} catch (error) {
setError(error as Error, "Error loading WebRTCOffer");
} }
})
.catch((error) => {
setError(error, "Error loading WebRTCOffer");
});
} }
}); });
@@ -63,7 +68,7 @@ const useWebRTC = (
return () => { return () => {
p.destroy(); p.destroy();
}; };
}, [stream, transcriptId, !api]); }, [stream, transcriptId, webRTCMutation]);
return peer; return peer;
}; };

View File

@@ -3,7 +3,8 @@ import { Topic, FinalSummary, Status } from "./webSocketTypes";
import { useError } from "../../(errors)/errorContext"; import { useError } from "../../(errors)/errorContext";
import { DomainContext } from "../../domainContext"; import { DomainContext } from "../../domainContext";
import { AudioWaveform, GetTranscriptSegmentTopic } from "../../lib/api-types"; 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 = { export type UseWebSockets = {
transcriptTextLive: string; transcriptTextLive: string;
@@ -34,7 +35,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
const { setError } = useError(); const { setError } = useError();
const { websocket_url } = useContext(DomainContext); const { websocket_url } = useContext(DomainContext);
const api = useApi(); const queryClient = useQueryClient();
const [accumulatedText, setAccumulatedText] = useState<string>(""); const [accumulatedText, setAccumulatedText] = useState<string>("");
@@ -105,6 +106,13 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
title: "Topic 1: Introduction to Quantum Mechanics", title: "Topic 1: Introduction to Quantum Mechanics",
transcript: transcript:
"A brief overview of quantum mechanics and its principles.", "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", id: "2",
@@ -315,9 +323,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
} }
}; };
if (!transcriptId || !api) return; if (!transcriptId) return;
api?.v1TranscriptGetWebsocketEvents({ transcriptId }).then((result) => {});
const url = `${websocket_url}/v1/transcripts/${transcriptId}/events`; const url = `${websocket_url}/v1/transcripts/${transcriptId}/events`;
let ws = new WebSocket(url); let ws = new WebSocket(url);
@@ -361,6 +367,16 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
return [...prevTopics, topic]; return [...prevTopics, topic];
}); });
console.debug("TOPIC event:", message.data); 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; break;
case "FINAL_SHORT_SUMMARY": case "FINAL_SHORT_SUMMARY":
@@ -370,6 +386,16 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
case "FINAL_LONG_SUMMARY": case "FINAL_LONG_SUMMARY":
if (message.data) { if (message.data) {
setFinalSummary(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; break;
@@ -377,6 +403,16 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
console.debug("FINAL_TITLE event:", message.data); console.debug("FINAL_TITLE event:", message.data);
if (message.data) { if (message.data) {
setTitle(message.data.title); 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; break;
@@ -450,7 +486,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
return () => { return () => {
ws.close(); ws.close();
}; };
}, [transcriptId, !api]); }, [transcriptId, websocket_url, queryClient]);
return { return {
transcriptTextLive, transcriptTextLive,

View File

@@ -23,7 +23,7 @@ import { useRouter } from "next/navigation";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import useSessionStatus from "../lib/useSessionStatus"; import useSessionStatus from "../lib/useSessionStatus";
import { useRecordingConsent } from "../recordingConsentContext"; import { useRecordingConsent } from "../recordingConsentContext";
import useApi from "../lib/useApi"; import { useMeetingAudioConsent } from "../lib/api-hooks";
import { Meeting } from "../lib/api-types"; import { Meeting } from "../lib/api-types";
import { FaBars } from "react-icons/fa6"; import { FaBars } from "react-icons/fa6";
@@ -76,31 +76,30 @@ const useConsentDialog = (
wherebyRef: RefObject<HTMLElement> /*accessibility*/, wherebyRef: RefObject<HTMLElement> /*accessibility*/,
) => { ) => {
const { state: consentState, touch, hasConsent } = useRecordingConsent(); const { state: consentState, touch, hasConsent } = useRecordingConsent();
const [consentLoading, setConsentLoading] = useState(false);
// toast would open duplicates, even with using "id=" prop // toast would open duplicates, even with using "id=" prop
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const api = useApi(); const audioConsentMutation = useMeetingAudioConsent();
const handleConsent = useCallback( const handleConsent = useCallback(
async (meetingId: string, given: boolean) => { async (meetingId: string, given: boolean) => {
if (!api) return;
setConsentLoading(true);
try { try {
await api.v1MeetingAudioConsent({ await audioConsentMutation.mutateAsync({
meetingId, params: {
requestBody: { consent_given: given }, path: {
meeting_id: meetingId,
},
},
body: {
consent_given: given,
},
}); });
touch(meetingId); touch(meetingId);
} catch (error) { } catch (error) {
console.error("Error submitting consent:", error); console.error("Error submitting consent:", error);
} finally {
setConsentLoading(false);
} }
}, },
[api, touch], [audioConsentMutation, touch],
); );
const showConsentModal = useCallback(() => { const showConsentModal = useCallback(() => {
@@ -194,7 +193,12 @@ const useConsentDialog = (
return cleanup; return cleanup;
}, [meetingId, handleConsent, wherebyRef, modalOpen]); }, [meetingId, handleConsent, wherebyRef, modalOpen]);
return { showConsentModal, consentState, hasConsent, consentLoading }; return {
showConsentModal,
consentState,
hasConsent,
consentLoading: audioConsentMutation.isPending,
};
}; };
function ConsentDialogButton({ function ConsentDialogButton({

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { useError } from "../(errors)/errorContext"; import { useError } from "../(errors)/errorContext";
import { Meeting } from "../lib/api-types"; import { Meeting } from "../lib/api-types";
import { shouldShowError } from "../lib/errorUtils"; import { shouldShowError } from "../lib/errorUtils";
import useApi from "../lib/useApi"; import { useRoomsCreateMeeting } from "../lib/api-hooks";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
type ErrorMeeting = { type ErrorMeeting = {
@@ -30,27 +30,25 @@ const useRoomMeeting = (
roomName: string | null | undefined, roomName: string | null | undefined,
): ErrorMeeting | LoadingMeeting | SuccessMeeting => { ): ErrorMeeting | LoadingMeeting | SuccessMeeting => {
const [response, setResponse] = useState<Meeting | null>(null); const [response, setResponse] = useState<Meeting | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setErrorState] = useState<Error | null>(null);
const [reload, setReload] = useState(0); const [reload, setReload] = useState(0);
const { setError } = useError(); const { setError } = useError();
const api = useApi(); const createMeetingMutation = useRoomsCreateMeeting();
const reloadHandler = () => setReload((prev) => prev + 1); const reloadHandler = () => setReload((prev) => prev + 1);
useEffect(() => { useEffect(() => {
if (!roomName || !api) return; if (!roomName) return;
if (!response) { const createMeeting = async () => {
setLoading(true); try {
} const result = await createMeetingMutation.mutateAsync({
params: {
api path: {
.v1RoomsCreateMeeting({ roomName }) room_name: roomName,
.then((result) => { },
},
});
setResponse(result); setResponse(result);
setLoading(false); } catch (error: any) {
})
.catch((error) => {
const shouldShowHuman = shouldShowError(error); const shouldShowHuman = shouldShowError(error);
if (shouldShowHuman && error.status !== 404) { if (shouldShowHuman && error.status !== 404) {
setError( setError(
@@ -60,9 +58,14 @@ const useRoomMeeting = (
} else { } else {
setError(error); 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 return { response, loading, error, reload: reloadHandler } as
| ErrorMeeting | ErrorMeeting

View File

@@ -9,20 +9,11 @@ import type { paths } from "../reflector-api";
export function useRoomsList(page: number = 1) { export function useRoomsList(page: number = 1) {
const { setError } = useError(); const { setError } = useError();
return $api.useQuery( return $api.useQuery("get", "/v1/rooms", {
"get",
"/v1/rooms",
{
params: { params: {
query: { page }, query: { page },
}, },
}, });
{
onError: (error) => {
setError(error as Error, "There was an error fetching the rooms");
},
},
);
} }
// Transcripts hooks // Transcripts hooks
@@ -37,10 +28,7 @@ export function useTranscriptsSearch(
) { ) {
const { setError } = useError(); const { setError } = useError();
return $api.useQuery( return $api.useQuery("get", "/v1/transcripts/search", {
"get",
"/v1/transcripts/search",
{
params: { params: {
query: { query: {
q, q,
@@ -50,14 +38,7 @@ export function useTranscriptsSearch(
source_kind: options.source_kind as any, 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() { export function useTranscriptDelete() {
@@ -68,7 +49,9 @@ export function useTranscriptDelete() {
onSuccess: () => { onSuccess: () => {
// Invalidate transcripts queries to refetch // Invalidate transcripts queries to refetch
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: $api.queryOptions("get", "/v1/transcripts/search").queryKey, queryKey: $api.queryOptions("get", "/v1/transcripts/search", {
params: { query: { q: "" } },
}).queryKey,
}); });
}, },
onError: (error) => { onError: (error) => {
@@ -102,9 +85,6 @@ export function useTranscriptGet(transcriptId: string | null) {
}, },
{ {
enabled: !!transcriptId, 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() { export function useZulipStreams() {
const { setError } = useError(); const { setError } = useError();
return $api.useQuery( // @ts-ignore - Zulip endpoint not in OpenAPI spec
"get", return $api.useQuery("get", "/v1/zulip/get-streams" as any, {});
"/v1/zulip/get-streams",
{},
{
onError: (error) => {
setError(error as Error, "There was an error fetching Zulip streams");
},
},
);
} }
export function useZulipTopics(streamId: number | null) { export function useZulipTopics(streamId: number | null) {
const { setError } = useError(); const { setError } = useError();
// @ts-ignore - Zulip endpoint not in OpenAPI spec
return $api.useQuery( return $api.useQuery(
"get", "get",
"/v1/zulip/get-topics", "/v1/zulip/get-topics" as any,
{ {
params: { params: {
query: { stream_id: streamId || 0 }, query: { stream_id: streamId || 0 },
@@ -187,9 +160,6 @@ export function useZulipTopics(streamId: number | null) {
}, },
{ {
enabled: !!streamId, 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() { export function useTranscriptPostToZulip() {
const { setError } = useError(); const { setError } = useError();
return $api.useMutation("post", "/v1/transcripts/{transcript_id}/zulip", { // @ts-ignore - Zulip endpoint not in OpenAPI spec
return $api.useMutation(
"post",
"/v1/transcripts/{transcript_id}/zulip" as any,
{
onError: (error) => { onError: (error) => {
setError(error as Error, "There was an error posting to Zulip"); setError(error as Error, "There was an error posting to Zulip");
}, },
}); },
);
} }
export function useTranscriptUploadAudio() { export function useTranscriptUploadAudio() {
@@ -269,9 +244,6 @@ export function useTranscriptWaveform(transcriptId: string | null) {
}, },
{ {
enabled: !!transcriptId, 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, 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, enabled: !!transcriptId,
onError: (error) => {
setError(error as Error, "There was an error fetching topics");
},
}, },
); );
} }
@@ -329,12 +295,29 @@ export function useTranscriptTopicsWithWords(transcriptId: string | null) {
}, },
{ {
enabled: !!transcriptId, 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, 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 { setError } = useError();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return $api.useMutation( return $api.useMutation(
"post", "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", "/v1/transcripts/{transcript_id}/speaker/assign",
{ {
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
@@ -434,7 +472,7 @@ export function useTranscriptSpeakerMerge() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return $api.useMutation( return $api.useMutation(
"post", "patch",
"/v1/transcripts/{transcript_id}/speaker/merge", "/v1/transcripts/{transcript_id}/speaker/merge",
{ {
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
@@ -504,7 +542,9 @@ export function useTranscriptCreate() {
onSuccess: () => { onSuccess: () => {
// Invalidate transcripts list // Invalidate transcripts list
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: $api.queryOptions("get", "/v1/transcripts/search").queryKey, queryKey: $api.queryOptions("get", "/v1/transcripts/search", {
params: { query: { q: "" } },
}).queryKey,
}); });
}, },
onError: (error) => { 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");
},
});
}

View File

@@ -16,7 +16,6 @@ export type RtcOffer = components["schemas"]["RtcOffer"];
export type GetTranscriptSegmentTopic = export type GetTranscriptSegmentTopic =
components["schemas"]["GetTranscriptSegmentTopic"]; components["schemas"]["GetTranscriptSegmentTopic"];
export type Page_Room_ = components["schemas"]["Page_Room_"]; export type Page_Room_ = components["schemas"]["Page_Room_"];
export type ApiError = components["schemas"]["ApiError"];
export type GetTranscriptTopicWithWordsPerSpeaker = export type GetTranscriptTopicWithWordsPerSpeaker =
components["schemas"]["GetTranscriptTopicWithWordsPerSpeaker"]; components["schemas"]["GetTranscriptTopicWithWordsPerSpeaker"];
export type GetTranscriptMinimal = export type GetTranscriptMinimal =
@@ -24,5 +23,5 @@ export type GetTranscriptMinimal =
// Export any enums or constants that were in the old API // Export any enums or constants that were in the old API
export const $SourceKind = { export const $SourceKind = {
values: ["SINGLE", "CALL", "WHEREBY", "UPLOAD"] as const, values: ["room", "live", "file"] as const,
} as const; } as const;

View File

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