Merge branch 'main' into reenable-non-latin-languages

This commit is contained in:
Koper
2024-01-02 18:09:08 +07:00
196 changed files with 13533 additions and 4368 deletions

View File

@@ -1,11 +1,18 @@
"use client";
import { FiefAuthProvider } from "@fief/fief/nextjs/react";
import { createContext } from "react";
export default function FiefWrapper({ children }) {
export const CookieContext = createContext<{ hasAuthCookie: boolean }>({
hasAuthCookie: false,
});
export default function FiefWrapper({ children, hasAuthCookie }) {
return (
<FiefAuthProvider currentUserPath="/api/current-user">
{children}
</FiefAuthProvider>
<CookieContext.Provider value={{ hasAuthCookie }}>
<FiefAuthProvider currentUserPath="/api/current-user">
{children}
</FiefAuthProvider>
</CookieContext.Provider>
);
}

View File

@@ -1,29 +1,23 @@
"use client";
import {
useFiefIsAuthenticated,
useFiefUserinfo,
} from "@fief/fief/nextjs/react";
import { useFiefIsAuthenticated } from "@fief/fief/nextjs/react";
import Link from "next/link";
export default function UserInfo() {
const isAuthenticated = useFiefIsAuthenticated();
const userinfo = useFiefUserinfo();
return !isAuthenticated ? (
<span className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2">
<Link href="/login" className="outline-none">
Log in or create account
<Link href="/login" className="outline-none" prefetch={false}>
Log in
</Link>
</span>
) : (
<span className="font-light px-2">
{userinfo?.email} (
<span className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px]">
<Link href="/logout" className="outline-none">
<Link href="/logout" className="outline-none" prefetch={false}>
Log out
</Link>
</span>
)
</span>
);
}

View File

@@ -3,7 +3,8 @@ import React, { createContext, useContext, useState } from "react";
interface ErrorContextProps {
error: Error | null;
setError: React.Dispatch<React.SetStateAction<Error | null>>;
humanMessage?: string;
setError: (error: Error, humanMessage?: string) => void;
}
const ErrorContext = createContext<ErrorContextProps | undefined>(undefined);
@@ -22,9 +23,16 @@ interface ErrorProviderProps {
export const ErrorProvider: React.FC<ErrorProviderProps> = ({ children }) => {
const [error, setError] = useState<Error | null>(null);
const [humanMessage, setHumanMessage] = useState<string | undefined>();
const declareError = (error, humanMessage?) => {
setError(error);
setHumanMessage(humanMessage);
};
return (
<ErrorContext.Provider value={{ error, setError }}>
<ErrorContext.Provider
value={{ error, setError: declareError, humanMessage }}
>
{children}
</ErrorContext.Provider>
);

View File

@@ -4,29 +4,51 @@ import { useEffect, useState } from "react";
import * as Sentry from "@sentry/react";
const ErrorMessage: React.FC = () => {
const { error, setError } = useError();
const { error, setError, humanMessage } = useError();
const [isVisible, setIsVisible] = useState<boolean>(false);
// Setup Shortcuts
useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
switch (event.key) {
case "^":
throw new Error("Unhandled Exception thrown by '^' shortcut");
case "$":
setError(
new Error("Unhandled Exception thrown by '$' shortcut"),
"You did this to yourself",
);
}
};
document.addEventListener("keydown", handleKeyPress);
return () => document.removeEventListener("keydown", handleKeyPress);
}, []);
useEffect(() => {
if (error) {
setIsVisible(true);
Sentry.captureException(error);
console.error("Error", error.message, error);
if (humanMessage) {
setIsVisible(true);
Sentry.captureException(Error(humanMessage, { cause: error }));
} else {
Sentry.captureException(error);
}
console.error("Error", error);
}
}, [error]);
if (!isVisible || !error) return null;
if (!isVisible || !humanMessage) return null;
return (
<button
onClick={() => {
setIsVisible(false);
setError(null);
}}
className="max-w-xs z-50 fixed bottom-5 right-5 md:bottom-10 md:right-10 border-solid bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded transition-opacity duration-300 ease-out opacity-100 hover:opacity-80 focus-visible:opacity-80 cursor-pointer transform hover:scale-105 focus-visible:scale-105"
role="alert"
>
<span className="block sm:inline">{error?.message}</span>
<span className="block sm:inline">{humanMessage}</span>
</button>
);
};

View File

@@ -0,0 +1,94 @@
"use client";
import React, { useState } from "react";
import { GetTranscript } from "../../api";
import { Title } from "../../lib/textComponents";
import Pagination from "./pagination";
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGear } from "@fortawesome/free-solid-svg-icons";
import useTranscriptList from "../transcripts/useTranscriptList";
export default function TranscriptBrowser() {
const [page, setPage] = useState<number>(1);
const { loading, response } = useTranscriptList(page);
return (
<div>
{/*
<div className="flex flex-row gap-2">
<input className="text-sm p-2 w-80 ring-1 ring-slate-900/10 shadow-sm rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 caret-blue-500" placeholder="Search" />
</div>
*/}
<div className="flex flex-row gap-2 items-center">
<Title className="mb-5 mt-5 flex-1">Past transcripts</Title>
<Pagination
page={page}
setPage={setPage}
total={response?.total || 0}
size={response?.size || 0}
/>
</div>
{loading && (
<div className="full-screen flex flex-col items-center justify-center">
<FontAwesomeIcon
icon={faGear}
className="animate-spin-slow h-14 w-14 md:h-20 md:w-20"
/>
</div>
)}
{!loading && !response && (
<div className="text-gray-500">
No transcripts found, but you can&nbsp;
<Link href="/transcripts/new" className="underline">
record a meeting
</Link>
&nbsp;to get started.
</div>
)}
<div /** center and max 900px wide */ className="mx-auto max-w-[900px]">
<div className="grid grid-cols-1 gap-2 lg:gap-4 h-full">
{response?.items.map((item: GetTranscript) => (
<div
key={item.id}
className="flex flex-col bg-blue-400/20 rounded-lg md:rounded-xl p-2 md:px-4"
>
<div className="flex flex-col">
<div className="flex flex-row gap-2 items-start">
<Link
href={`/transcripts/${item.id}`}
className="text-1xl font-semibold flex-1 pl-0 hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
>
{item.title || item.name}
</Link>
{item.locked ? (
<div className="inline-block bg-red-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
Locked
</div>
) : (
<></>
)}
{item.source_language ? (
<div className="inline-block bg-blue-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
{item.source_language}
</div>
) : (
<></>
)}
</div>
<div className="text-xs text-gray-700">
{new Date(item.created_at).toLocaleDateString("en-US")}
</div>
<div className="text-sm">{item.short_summary}</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,75 @@
type PaginationProps = {
page: number;
setPage: (page: number) => void;
total: number;
size: number;
};
export default function Pagination(props: PaginationProps) {
const { page, setPage, total, size } = props;
const totalPages = Math.ceil(total / size);
const pageNumbers = Array.from(
{ length: totalPages },
(_, i) => i + 1,
).filter((pageNumber) => {
if (totalPages <= 3) {
// If there are 3 or fewer total pages, show all pages.
return true;
} else if (page <= 2) {
// For the first two pages, show the first 3 pages.
return pageNumber <= 3;
} else if (page >= totalPages - 1) {
// For the last two pages, show the last 3 pages.
return pageNumber >= totalPages - 2;
} else {
// For all other cases, show 3 pages centered around the current page.
return pageNumber >= page - 1 && pageNumber <= page + 1;
}
});
const canGoPrevious = page > 1;
const canGoNext = page < totalPages;
const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) {
setPage(newPage);
}
};
return (
<div className="flex justify-center space-x-4 my-4">
<button
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
canGoPrevious ? "text-gray-500" : "text-gray-300"
}`}
onClick={() => handlePageChange(page - 1)}
disabled={!canGoPrevious}
>
<i className="fa fa-chevron-left">&lt;</i>
</button>
{pageNumbers.map((pageNumber) => (
<button
key={pageNumber}
className={`w-10 h-10 rounded-full p-2 border ${
page === pageNumber ? "border-gray-600" : "border-gray-300"
} rounded`}
onClick={() => handlePageChange(pageNumber)}
>
{pageNumber}
</button>
))}
<button
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
canGoNext ? "text-gray-500" : "text-gray-300"
}`}
onClick={() => handlePageChange(page + 1)}
disabled={!canGoNext}
>
<i className="fa fa-chevron-right">&gt;</i>
</button>
</div>
);
}

View File

@@ -0,0 +1,50 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { DomainConfig } from "../lib/edgeConfig";
type DomainContextType = Omit<DomainConfig, "auth_callback_url">;
export const DomainContext = createContext<DomainContextType>({
features: {
requireLogin: false,
privacy: true,
browse: false,
sendToZulip: false,
},
api_url: "",
websocket_url: "",
zulip_streams: "",
});
export const DomainContextProvider = ({
config,
children,
}: {
config: DomainConfig;
children: any;
}) => {
const [context, setContext] = useState<DomainContextType>();
useEffect(() => {
if (!config) return;
const { auth_callback_url, ...others } = config;
setContext(others);
}, [config]);
if (!context) return;
return (
<DomainContext.Provider value={context}>{children}</DomainContext.Provider>
);
};
// Get feature config client-side with
export const featureEnabled = (
featureName: "requireLogin" | "privacy" | "browse" | "sendToZulip",
) => {
const context = useContext(DomainContext);
return context.features[featureName] as boolean | undefined;
};
// Get config server-side (out of react) : see lib/edgeConfig.

166
www/app/[domain]/layout.tsx Normal file
View File

@@ -0,0 +1,166 @@
import "../styles/globals.scss";
import { Poppins } from "next/font/google";
import { Metadata, Viewport } from "next";
import FiefWrapper from "../(auth)/fiefWrapper";
import UserInfo from "../(auth)/userInfo";
import { ErrorProvider } from "../(errors)/errorContext";
import ErrorMessage from "../(errors)/errorMessage";
import Image from "next/image";
import Link from "next/link";
import About from "../(aboutAndPrivacy)/about";
import Privacy from "../(aboutAndPrivacy)/privacy";
import { DomainContextProvider } from "./domainContext";
import { getConfig } from "../lib/edgeConfig";
import { ErrorBoundary } from "@sentry/nextjs";
import { cookies } from "next/dist/client/components/headers";
import { SESSION_COOKIE_NAME } from "../lib/fief";
const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] });
export const viewport: Viewport = {
themeColor: "black",
width: "device-width",
initialScale: 1,
maximumScale: 1,
};
export const metadata: Metadata = {
metadataBase: new URL(process.env.DEV_URL || "https://reflector.media"),
title: {
template: "%s Reflector",
default: "Reflector - AI-Powered Meeting Transcriptions by Monadical",
},
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise",
applicationName: "Reflector",
referrer: "origin-when-cross-origin",
keywords: ["Reflector", "Monadical", "AI", "Meetings", "Transcription"],
authors: [{ name: "Monadical Team", url: "https://monadical.com/team.html" }],
formatDetection: {
email: false,
address: false,
telephone: false,
},
openGraph: {
title: "Reflector",
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise.",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Reflector",
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise.",
images: ["/r-icon.png"],
},
icons: {
icon: "/r-icon.png",
shortcut: "/r-icon.png",
apple: "/r-icon.png",
},
robots: { index: false, follow: false, noarchive: true, noimageindex: true },
};
type LayoutProps = {
params: {
domain: string;
};
children: any;
};
export default async function RootLayout({ children, params }: LayoutProps) {
const config = await getConfig(params.domain);
const { requireLogin, privacy, browse } = config.features;
const hasAuthCookie = !!cookies().get(SESSION_COOKIE_NAME);
return (
<html lang="en">
<body className={poppins.className + " h-screen relative"}>
<FiefWrapper hasAuthCookie={hasAuthCookie}>
<DomainContextProvider config={config}>
<ErrorBoundary fallback={<p>"something went really wrong"</p>}>
<ErrorProvider>
<ErrorMessage />
<div
id="container"
className="items-center h-[100svh] w-[100svw] p-2 md:p-4 grid grid-rows-layout gap-2 md:gap-4"
>
<header className="flex justify-between items-center w-full">
{/* Logo on the left */}
<Link
href="/"
className="flex outline-blue-300 md:outline-none focus-visible:underline underline-offset-2 decoration-[.5px] decoration-gray-500"
>
<Image
src="/reach.png"
width={16}
height={16}
className="h-10 w-auto"
alt="Reflector"
/>
<div className="hidden flex-col ml-2 md:block">
<h1 className="text-[38px] font-bold tracking-wide leading-tight">
Reflector
</h1>
<p className="text-gray-500 text-xs tracking-tighter">
Capture the signal, not the noise
</p>
</div>
</Link>
<div>
{/* Text link on the right */}
<Link
href="/transcripts/new"
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
>
Create
</Link>
{browse ? (
<>
&nbsp;·&nbsp;
<Link
href="/browse"
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
prefetch={false}
>
Browse
</Link>
</>
) : (
<></>
)}
&nbsp;·&nbsp;
<About buttonText="About" />
{privacy ? (
<>
&nbsp;·&nbsp;
<Privacy buttonText="Privacy" />
</>
) : (
<></>
)}
{requireLogin ? (
<>
&nbsp;·&nbsp;
<UserInfo />
</>
) : (
<></>
)}
</div>
</header>
{children}
</div>
</ErrorProvider>
</ErrorBoundary>
</DomainContextProvider>
</FiefWrapper>
</body>
</html>
);
}

View File

@@ -0,0 +1,154 @@
"use client";
import Modal from "../modal";
import useTranscript from "../useTranscript";
import useTopics from "../useTopics";
import useWaveform from "../useWaveform";
import useMp3 from "../useMp3";
import { TopicList } from "../topicList";
import { Topic } from "../webSocketTypes";
import React, { useEffect, useState } from "react";
import "../../../styles/button.css";
import FinalSummary from "../finalSummary";
import ShareLink from "../shareLink";
import QRCode from "react-qr-code";
import TranscriptTitle from "../transcriptTitle";
import ShareModal from "./shareModal";
import Player from "../player";
import WaveformLoading from "../waveformLoading";
import { useRouter } from "next/navigation";
import { featureEnabled } from "../../domainContext";
import { toShareMode } from "../../../lib/shareMode";
type TranscriptDetails = {
params: {
transcriptId: string;
};
};
export default function TranscriptDetails(details: TranscriptDetails) {
const transcriptId = details.params.transcriptId;
const router = useRouter();
const transcript = useTranscript(transcriptId);
const topics = useTopics(transcriptId);
const waveform = useWaveform(transcriptId);
const useActiveTopic = useState<Topic | null>(null);
const mp3 = useMp3(transcriptId);
const [showModal, setShowModal] = useState(false);
useEffect(() => {
const statusToRedirect = ["idle", "recording", "processing"];
if (statusToRedirect.includes(transcript.response?.status)) {
const newUrl = "/transcripts/" + details.params.transcriptId + "/record";
// Shallow redirection does not work on NextJS 13
// https://github.com/vercel/next.js/discussions/48110
// https://github.com/vercel/next.js/discussions/49540
router.push(newUrl, undefined);
// history.replaceState({}, "", newUrl);
}
}, [transcript.response?.status]);
const fullTranscript =
topics.topics
?.map((topic) => topic.transcript)
.join("\n\n")
.replace(/ +/g, " ")
.trim() || "";
if (transcript && transcript.response) {
if (transcript.error || topics?.error) {
return (
<Modal
title="Transcription Not Found"
text="A trascription with this ID does not exist."
/>
);
}
if (!transcriptId || transcript?.loading || topics?.loading) {
return <Modal title="Loading" text={"Loading transcript..."} />;
}
return (
<>
{featureEnabled("sendToZulip") && (
<ShareModal
transcript={transcript.response}
topics={topics ? topics.topics : null}
show={showModal}
setShow={(v) => setShowModal(v)}
/>
)}
<div className="flex flex-col">
{transcript?.response?.title && (
<TranscriptTitle
title={transcript.response.title}
transcriptId={transcript.response.id}
/>
)}
{waveform.waveform && mp3.media ? (
<Player
topics={topics?.topics || []}
useActiveTopic={useActiveTopic}
waveform={waveform.waveform}
media={mp3.media}
mediaDuration={transcript.response.duration}
/>
) : waveform.error ? (
<div>"error loading this recording"</div>
) : (
<WaveformLoading />
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-2 lg:grid-rows-1 gap-2 lg:gap-4 h-full">
<TopicList
topics={topics.topics || []}
useActiveTopic={useActiveTopic}
autoscroll={false}
/>
<div className="w-full h-full grid grid-rows-layout-one grid-cols-1 gap-2 lg:gap-4">
<section className=" bg-blue-400/20 rounded-lg md:rounded-xl p-2 md:px-4 h-full">
{transcript.response.long_summary ? (
<FinalSummary
fullTranscript={fullTranscript}
summary={transcript.response.long_summary}
transcriptId={transcript.response.id}
openZulipModal={() => setShowModal(true)}
/>
) : (
<div className="flex flex-col h-full justify-center content-center">
{transcript.response.status == "processing" ? (
<p>Loading Transcript</p>
) : (
<p>
There was an error generating the final summary, please
come back later
</p>
)}
</div>
)}
</section>
<section className="flex items-center">
<div className="mr-4 hidden md:block h-auto">
<QRCode
value={`${location.origin}/transcripts/${details.params.transcriptId}`}
level="L"
size={98}
/>
</div>
<div className="flex-grow max-w-full">
<ShareLink
transcriptId={transcript?.response?.id}
userId={transcript?.response?.user_id}
shareMode={toShareMode(transcript?.response?.share_mode)}
/>
</div>
</section>
</div>
</div>
</>
);
}
}

View File

@@ -6,14 +6,17 @@ import useWebRTC from "../../useWebRTC";
import useTranscript from "../../useTranscript";
import { useWebSockets } from "../../useWebSockets";
import useAudioDevice from "../../useAudioDevice";
import "../../../styles/button.css";
import "../../../../styles/button.css";
import { Topic } from "../../webSocketTypes";
import getApi from "../../../lib/getApi";
import LiveTrancription from "../../liveTranscription";
import DisconnectedIndicator from "../../disconnectedIndicator";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGear } from "@fortawesome/free-solid-svg-icons";
import { lockWakeState, releaseWakeState } from "../../../lib/wakeLock";
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
import { useRouter } from "next/navigation";
import Player from "../../player";
import useMp3 from "../../useMp3";
import WaveformLoading from "../../waveformLoading";
type TranscriptDetails = {
params: {
@@ -37,14 +40,17 @@ const TranscriptRecord = (details: TranscriptDetails) => {
}, []);
const transcript = useTranscript(details.params.transcriptId);
const api = getApi();
const webRTC = useWebRTC(stream, details.params.transcriptId, api);
const webRTC = useWebRTC(stream, details.params.transcriptId);
const webSockets = useWebSockets(details.params.transcriptId);
const { audioDevices, getAudioStream } = useAudioDevice();
const [hasRecorded, setHasRecorded] = useState(false);
const [recordedTime, setRecordedTime] = useState(0);
const [startTime, setStartTime] = useState(0);
const [transcriptStarted, setTranscriptStarted] = useState(false);
let mp3 = useMp3(details.params.transcriptId, true);
const router = useRouter();
useEffect(() => {
if (!transcriptStarted && webSockets.transcriptText.length !== 0)
@@ -52,15 +58,25 @@ const TranscriptRecord = (details: TranscriptDetails) => {
}, [webSockets.transcriptText]);
useEffect(() => {
if (transcript?.response?.longSummary) {
const newUrl = `/transcripts/${transcript.response.id}`;
const statusToRedirect = ["ended", "error"];
//TODO if has no topic and is error, get back to new
if (
statusToRedirect.includes(transcript.response?.status) ||
statusToRedirect.includes(webSockets.status.value)
) {
const newUrl = "/transcripts/" + details.params.transcriptId;
// Shallow redirection does not work on NextJS 13
// https://github.com/vercel/next.js/discussions/48110
// https://github.com/vercel/next.js/discussions/49540
// router.push(newUrl, undefined, { shallow: true });
history.replaceState({}, "", newUrl);
}
});
router.replace(newUrl);
// history.replaceState({}, "", newUrl);
} // history.replaceState({}, "", newUrl);
}, [webSockets.status.value, transcript.response?.status]);
useEffect(() => {
if (transcript.response?.status === "ended") mp3.getNow();
}, [transcript.response]);
useEffect(() => {
lockWakeState();
@@ -71,19 +87,32 @@ const TranscriptRecord = (details: TranscriptDetails) => {
return (
<>
<Recorder
setStream={setStream}
onStop={() => {
setStream(null);
setHasRecorded(true);
webRTC?.send(JSON.stringify({ cmd: "STOP" }));
}}
topics={webSockets.topics}
getAudioStream={getAudioStream}
useActiveTopic={useActiveTopic}
isPastMeeting={false}
audioDevices={audioDevices}
/>
{webSockets.waveform && webSockets.duration && mp3?.media ? (
<Player
topics={webSockets.topics || []}
useActiveTopic={useActiveTopic}
waveform={webSockets.waveform}
media={mp3.media}
mediaDuration={webSockets.duration}
/>
) : recordedTime ? (
<WaveformLoading />
) : (
<Recorder
setStream={setStream}
onStop={() => {
setStream(null);
setRecordedTime(Date.now() - startTime);
webRTC?.send(JSON.stringify({ cmd: "STOP" }));
}}
onRecord={() => {
setStartTime(Date.now());
}}
getAudioStream={getAudioStream}
audioDevices={audioDevices}
transcriptId={details.params.transcriptId}
/>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-mobile-inner lg:grid-rows-1 gap-2 lg:gap-4 h-full">
<TopicList
@@ -95,7 +124,7 @@ const TranscriptRecord = (details: TranscriptDetails) => {
<section
className={`w-full h-full bg-blue-400/20 rounded-lg md:rounded-xl p-2 md:px-4`}
>
{!hasRecorded ? (
{!recordedTime ? (
<>
{transcriptStarted && (
<h2 className="md:text-lg font-bold">Transcription</h2>
@@ -129,6 +158,7 @@ const TranscriptRecord = (details: TranscriptDetails) => {
couple of minutes. Please do not navigate away from the page
during this time.
</p>
{/* NTH If login required remove last sentence */}
</div>
)}
</section>

View File

@@ -0,0 +1,159 @@
import React, { useContext, useState, useEffect } from "react";
import SelectSearch from "react-select-search";
import { getZulipMessage, sendZulipMessage } from "../../../lib/zulip";
import { GetTranscript, GetTranscriptTopic } from "../../../api";
import "react-select-search/style.css";
import { DomainContext } from "../../domainContext";
type ShareModal = {
show: boolean;
setShow: (show: boolean) => void;
transcript: GetTranscript | null;
topics: GetTranscriptTopic[] | null;
};
interface Stream {
id: number;
name: string;
topics: string[];
}
interface SelectSearchOption {
name: string;
value: string;
}
const ShareModal = (props: ShareModal) => {
const [stream, setStream] = useState<string | undefined>(undefined);
const [topic, setTopic] = useState<string | undefined>(undefined);
const [includeTopics, setIncludeTopics] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [streams, setStreams] = useState<Stream[]>([]);
const { zulip_streams } = useContext(DomainContext);
useEffect(() => {
fetch(zulip_streams + "/streams.json")
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then((data) => {
data = data.sort((a: Stream, b: Stream) =>
a.name.localeCompare(b.name),
);
setStreams(data);
setIsLoading(false);
// data now contains the JavaScript object decoded from JSON
})
.catch((error) => {
console.error("There was a problem with your fetch operation:", error);
});
}, []);
const handleSendToZulip = () => {
if (!props.transcript) return;
const msg = getZulipMessage(props.transcript, props.topics, includeTopics);
if (stream && topic) sendZulipMessage(stream, topic, msg);
};
if (props.show && isLoading) {
return <div>Loading...</div>;
}
let streamOptions: SelectSearchOption[] = [];
if (streams) {
streams.forEach((stream) => {
const value = stream.name;
streamOptions.push({ name: value, value: value });
});
}
return (
<div className="absolute">
{props.show && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 w-96 shadow-lg rounded-md bg-white">
<div className="mt-3 text-center">
<h3 className="font-bold text-xl">Send to Zulip</h3>
{/* Checkbox for 'Include Topics' */}
<div className="mt-4 text-left ml-5">
<label className="flex items-center">
<input
type="checkbox"
className="form-checkbox rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
checked={includeTopics}
onChange={(e) => setIncludeTopics(e.target.checked)}
/>
<span className="ml-2">Include topics</span>
</label>
</div>
<div className="flex items-center mt-4">
<span className="mr-2">#</span>
<SelectSearch
search={true}
options={streamOptions}
value={stream}
onChange={(val) => {
setTopic(undefined);
setStream(val.toString());
}}
placeholder="Pick a stream"
/>
</div>
{stream && (
<>
<div className="flex items-center mt-4">
<span className="mr-2 invisible">#</span>
<SelectSearch
search={true}
options={
streams
.find((s) => s.name == stream)
?.topics.sort((a: string, b: string) =>
a.localeCompare(b),
)
.map((t) => ({ name: t, value: t })) || []
}
value={topic}
onChange={(val) => setTopic(val.toString())}
placeholder="Pick a topic"
/>
</div>
</>
)}
<button
className={`bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded py-2 px-4 mr-3 ${
!stream || !topic ? "opacity-50 cursor-not-allowed" : ""
}`}
disabled={!stream || !topic}
onClick={() => {
handleSendToZulip();
props.setShow(false);
}}
>
Send to Zulip
</button>
<button
className="bg-red-500 hover:bg-red-700 focus-visible:bg-red-700 text-white rounded py-2 px-4 mt-4"
onClick={() => props.setShow(false)}
>
Close
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default ShareModal;

View File

@@ -0,0 +1,44 @@
import { useState } from "react";
import { useError } from "../../(errors)/errorContext";
import { GetTranscript, CreateTranscript } from "../../api";
import useApi from "../../lib/useApi";
type UseTranscript = {
transcript: GetTranscript | null;
loading: boolean;
error: Error | null;
create: (transcriptCreationDetails: CreateTranscript) => void;
};
const useCreateTranscript = (): UseTranscript => {
const [transcript, setTranscript] = useState<GetTranscript | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = useApi();
const create = (transcriptCreationDetails: CreateTranscript) => {
if (loading || !api) return;
setLoading(true);
api
.v1TranscriptsCreate(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 };
};
export default useCreateTranscript;

View File

@@ -0,0 +1,50 @@
import React from "react";
import useApi from "../../lib/useApi";
import { Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post } from "../../api";
type FileUploadButton = {
transcriptId: string;
};
export default function FileUploadButton(props: FileUploadButton) {
const fileInputRef = React.useRef<HTMLInputElement>(null);
const api = useApi();
const triggerFileUpload = () => {
fileInputRef.current?.click();
};
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
console.log("Calling api.v1TranscriptRecordUpload()...");
// Create an object of the expected type
const uploadData = {
file: file,
// Add other properties if required by the type definition
};
api?.v1TranscriptRecordUpload(props.transcriptId, uploadData);
}
};
return (
<>
<button
className="bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white ml-2 md:ml:4 md:h-[78px] md:min-w-[100px] text-lg"
onClick={triggerFileUpload}
>
Upload File
</button>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={handleFileUpload}
/>
</>
);
}

View File

@@ -0,0 +1,186 @@
import { useRef, useState } from "react";
import React from "react";
import Markdown from "react-markdown";
import "../../styles/markdown.css";
import { featureEnabled } from "../domainContext";
import { UpdateTranscript } from "../../api";
import useApi from "../../lib/useApi";
type FinalSummaryProps = {
summary: string;
fullTranscript: string;
transcriptId: string;
openZulipModal: () => void;
};
export default function FinalSummary(props: FinalSummaryProps) {
const finalSummaryRef = useRef<HTMLParagraphElement>(null);
const [isCopiedSummary, setIsCopiedSummary] = useState(false);
const [isCopiedTranscript, setIsCopiedTranscript] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [preEditSummary, setPreEditSummary] = useState(props.summary);
const [editedSummary, setEditedSummary] = useState(props.summary);
const updateSummary = async (newSummary: string, transcriptId: string) => {
try {
const api = useApi();
const requestBody: UpdateTranscript = {
long_summary: newSummary,
};
const updatedTranscript = await api?.v1TranscriptUpdate(
transcriptId,
requestBody,
);
console.log("Updated long summary:", updatedTranscript);
} catch (err) {
console.error("Failed to update long summary:", err);
}
};
const onCopySummaryClick = () => {
let text_to_copy = finalSummaryRef.current?.innerText;
text_to_copy &&
navigator.clipboard.writeText(text_to_copy).then(() => {
setIsCopiedSummary(true);
// Reset the copied state after 2 seconds
setTimeout(() => setIsCopiedSummary(false), 2000);
});
};
const onCopyTranscriptClick = () => {
let text_to_copy = props.fullTranscript;
text_to_copy &&
navigator.clipboard.writeText(text_to_copy).then(() => {
setIsCopiedTranscript(true);
// Reset the copied state after 2 seconds
setTimeout(() => setIsCopiedTranscript(false), 2000);
});
};
const onEditClick = () => {
setPreEditSummary(editedSummary);
setIsEditMode(true);
};
const onDiscardClick = () => {
setEditedSummary(preEditSummary);
setIsEditMode(false);
};
const onSaveClick = () => {
updateSummary(editedSummary, props.transcriptId);
setIsEditMode(false);
};
const handleTextAreaKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
onDiscardClick();
}
if (e.key === "Enter" && e.shiftKey) {
onSaveClick();
e.preventDefault(); // prevent the default action of adding a new line
}
};
return (
<div
className={
(isEditMode ? "overflow-y-none" : "overflow-y-auto") +
" max-h-full flex flex-col h-full"
}
>
<div className="flex flex-row flex-wrap-reverse justify-between items-center">
<h2 className="text-lg sm:text-xl md:text-2xl font-bold">
Final Summary
</h2>
<div className="ml-auto flex space-x-2 mb-2">
{isEditMode && (
<>
<button
onClick={onDiscardClick}
className={"text-gray-500 text-sm hover:underline"}
>
Discard Changes
</button>
<button
onClick={onSaveClick}
className={
"bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2"
}
>
Save Changes
</button>
</>
)}
{!isEditMode && (
<>
{featureEnabled("sendToZulip") && (
<button
className={
"bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base"
}
onClick={() => props.openZulipModal()}
>
<span className="text-xs"> Zulip</span>
</button>
)}
<button
onClick={onEditClick}
className={
"bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base"
}
>
<span className="text-xs"> Summary</span>
</button>
<button
onClick={onCopyTranscriptClick}
className={
(isCopiedTranscript ? "bg-blue-500" : "bg-blue-400") +
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base"
}
style={{ minHeight: "30px" }}
>
<span className="text-xs">
{isCopiedTranscript ? "Copied!" : "Copy Transcript"}
</span>
</button>
<button
onClick={onCopySummaryClick}
className={
(isCopiedSummary ? "bg-blue-500" : "bg-blue-400") +
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base"
}
style={{ minHeight: "30px" }}
>
<span className="text-xs">
{isCopiedSummary ? "Copied!" : "Copy Summary"}
</span>
</button>
</>
)}
</div>
</div>
{isEditMode ? (
<div className="flex-grow overflow-y-none">
<textarea
value={editedSummary}
onChange={(e) => setEditedSummary(e.target.value)}
className="markdown w-full h-full d-block p-2 border rounded shadow-sm"
onKeyDown={(e) => handleTextAreaKeyDown(e)}
/>
</div>
) : (
<p ref={finalSummaryRef} className="markdown">
<Markdown>{editedSummary}</Markdown>
</p>
)}
</div>
);
}

View File

@@ -2,22 +2,23 @@
import React, { useEffect, useState } from "react";
import useAudioDevice from "../useAudioDevice";
import "react-select-search/style.css";
import "../../styles/button.css";
import "../../styles/form.scss";
import getApi from "../../lib/getApi";
import About from "../../(aboutAndPrivacy)/about";
import Privacy from "../../(aboutAndPrivacy)/privacy";
import "../../../styles/button.css";
import "../../../styles/form.scss";
import About from "../../../(aboutAndPrivacy)/about";
import Privacy from "../../../(aboutAndPrivacy)/privacy";
import { useRouter } from "next/navigation";
import useCreateTranscript from "../createTranscript";
import SelectSearch from "react-select-search";
import { supportedLanguages } from "../../supportedLanguages";
import { useFiefIsAuthenticated } from "@fief/fief/nextjs/react";
import { featureEnabled } from "../../domainContext";
const TranscriptCreate = () => {
// const transcript = useTranscript(stream, api);
const router = useRouter();
const api = getApi();
const isAuthenticated = useFiefIsAuthenticated();
const requireLogin = featureEnabled("requireLogin");
const [name, setName] = useState<string>();
const [name, setName] = useState<string>("");
const nameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
@@ -34,13 +35,13 @@ const TranscriptCreate = () => {
const send = () => {
if (loadingSend || createTranscript.loading || permissionDenied) return;
setLoadingSend(true);
createTranscript.create({ name, targetLanguage });
createTranscript.create({ name, target_language: targetLanguage });
};
useEffect(() => {
createTranscript.response &&
router.push(`/transcripts/${createTranscript.response.id}/record`);
}, [createTranscript.response]);
createTranscript.transcript &&
router.push(`/transcripts/${createTranscript.transcript.id}/record`);
}, [createTranscript.transcript]);
useEffect(() => {
if (createTranscript.error) setLoadingSend(false);
@@ -58,6 +59,7 @@ const TranscriptCreate = () => {
<h1 className="text-2xl font-bold mb-2">
Welcome to reflector.media
</h1>
<button>Test upload</button>
<p>
Reflector is a transcription and summarization pipeline that
transforms audio into knowledge.
@@ -73,61 +75,72 @@ const TranscriptCreate = () => {
In order to use Reflector, we kindly request permission to access
your microphone during meetings and events.
</p>
<Privacy buttonText="Privacy policy" />
{featureEnabled("privacy") && (
<Privacy buttonText="Privacy policy" />
)}
</div>
</section>
<section className="flex flex-col justify-center items-center w-full h-full">
<div className="rounded-xl md:bg-blue-200 md:w-96 p-4 lg:p-6 flex flex-col mb-4 md:mb-10">
<h2 className="text-2xl font-bold mt-2 mb-2"> Try Reflector</h2>
<label className="mb-3">
<p>Recording name</p>
<div className="select-search-container">
<input
className="select-search-input"
type="text"
onChange={nameChange}
placeholder="Optional"
/>
</div>
</label>
<label className="mb-3">
<p>Do you want to enable live translation?</p>
<SelectSearch
search
options={supportedLanguages}
value={targetLanguage}
onChange={onLanguageChange}
placeholder="Choose your language"
/>
</label>
{loading ? (
<p className="">Checking permissions...</p>
) : permissionOk ? (
<p className=""> Microphone permission granted </p>
) : permissionDenied ? (
<p className="">
Permission to use your microphone was denied, please change the
permission setting in your browser and refresh this page.
</p>
) : (
<button
className="mt-4 bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white font-bold py-2 px-4 rounded"
onClick={requestPermission}
disabled={permissionDenied}
>
Request Microphone Permission
</button>
)}
{requireLogin && !isAuthenticated ? (
<button
className="mt-4 bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white font-bold py-2 px-4 rounded"
onClick={send}
disabled={!permissionOk || loadingSend}
onClick={() => router.push("/login")}
>
{loadingSend ? "Loading..." : "Confirm"}
Log in
</button>
</div>
) : (
<div className="rounded-xl md:bg-blue-200 md:w-96 p-4 lg:p-6 flex flex-col mb-4 md:mb-10">
<h2 className="text-2xl font-bold mt-2 mb-2">Try Reflector</h2>
<label className="mb-3">
<p>Recording name</p>
<div className="select-search-container">
<input
className="select-search-input"
type="text"
onChange={nameChange}
placeholder="Optional"
/>
</div>
</label>
<label className="mb-3">
<p>Do you want to enable live translation?</p>
<SelectSearch
search
options={supportedLanguages}
value={targetLanguage}
onChange={onLanguageChange}
placeholder="Choose your language"
/>
</label>
{loading ? (
<p className="">Checking permissions...</p>
) : permissionOk ? (
<p className=""> Microphone permission granted </p>
) : permissionDenied ? (
<p className="">
Permission to use your microphone was denied, please change
the permission setting in your browser and refresh this page.
</p>
) : (
<button
className="mt-4 bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white font-bold py-2 px-4 rounded"
onClick={requestPermission}
disabled={permissionDenied}
>
Request Microphone Permission
</button>
)}
<button
className="mt-4 bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white font-bold py-2 px-4 rounded"
onClick={send}
disabled={!permissionOk || loadingSend}
>
{loadingSend ? "Loading..." : "Confirm"}
</button>
</div>
)}
</section>
</div>
</>

View File

@@ -0,0 +1,166 @@
import React, { useRef, useEffect, useState } from "react";
import WaveSurfer from "wavesurfer.js";
import CustomRegionsPlugin from "../../lib/custom-plugins/regions";
import { formatTime } from "../../lib/time";
import { Topic } from "./webSocketTypes";
import { AudioWaveform } from "../../api";
import { waveSurferStyles } from "../../styles/recorder";
type PlayerProps = {
topics: Topic[];
useActiveTopic: [
Topic | null,
React.Dispatch<React.SetStateAction<Topic | null>>,
];
waveform: AudioWaveform;
media: HTMLMediaElement;
mediaDuration: number;
};
export default function Player(props: PlayerProps) {
const waveformRef = useRef<HTMLDivElement>(null);
const [wavesurfer, setWavesurfer] = useState<WaveSurfer | null>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [currentTime, setCurrentTime] = useState<number>(0);
const [waveRegions, setWaveRegions] = useState<CustomRegionsPlugin | null>(
null,
);
const [activeTopic, setActiveTopic] = props.useActiveTopic;
const topicsRef = useRef(props.topics);
// Waveform setup
useEffect(() => {
if (waveformRef.current) {
// XXX duration is required to prevent recomputing peaks from audio
// However, the current waveform returns only the peaks, and no duration
// And the backend does not save duration properly.
// So at the moment, we deduct the duration from the topics.
// This is not ideal, but it works for now.
const _wavesurfer = WaveSurfer.create({
container: waveformRef.current,
peaks: props.waveform,
hideScrollbar: true,
autoCenter: true,
barWidth: 2,
height: "auto",
duration: props.mediaDuration,
...waveSurferStyles.player,
});
// styling
const wsWrapper = _wavesurfer.getWrapper();
wsWrapper.style.cursor = waveSurferStyles.playerStyle.cursor;
wsWrapper.style.backgroundColor =
waveSurferStyles.playerStyle.backgroundColor;
wsWrapper.style.borderRadius = waveSurferStyles.playerStyle.borderRadius;
_wavesurfer.on("play", () => {
setIsPlaying(true);
});
_wavesurfer.on("pause", () => {
setIsPlaying(false);
});
_wavesurfer.on("timeupdate", setCurrentTime);
setWaveRegions(_wavesurfer.registerPlugin(CustomRegionsPlugin.create()));
_wavesurfer.toggleInteraction(true);
_wavesurfer.setMediaElement(props.media);
setWavesurfer(_wavesurfer);
return () => {
_wavesurfer.destroy();
setIsPlaying(false);
setCurrentTime(0);
};
}
}, []);
useEffect(() => {
if (!wavesurfer) return;
if (!props.media) return;
wavesurfer.setMediaElement(props.media);
}, [props.media, wavesurfer]);
useEffect(() => {
topicsRef.current = props.topics;
renderMarkers();
}, [props.topics, waveRegions]);
const renderMarkers = () => {
if (!waveRegions) return;
waveRegions.clearRegions();
for (let topic of topicsRef.current) {
const content = document.createElement("div");
content.setAttribute("style", waveSurferStyles.marker);
content.onmouseover = () => {
content.style.backgroundColor =
waveSurferStyles.markerHover.backgroundColor;
content.style.zIndex = "999";
content.style.width = "300px";
};
content.onmouseout = () => {
content.setAttribute("style", waveSurferStyles.marker);
};
content.textContent = topic.title;
const region = waveRegions.addRegion({
start: topic.timestamp,
content,
color: "f00",
drag: false,
});
region.on("click", (e) => {
e.stopPropagation();
setActiveTopic(topic);
wavesurfer?.setTime(region.start);
});
}
};
useEffect(() => {
if (activeTopic) {
wavesurfer?.setTime(activeTopic.timestamp);
}
}, [activeTopic]);
const handlePlayClick = () => {
wavesurfer?.playPause();
};
const timeLabel = () => {
if (props.mediaDuration)
return `${formatTime(currentTime)}/${formatTime(props.mediaDuration)}`;
return "";
};
return (
<div className="flex items-center w-full relative">
<div className="flex-grow items-end relative">
<div
ref={waveformRef}
className="flex-grow rounded-lg md:rounded-xl h-20"
></div>
<div className="absolute right-2 bottom-0">{timeLabel()}</div>
</div>
<button
className={`${
isPlaying
? "bg-orange-400 hover:bg-orange-500 focus-visible:bg-orange-500"
: "bg-green-400 hover:bg-green-500 focus-visible:bg-green-500"
} text-white ml-2 md:ml:4 md:h-[78px] md:min-w-[100px] text-lg`}
id="play-btn"
onClick={handlePlayClick}
>
{isPlaying ? "Pause" : "Play"}
</button>
</div>
);
}

View File

@@ -1,34 +1,26 @@
import React, { useRef, useEffect, useState } from "react";
import WaveSurfer from "wavesurfer.js";
import RecordPlugin from "../lib/custom-plugins/record";
import CustomRegionsPlugin from "../lib/custom-plugins/regions";
import RecordPlugin from "../../lib/custom-plugins/record";
import CustomRegionsPlugin from "../../lib/custom-plugins/regions";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMicrophone } from "@fortawesome/free-solid-svg-icons";
import { faDownload } from "@fortawesome/free-solid-svg-icons";
import { formatTime } from "../lib/time";
import { Topic } from "./webSocketTypes";
import { AudioWaveform } from "../api";
import { formatTime } from "../../lib/time";
import AudioInputsDropdown from "./audioInputsDropdown";
import { Option } from "react-dropdown";
import { useError } from "../(errors)/errorContext";
import { waveSurferStyles } from "../styles/recorder";
import { waveSurferStyles } from "../../styles/recorder";
import { useError } from "../../(errors)/errorContext";
import FileUploadButton from "./fileUploadButton";
type RecorderProps = {
setStream?: React.Dispatch<React.SetStateAction<MediaStream | null>>;
onStop?: () => void;
topics: Topic[];
getAudioStream?: (deviceId) => Promise<MediaStream | null>;
audioDevices?: Option[];
useActiveTopic: [
Topic | null,
React.Dispatch<React.SetStateAction<Topic | null>>,
];
waveform?: AudioWaveform | null;
isPastMeeting: boolean;
transcriptId?: string | null;
setStream: React.Dispatch<React.SetStateAction<MediaStream | null>>;
onStop: () => void;
onRecord?: () => void;
getAudioStream: (deviceId) => Promise<MediaStream | null>;
audioDevices: Option[];
transcriptId: string;
};
export default function Recorder(props: RecorderProps) {
@@ -36,7 +28,7 @@ export default function Recorder(props: RecorderProps) {
const [wavesurfer, setWavesurfer] = useState<WaveSurfer | null>(null);
const [record, setRecord] = useState<RecordPlugin | null>(null);
const [isRecording, setIsRecording] = useState<boolean>(false);
const [hasRecorded, setHasRecorded] = useState<boolean>(props.isPastMeeting);
const [hasRecorded, setHasRecorded] = useState<boolean>(false);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [currentTime, setCurrentTime] = useState<number>(0);
const [timeInterval, setTimeInterval] = useState<number | null>(null);
@@ -46,8 +38,6 @@ export default function Recorder(props: RecorderProps) {
);
const [deviceId, setDeviceId] = useState<string | null>(null);
const [recordStarted, setRecordStarted] = useState(false);
const [activeTopic, setActiveTopic] = props.useActiveTopic;
const topicsRef = useRef(props.topics);
const [showDevices, setShowDevices] = useState(false);
const { setError } = useError();
@@ -71,11 +61,6 @@ export default function Recorder(props: RecorderProps) {
if (!record.isRecording()) return;
handleRecClick();
break;
case "%":
setError(new Error("Error triggered by '%' shortcut"));
break;
case "^":
throw new Error("Unhandled Exception thrown by '^' shortcut");
case "(":
location.href = "/login";
break;
@@ -107,11 +92,6 @@ export default function Recorder(props: RecorderProps) {
if (waveformRef.current) {
const _wavesurfer = WaveSurfer.create({
container: waveformRef.current,
url: props.transcriptId
? `${process.env.NEXT_PUBLIC_API_URL}/v1/transcripts/${props.transcriptId}/audio/mp3`
: undefined,
peaks: props.waveform?.data,
hideScrollbar: true,
autoCenter: true,
barWidth: 2,
@@ -120,10 +100,8 @@ export default function Recorder(props: RecorderProps) {
...waveSurferStyles.player,
});
if (!props.transcriptId) {
const _wshack: any = _wavesurfer;
_wshack.renderer.renderSingleCanvas = () => {};
}
const _wshack: any = _wavesurfer;
_wshack.renderer.renderSingleCanvas = () => {};
// styling
const wsWrapper = _wavesurfer.getWrapper();
@@ -143,8 +121,6 @@ export default function Recorder(props: RecorderProps) {
setRecord(_wavesurfer.registerPlugin(RecordPlugin.create()));
setWaveRegions(_wavesurfer.registerPlugin(CustomRegionsPlugin.create()));
if (props.isPastMeeting) _wavesurfer.toggleInteraction(true);
setWavesurfer(_wavesurfer);
return () => {
@@ -156,52 +132,6 @@ export default function Recorder(props: RecorderProps) {
}
}, []);
useEffect(() => {
topicsRef.current = props.topics;
if (!isRecording) renderMarkers();
}, [props.topics, waveRegions]);
const renderMarkers = () => {
if (!waveRegions) return;
waveRegions.clearRegions();
for (let topic of topicsRef.current) {
const content = document.createElement("div");
content.setAttribute("style", waveSurferStyles.marker);
content.onmouseover = () => {
content.style.backgroundColor =
waveSurferStyles.markerHover.backgroundColor;
content.style.zIndex = "999";
content.style.width = "300px";
};
content.onmouseout = () => {
content.setAttribute("style", waveSurferStyles.marker);
};
content.textContent = topic.title;
const region = waveRegions.addRegion({
start: topic.timestamp,
content,
color: "f00",
drag: false,
});
region.on("click", (e) => {
e.stopPropagation();
setActiveTopic(topic);
wavesurfer?.setTime(region.start);
});
}
};
useEffect(() => {
if (!record) return;
return record.on("stopRecording", () => {
renderMarkers();
});
}, [record]);
useEffect(() => {
if (isRecording) {
const interval = window.setInterval(() => {
@@ -218,25 +148,24 @@ export default function Recorder(props: RecorderProps) {
}
}, [isRecording]);
useEffect(() => {
if (activeTopic) {
wavesurfer?.setTime(activeTopic.timestamp);
}
}, [activeTopic]);
const handleRecClick = async () => {
if (!record) return console.log("no record");
if (record.isRecording()) {
if (props.onStop) props.onStop();
record.stopRecording();
if (screenMediaStream) {
screenMediaStream.getTracks().forEach((t) => t.stop());
}
setIsRecording(false);
setHasRecorded(true);
setScreenMediaStream(null);
setDestinationStream(null);
} else {
if (props.onRecord) props.onRecord();
const stream = await getCurrentStream();
if (props.setStream) props.setStream(stream);
waveRegions?.clearRegions();
if (stream) {
await record.startRecording(stream);
setIsRecording(true);
@@ -244,6 +173,76 @@ export default function Recorder(props: RecorderProps) {
}
};
const [screenMediaStream, setScreenMediaStream] =
useState<MediaStream | null>(null);
const handleRecordTabClick = async () => {
if (!record) return console.log("no record");
const stream: MediaStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100,
},
});
if (stream.getAudioTracks().length == 0) {
setError(new Error("No audio track found in screen recording."));
return;
}
setScreenMediaStream(stream);
};
const [destinationStream, setDestinationStream] =
useState<MediaStream | null>(null);
const startTabRecording = async () => {
if (!screenMediaStream) return;
if (!record) return;
if (destinationStream !== null) return console.log("already recording");
// connect mic audio (microphone)
const micStream = await getCurrentStream();
if (!micStream) {
console.log("no microphone audio");
return;
}
// Create MediaStreamSource nodes for the microphone and tab
const audioContext = new AudioContext();
const micSource = audioContext.createMediaStreamSource(micStream);
const tabSource = audioContext.createMediaStreamSource(screenMediaStream);
// Merge channels
// XXX If the length is not the same, we do not receive audio in WebRTC.
// So for now, merge the channels to have only one stereo source
const channelMerger = audioContext.createChannelMerger(1);
micSource.connect(channelMerger, 0, 0);
tabSource.connect(channelMerger, 0, 0);
// Create a MediaStreamDestination node
const destination = audioContext.createMediaStreamDestination();
channelMerger.connect(destination);
// Use the destination's stream for the WebRTC connection
setDestinationStream(destination.stream);
};
useEffect(() => {
if (!record) return;
if (!destinationStream) return;
if (props.setStream) props.setStream(destinationStream);
if (destinationStream) {
record.startRecording(destinationStream);
setIsRecording(true);
}
}, [record, destinationStream]);
useEffect(() => {
startTabRecording();
}, [record, screenMediaStream]);
const handlePlayClick = () => {
wavesurfer?.playPause();
};
@@ -292,23 +291,9 @@ export default function Recorder(props: RecorderProps) {
} text-white ml-2 md:ml:4 md:h-[78px] md:min-w-[100px] text-lg`}
id="play-btn"
onClick={handlePlayClick}
disabled={isRecording}
>
{isPlaying ? "Pause" : "Play"}
</button>
{props.transcriptId && (
<a
title="Download recording"
className="text-center cursor-pointer text-blue-400 hover:text-blue-700 ml-2 md:ml:4 p-2 rounded-lg outline-blue-400"
download={`recording-${
props.transcriptId?.split("-")[0] || "0000"
}`}
href={`${process.env.NEXT_PUBLIC_API_URL}/v1/transcripts/${props.transcriptId}/audio/mp3`}
>
<FontAwesomeIcon icon={faDownload} className="h-5 w-auto" />
</a>
)}
</>
)}
{!hasRecorded && (
@@ -324,6 +309,24 @@ export default function Recorder(props: RecorderProps) {
>
{isRecording ? "Stop" : "Record"}
</button>
<FileUploadButton
transcriptId={props.transcriptId}
></FileUploadButton>
{!isRecording && (
<button
className={`${
isRecording
? "bg-red-400 hover:bg-red-500 focus-visible:bg-red-500"
: "bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500"
} text-white ml-2 md:ml:4 md:h-[78px] md:min-w-[100px] text-lg`}
onClick={handleRecordTabClick}
>
Record
<br />a tab
</button>
)}
{props.audioDevices && props.audioDevices?.length > 0 && deviceId && (
<>
<button

View File

@@ -0,0 +1,153 @@
import React, { useState, useRef, useEffect, use } from "react";
import { featureEnabled } from "../domainContext";
import { useFiefUserinfo } from "@fief/fief/nextjs/react";
import SelectSearch from "react-select-search";
import "react-select-search/style.css";
import "../../styles/button.css";
import "../../styles/form.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import { UpdateTranscript } from "../../api";
import { ShareMode, toShareMode } from "../../lib/shareMode";
import useApi from "../../lib/useApi";
type ShareLinkProps = {
transcriptId: string;
userId: string | null;
shareMode: ShareMode;
};
const ShareLink = (props: ShareLinkProps) => {
const [isCopied, setIsCopied] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const [currentUrl, setCurrentUrl] = useState<string>("");
const requireLogin = featureEnabled("requireLogin");
const [isOwner, setIsOwner] = useState(false);
const [shareMode, setShareMode] = useState<ShareMode>(props.shareMode);
const [shareLoading, setShareLoading] = useState(false);
const userinfo = useFiefUserinfo();
const api = useApi();
useEffect(() => {
setCurrentUrl(window.location.href);
}, []);
useEffect(() => {
setIsOwner(!!(requireLogin && userinfo?.sub === props.userId));
}, [userinfo, props.userId]);
const handleCopyClick = () => {
if (inputRef.current) {
let text_to_copy = inputRef.current.value;
text_to_copy &&
navigator.clipboard.writeText(text_to_copy).then(() => {
setIsCopied(true);
// Reset the copied state after 2 seconds
setTimeout(() => setIsCopied(false), 2000);
});
}
};
const updateShareMode = async (selectedShareMode: string) => {
if (!api)
throw new Error("ShareLink's API should always be ready at this point");
setShareLoading(true);
const requestBody: UpdateTranscript = {
share_mode: toShareMode(selectedShareMode),
};
const updatedTranscript = await api.v1TranscriptUpdate(
props.transcriptId,
requestBody,
);
setShareMode(toShareMode(updatedTranscript.share_mode));
setShareLoading(false);
};
const privacyEnabled = featureEnabled("privacy");
return (
<div
className="p-2 md:p-4 rounded"
style={{ background: "rgba(96, 165, 250, 0.2)" }}
>
{requireLogin && (
<div className="text-sm mb-2">
{shareMode === "private" && (
<p>This transcript is private and can only be accessed by you.</p>
)}
{shareMode === "semi-private" && (
<p>
This transcript is secure. Only authenticated users can access it.
</p>
)}
{shareMode === "public" && (
<p>This transcript is public. Everyone can access it.</p>
)}
{isOwner && api && (
<div className="relative">
<SelectSearch
className="select-search--top select-search"
options={[
{ name: "Private", value: "private" },
{ name: "Secure", value: "semi-private" },
{ name: "Public", value: "public" },
]}
value={shareMode?.toString()}
onChange={updateShareMode}
closeOnSelect={true}
/>
{shareLoading && (
<div className="h-4 w-4 absolute top-1/3 right-3 z-10">
<FontAwesomeIcon
icon={faSpinner}
className="animate-spin-slow text-gray-600 flex-grow rounded-lg md:rounded-xl h-4 w-4"
/>
</div>
)}
</div>
)}
</div>
)}
{!requireLogin && (
<>
{privacyEnabled ? (
<p className="text-sm mb-2">
Share this link to grant others access to this page. The link
includes the full audio recording and is valid for the next 7
days.
</p>
) : (
<p className="text-sm mb-2">
Share this link to allow others to view this page and listen to
the full audio recording.
</p>
)}
</>
)}
<div className="flex items-center">
<input
type="text"
readOnly
value={currentUrl}
ref={inputRef}
onChange={() => {}}
className="border rounded-lg md:rounded-xl p-2 flex-grow flex-shrink overflow-auto mr-2 text-sm bg-slate-100 outline-slate-400"
/>
<button
onClick={handleCopyClick}
className={
(isCopied ? "bg-blue-500" : "bg-blue-400") +
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2"
}
style={{ minHeight: "38px" }}
>
{isCopied ? "Copied!" : "Copy"}
</button>
</div>
</div>
);
};
export default ShareLink;

View File

@@ -4,9 +4,10 @@ import {
faChevronRight,
faChevronDown,
} from "@fortawesome/free-solid-svg-icons";
import { formatTime } from "../lib/time";
import { formatTime } from "../../lib/time";
import ScrollToBottom from "./scrollToBottom";
import { Topic } from "./webSocketTypes";
import { generateHighContrastColor } from "../../lib/utils";
type TopicListProps = {
topics: Topic[];
@@ -103,7 +104,37 @@ export function TopicList({
/>
</div>
{activeTopic?.id == topic.id && (
<div className="p-2">{topic.transcript}</div>
<div className="p-2">
{topic.segments ? (
<>
{topic.segments.map((segment, index: number) => (
<p
key={index}
className="text-left text-slate-500 text-sm md:text-base"
>
<span className="font-mono text-slate-500">
[{formatTime(segment.start)}]
</span>
<span
className="font-bold text-slate-500"
style={{
color: generateHighContrastColor(
`Speaker ${segment.speaker}`,
[96, 165, 250],
),
}}
>
{" "}
(Speaker {segment.speaker}):
</span>{" "}
<span>{segment.text}</span>
</p>
))}
</>
) : (
<>{topic.transcript}</>
)}
</div>
)}
</button>
))}

View File

@@ -0,0 +1,86 @@
import { useState } from "react";
import { UpdateTranscript } from "../../api";
import useApi from "../../lib/useApi";
type TranscriptTitle = {
title: string;
transcriptId: string;
};
const TranscriptTitle = (props: TranscriptTitle) => {
const [displayedTitle, setDisplayedTitle] = useState(props.title);
const [preEditTitle, setPreEditTitle] = useState(props.title);
const [isEditing, setIsEditing] = useState(false);
const api = useApi();
const updateTitle = async (newTitle: string, transcriptId: string) => {
if (!api) return;
try {
const requestBody: UpdateTranscript = {
title: newTitle,
};
const api = useApi();
const updatedTranscript = await api?.v1TranscriptUpdate(
transcriptId,
requestBody,
);
console.log("Updated transcript:", updatedTranscript);
} catch (err) {
console.error("Failed to update transcript:", err);
}
};
const handleTitleClick = () => {
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {
// Use prompt
const newTitle = prompt("Please enter the new title:", displayedTitle);
if (newTitle !== null) {
setDisplayedTitle(newTitle);
updateTitle(newTitle, props.transcriptId);
}
} else {
setPreEditTitle(displayedTitle);
setIsEditing(true);
}
};
const handleChange = (e) => {
setDisplayedTitle(e.target.value);
};
const handleKeyDown = (e) => {
if (e.key === "Enter") {
updateTitle(displayedTitle, props.transcriptId);
setIsEditing(false);
} else if (e.key === "Escape") {
setDisplayedTitle(preEditTitle);
setIsEditing(false);
}
};
return (
<>
{isEditing ? (
<input
type="text"
value={displayedTitle}
onChange={handleChange}
onKeyDown={handleKeyDown}
autoFocus
className="text-2xl lg:text-4xl font-extrabold text-center mb-4 w-full border-none bg-transparent overflow-hidden h-[fit-content]"
/>
) : (
<h2
className="text-2xl lg:text-4xl font-extrabold text-center mb-4 cursor-pointer"
onClick={handleTitleClick}
>
{displayedTitle}
</h2>
)}
</>
);
};
export default TranscriptTitle;

View File

@@ -0,0 +1,64 @@
import { useContext, useEffect, useState } from "react";
import { DomainContext } from "../domainContext";
import getApi from "../../lib/useApi";
import { useFiefAccessTokenInfo } from "@fief/fief/build/esm/nextjs/react";
export type Mp3Response = {
media: HTMLMediaElement | null;
loading: boolean;
getNow: () => void;
};
const useMp3 = (id: string, waiting?: boolean): Mp3Response => {
const [media, setMedia] = useState<HTMLMediaElement | null>(null);
const [later, setLater] = useState(waiting);
const [loading, setLoading] = useState<boolean>(false);
const api = getApi();
const { api_url } = useContext(DomainContext);
const accessTokenInfo = useFiefAccessTokenInfo();
const [serviceWorker, setServiceWorker] =
useState<ServiceWorkerRegistration | null>(null);
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/service-worker.js").then((worker) => {
setServiceWorker(worker);
});
}
return () => {
serviceWorker?.unregister();
};
}, []);
useEffect(() => {
if (!navigator.serviceWorker) return;
if (!navigator.serviceWorker.controller) return;
if (!serviceWorker) return;
// Send the token to the service worker
navigator.serviceWorker.controller.postMessage({
type: "SET_AUTH_TOKEN",
token: accessTokenInfo?.access_token,
});
}, [navigator.serviceWorker, !serviceWorker, accessTokenInfo]);
useEffect(() => {
if (!id || !api || later) return;
// createa a audio element and set the source
setLoading(true);
const audioElement = document.createElement("audio");
audioElement.src = `${api_url}/v1/transcripts/${id}/audio/mp3`;
audioElement.crossOrigin = "anonymous";
audioElement.preload = "auto";
setMedia(audioElement);
setLoading(false);
}, [id, api, later]);
const getNow = () => {
setLater(false);
};
return { media, loading, getNow };
};
export default useMp3;

View File

@@ -1,11 +1,8 @@
import { useEffect, useState } from "react";
import {
DefaultApi,
V1TranscriptGetTopicsRequest,
} from "../api/apis/DefaultApi";
import { TranscriptTopic } from "../api";
import { useError } from "../(errors)/errorContext";
import { useError } from "../../(errors)/errorContext";
import { Topic } from "./webSocketTypes";
import useApi from "../../lib/useApi";
import { shouldShowError } from "../../lib/errorUtils";
type TranscriptTopics = {
topics: Topic[] | null;
@@ -13,38 +10,36 @@ type TranscriptTopics = {
error: Error | null;
};
const useTranscript = (api: DefaultApi, id: string): TranscriptTopics => {
const useTopics = (id: string): TranscriptTopics => {
const [topics, setTopics] = useState<Topic[] | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = useApi();
const getTopics = (id: string) => {
if (!id)
throw new Error("Transcript ID is required to get transcript topics");
useEffect(() => {
if (!id || !api) return;
setLoading(true);
const requestParameters: V1TranscriptGetTopicsRequest = {
transcriptId: id,
};
api
.v1TranscriptGetTopics(requestParameters)
.v1TranscriptGetTopics(id)
.then((result) => {
setTopics(result);
setLoading(false);
console.debug("Transcript topics loaded:", result);
})
.catch((err) => {
setError(err);
setErrorState(err);
const shouldShowHuman = shouldShowError(err);
if (shouldShowHuman) {
setError(err, "There was an error loading the topics");
} else {
setError(err);
}
});
};
useEffect(() => {
getTopics(id);
}, [id]);
}, [id, api]);
return { topics, loading, error };
};
export default useTranscript;
export default useTopics;

View File

@@ -0,0 +1,63 @@
import { useEffect, useState } from "react";
import { GetTranscript } from "../../api";
import { useError } from "../../(errors)/errorContext";
import { shouldShowError } from "../../lib/errorUtils";
import useApi from "../../lib/useApi";
type ErrorTranscript = {
error: Error;
loading: false;
response: any;
};
type LoadingTranscript = {
response: any;
loading: true;
error: false;
};
type SuccessTranscript = {
response: GetTranscript;
loading: false;
error: null;
};
const useTranscript = (
id: string | null,
): ErrorTranscript | LoadingTranscript | SuccessTranscript => {
const [response, setResponse] = useState<GetTranscript | null>(null);
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);
api
.v1TranscriptGet(id)
.then((result) => {
setResponse(result);
setLoading(false);
console.debug("Transcript Loaded:", result);
})
.catch((error) => {
const shouldShowHuman = shouldShowError(error);
if (shouldShowHuman) {
setError(error, "There was an error loading the transcript");
} else {
setError(error);
}
setErrorState(error);
});
}, [id, !api]);
return { response, loading, error } as
| ErrorTranscript
| LoadingTranscript
| SuccessTranscript;
};
export default useTranscript;

View File

@@ -0,0 +1,40 @@
import { useEffect, useState } from "react";
import { useError } from "../../(errors)/errorContext";
import useApi from "../../lib/useApi";
import { Page_GetTranscript_ } from "../../api";
type TranscriptList = {
response: Page_GetTranscript_ | null;
loading: boolean;
error: Error | null;
};
//always protected
const useTranscriptList = (page: number): TranscriptList => {
const [response, setResponse] = useState<Page_GetTranscript_ | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = useApi();
useEffect(() => {
if (!api) return;
setLoading(true);
api
.v1TranscriptsList(page)
.then((response) => {
setResponse(response);
setLoading(false);
})
.catch((err) => {
setResponse(null);
setLoading(false);
setError(err);
setErrorState(err);
});
}, [!api, page]);
return { response, loading, error };
};
export default useTranscriptList;

View File

@@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import { AudioWaveform } from "../../api";
import { useError } from "../../(errors)/errorContext";
import useApi from "../../lib/useApi";
import { shouldShowError } from "../../lib/errorUtils";
type AudioWaveFormResponse = {
waveform: AudioWaveform | null;
loading: boolean;
error: Error | null;
};
const useWaveform = (id: string): AudioWaveFormResponse => {
const [waveform, setWaveform] = useState<AudioWaveform | null>(null);
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);
api
.v1TranscriptGetAudioWaveform(id)
.then((result) => {
setWaveform(result);
setLoading(false);
console.debug("Transcript waveform loaded:", result);
})
.catch((err) => {
setErrorState(err);
const shouldShowHuman = shouldShowError(err);
if (shouldShowHuman) {
setError(err, "There was an error loading the waveform");
} else {
setError(err);
}
});
}, [id, api]);
return { waveform, loading, error };
};
export default useWaveform;

View File

@@ -1,18 +1,16 @@
import { useEffect, useState } from "react";
import Peer from "simple-peer";
import {
DefaultApi,
V1TranscriptRecordWebrtcRequest,
} from "../api/apis/DefaultApi";
import { useError } from "../(errors)/errorContext";
import { useError } from "../../(errors)/errorContext";
import useApi from "../../lib/useApi";
import { RtcOffer } from "../../api";
const useWebRTC = (
stream: MediaStream | null,
transcriptId: string | null,
api: DefaultApi,
): Peer => {
const [peer, setPeer] = useState<Peer | null>(null);
const { setError } = useError();
const api = useApi();
useEffect(() => {
if (!stream || !transcriptId) {
@@ -26,7 +24,7 @@ const useWebRTC = (
try {
p = new Peer({ initiator: true, stream: stream });
} catch (error) {
setError(error);
setError(error, "Error creating WebRTC");
return;
}
@@ -35,17 +33,15 @@ const useWebRTC = (
});
p.on("signal", (data: any) => {
if (!api) return;
if ("sdp" in data) {
const requestParameters: V1TranscriptRecordWebrtcRequest = {
transcriptId: transcriptId,
rtcOffer: {
sdp: data.sdp,
type: data.type,
},
const rtcOffer: RtcOffer = {
sdp: data.sdp,
type: data.type,
};
api
.v1TranscriptRecordWebrtc(requestParameters)
.v1TranscriptRecordWebrtc(transcriptId, rtcOffer)
.then((answer) => {
try {
p.signal(answer);
@@ -54,7 +50,7 @@ const useWebRTC = (
}
})
.catch((error) => {
setError(error);
setError(error, "Error loading WebRTCOffer");
});
}
});

View File

@@ -1,29 +1,39 @@
import { useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { Topic, FinalSummary, Status } from "./webSocketTypes";
import { useError } from "../(errors)/errorContext";
import { useRouter } from "next/navigation";
import { useError } from "../../(errors)/errorContext";
import { DomainContext } from "../domainContext";
import { AudioWaveform, GetTranscriptSegmentTopic } from "../../api";
import useApi from "../../lib/useApi";
type UseWebSockets = {
export type UseWebSockets = {
transcriptText: string;
translateText: string;
title: string;
topics: Topic[];
finalSummary: FinalSummary;
status: Status;
waveform: AudioWaveform | null;
duration: number | null;
};
export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
const [transcriptText, setTranscriptText] = useState<string>("");
const [translateText, setTranslateText] = useState<string>("");
const [title, setTitle] = useState<string>("");
const [textQueue, setTextQueue] = useState<string[]>([]);
const [translationQueue, setTranslationQueue] = useState<string[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [topics, setTopics] = useState<Topic[]>([]);
const [waveform, setWaveForm] = useState<AudioWaveform | null>(null);
const [duration, setDuration] = useState<number | null>(null);
const [finalSummary, setFinalSummary] = useState<FinalSummary>({
summary: "",
});
const [status, setStatus] = useState<Status>({ value: "disconnected" });
const [status, setStatus] = useState<Status>({ value: "initial" });
const { setError } = useError();
const router = useRouter();
const { websocket_url } = useContext(DomainContext);
const api = useApi();
useEffect(() => {
if (isProcessing || textQueue.length === 0) {
@@ -49,11 +59,45 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
useEffect(() => {
document.onkeyup = (e) => {
if (e.key === "a" && process.env.NEXT_PUBLIC_ENV === "development") {
const segments: GetTranscriptSegmentTopic[] = [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
{
speaker: 3,
start: 90,
text: "This is the third speaker",
},
{
speaker: 4,
start: 90,
text: "This is the fourth speaker",
},
{
speaker: 5,
start: 123,
text: "This is the fifth speaker",
},
{
speaker: 6,
start: 300,
text: "This is the sixth speaker",
},
];
setTranscriptText("Lorem Ipsum");
setTopics([
{
id: "1",
timestamp: 10,
duration: 10,
summary: "This is test topic 1",
title: "Topic 1: Introduction to Quantum Mechanics",
transcript:
@@ -62,32 +106,84 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
{
id: "2",
timestamp: 20,
duration: 10,
summary: "This is test topic 2",
title: "Topic 2: Machine Learning Algorithms",
transcript:
"Understanding the different types of machine learning algorithms.",
segments: [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
],
},
{
id: "3",
timestamp: 30,
duration: 10,
summary: "This is test topic 3",
title: "Topic 3: Mental Health Awareness",
transcript: "Ways to improve mental health and reduce stigma.",
segments: [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
],
},
{
id: "4",
timestamp: 40,
duration: 10,
summary: "This is test topic 4",
title: "Topic 4: Basics of Productivity",
transcript: "Tips and tricks to increase daily productivity.",
segments: [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
],
},
{
id: "5",
timestamp: 50,
duration: 10,
summary: "This is test topic 5",
title: "Topic 5: Future of Aviation",
transcript:
"Exploring the advancements and possibilities in aviation.",
segments: [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
],
},
]);
@@ -101,45 +197,110 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
{
id: "1",
timestamp: 10,
duration: 10,
summary: "This is test topic 1",
title:
"Topic 1: Introduction to Quantum Mechanics, a brief overview of quantum mechanics and its principles.",
transcript:
"A brief overview of quantum mechanics and its principles.",
segments: [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
],
},
{
id: "2",
timestamp: 20,
duration: 10,
summary: "This is test topic 2",
title:
"Topic 2: Machine Learning Algorithms, understanding the different types of machine learning algorithms.",
transcript:
"Understanding the different types of machine learning algorithms.",
segments: [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
],
},
{
id: "3",
timestamp: 30,
duration: 10,
summary: "This is test topic 3",
title:
"Topic 3: Mental Health Awareness, ways to improve mental health and reduce stigma.",
transcript: "Ways to improve mental health and reduce stigma.",
segments: [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
],
},
{
id: "4",
timestamp: 40,
duration: 10,
summary: "This is test topic 4",
title:
"Topic 4: Basics of Productivity, tips and tricks to increase daily productivity.",
transcript: "Tips and tricks to increase daily productivity.",
segments: [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
],
},
{
id: "5",
timestamp: 50,
duration: 10,
summary: "This is test topic 5",
title:
"Topic 5: Future of Aviation, exploring the advancements and possibilities in aviation.",
transcript:
"Exploring the advancements and possibilities in aviation.",
segments: [
{
speaker: 1,
start: 0,
text: "This is the transcription of an example title",
},
{
speaker: 2,
start: 10,
text: "This is the second speaker",
},
],
},
]);
@@ -147,10 +308,12 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
}
};
if (!transcriptId) return;
if (!transcriptId || !api) return;
const url = `${process.env.NEXT_PUBLIC_WEBSOCKET_URL}/v1/transcripts/${transcriptId}/events`;
const ws = new WebSocket(url);
api?.v1TranscriptGetWebsocketEvents(transcriptId).then((result) => {});
const url = `${websocket_url}/v1/transcripts/${transcriptId}/events`;
let ws = new WebSocket(url);
ws.onopen = () => {
console.debug("WebSocket connection opened");
@@ -173,7 +336,17 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
break;
case "TOPIC":
setTopics((prevTopics) => [...prevTopics, message.data]);
setTopics((prevTopics) => {
const topic = message.data as Topic;
const index = prevTopics.findIndex(
(prevTopic) => prevTopic.id === topic.id,
);
if (index >= 0) {
prevTopics[index] = topic;
return prevTopics;
}
return [...prevTopics, topic];
});
console.debug("TOPIC event:", message.data);
break;
@@ -189,21 +362,39 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
case "FINAL_TITLE":
console.debug("FINAL_TITLE event:", message.data);
if (message.data) {
setTitle(message.data.title);
}
break;
case "WAVEFORM":
console.debug(
"WAVEFORM event length:",
message.data.waveform.length,
);
if (message.data) {
setWaveForm(message.data.waveform);
}
break;
case "DURATION":
console.debug("DURATION event:", message.data);
if (message.data) {
setDuration(message.data.duration);
}
break;
case "STATUS":
console.log("STATUS event:", message.data);
if (message.data.value === "ended") {
const newUrl = "/transcripts/" + transcriptId;
router.push(newUrl);
console.debug(
"FINAL_LONG_SUMMARY event:",
message.data,
"newUrl",
newUrl,
if (message.data.value === "error") {
setError(
Error("Websocket error status"),
"There was an error processing this meeting.",
);
}
setStatus(message.data);
if (message.data.value === "ended") {
ws.close();
}
break;
default:
@@ -225,20 +416,37 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
console.debug("WebSocket connection closed");
switch (event.code) {
case 1000: // Normal Closure:
case 1001: // Going Away:
case 1005:
break;
case 1005: // Closure by client FF
break;
default:
setError(
new Error(`WebSocket closed unexpectedly with code: ${event.code}`),
"Disconnected",
);
console.log(
"Socket is closed. Reconnect will be attempted in 1 second.",
event.reason,
);
setTimeout(function () {
ws = new WebSocket(url);
}, 1000);
}
};
return () => {
ws.close();
};
}, [transcriptId]);
}, [transcriptId, !api]);
return { transcriptText, translateText, topics, finalSummary, status };
return {
transcriptText,
translateText,
topics,
finalSummary,
title,
status,
waveform,
duration,
};
};

View File

@@ -0,0 +1,11 @@
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default () => (
<div className="flex flex-grow items-center justify-center h-20">
<FontAwesomeIcon
icon={faSpinner}
className="animate-spin-slow text-gray-600 flex-grow rounded-lg md:rounded-xl h-10 w-10"
/>
</div>
);

View File

@@ -1,10 +1,6 @@
export type Topic = {
timestamp: number;
title: string;
transcript: string;
summary: string;
id: string;
};
import { GetTranscriptTopic } from "../../api";
export type Topic = GetTranscriptTopic;
export type Transcript = {
text: string;

View File

@@ -2,15 +2,27 @@ apis/DefaultApi.ts
apis/index.ts
index.ts
models/AudioWaveform.ts
models/CreateParticipant.ts
models/CreateTranscript.ts
models/DeletionStatus.ts
models/GetTranscript.ts
models/GetTranscriptSegmentTopic.ts
models/GetTranscriptTopic.ts
models/GetTranscriptTopicWithWords.ts
models/GetTranscriptTopicWithWordsPerSpeaker.ts
models/HTTPValidationError.ts
models/PageGetTranscript.ts
models/Participant.ts
models/RtcOffer.ts
models/TranscriptTopic.ts
models/SpeakerAssignment.ts
models/SpeakerAssignmentStatus.ts
models/SpeakerMerge.ts
models/SpeakerWords.ts
models/TranscriptParticipant.ts
models/UpdateParticipant.ts
models/UpdateTranscript.ts
models/UserInfo.ts
models/ValidationError.ts
models/Word.ts
models/index.ts
runtime.ts

36
www/app/api/Api.ts Normal file
View File

@@ -0,0 +1,36 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseHttpRequest } from "./core/BaseHttpRequest";
import type { OpenAPIConfig } from "./core/OpenAPI";
import { FetchHttpRequest } from "./core/FetchHttpRequest";
import { DefaultService } from "./services/DefaultService";
type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;
export class Api {
public readonly default: DefaultService;
public readonly request: BaseHttpRequest;
constructor(
config?: Partial<OpenAPIConfig>,
HttpRequest: HttpRequestConstructor = FetchHttpRequest,
) {
this.request = new HttpRequest({
BASE: config?.BASE ?? "",
VERSION: config?.VERSION ?? "0.1.0",
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? "include",
TOKEN: config?.TOKEN,
USERNAME: config?.USERNAME,
PASSWORD: config?.PASSWORD,
HEADERS: config?.HEADERS,
ENCODE_PATH: config?.ENCODE_PATH,
});
this.default = new DefaultService(this.request);
}
}

36
www/app/api/OpenApi.ts Normal file
View File

@@ -0,0 +1,36 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseHttpRequest } from "./core/BaseHttpRequest";
import type { OpenAPIConfig } from "./core/OpenAPI";
import { FetchHttpRequest } from "./core/FetchHttpRequest";
import { DefaultService } from "./services/DefaultService";
type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;
export class OpenApi {
public readonly default: DefaultService;
public readonly request: BaseHttpRequest;
constructor(
config?: Partial<OpenAPIConfig>,
HttpRequest: HttpRequestConstructor = FetchHttpRequest,
) {
this.request = new HttpRequest({
BASE: config?.BASE ?? "",
VERSION: config?.VERSION ?? "0.1.0",
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? "include",
TOKEN: config?.TOKEN,
USERNAME: config?.USERNAME,
PASSWORD: config?.PASSWORD,
HEADERS: config?.HEADERS,
ENCODE_PATH: config?.ENCODE_PATH,
});
this.default = new DefaultService(this.request);
}
}

View File

@@ -1,862 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import * as runtime from "../runtime";
import type {
AudioWaveform,
CreateTranscript,
DeletionStatus,
GetTranscript,
HTTPValidationError,
PageGetTranscript,
RtcOffer,
UpdateTranscript,
} from "../models";
import {
AudioWaveformFromJSON,
AudioWaveformToJSON,
CreateTranscriptFromJSON,
CreateTranscriptToJSON,
DeletionStatusFromJSON,
DeletionStatusToJSON,
GetTranscriptFromJSON,
GetTranscriptToJSON,
HTTPValidationErrorFromJSON,
HTTPValidationErrorToJSON,
PageGetTranscriptFromJSON,
PageGetTranscriptToJSON,
RtcOfferFromJSON,
RtcOfferToJSON,
UpdateTranscriptFromJSON,
UpdateTranscriptToJSON,
} from "../models";
export interface RtcOfferRequest {
rtcOffer: RtcOffer;
}
export interface V1TranscriptDeleteRequest {
transcriptId: any;
}
export interface V1TranscriptGetRequest {
transcriptId: any;
}
export interface V1TranscriptGetAudioMp3Request {
transcriptId: any;
}
export interface V1TranscriptGetAudioWaveformRequest {
transcriptId: any;
}
export interface V1TranscriptGetTopicsRequest {
transcriptId: any;
}
export interface V1TranscriptGetWebsocketEventsRequest {
transcriptId: any;
}
export interface V1TranscriptRecordWebrtcRequest {
transcriptId: any;
rtcOffer: RtcOffer;
}
export interface V1TranscriptUpdateRequest {
transcriptId: any;
updateTranscript: UpdateTranscript;
}
export interface V1TranscriptsCreateRequest {
createTranscript: CreateTranscript;
}
export interface V1TranscriptsListRequest {
page?: any;
size?: any;
}
/**
*
*/
export class DefaultApi extends runtime.BaseAPI {
/**
* Endpoint that serves Prometheus metrics.
* Metrics
*/
async metricsRaw(
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<any>> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
const response = await this.request(
{
path: `/metrics`,
method: "GET",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
if (this.isJsonMime(response.headers.get("content-type"))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Endpoint that serves Prometheus metrics.
* Metrics
*/
async metrics(
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<any> {
const response = await this.metricsRaw(initOverrides);
return await response.value();
}
/**
* Rtc Offer
*/
async rtcOfferRaw(
requestParameters: RtcOfferRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<any>> {
if (
requestParameters.rtcOffer === null ||
requestParameters.rtcOffer === undefined
) {
throw new runtime.RequiredError(
"rtcOffer",
"Required parameter requestParameters.rtcOffer was null or undefined when calling rtcOffer.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters["Content-Type"] = "application/json";
const response = await this.request(
{
path: `/offer`,
method: "POST",
headers: headerParameters,
query: queryParameters,
body: RtcOfferToJSON(requestParameters.rtcOffer),
},
initOverrides,
);
if (this.isJsonMime(response.headers.get("content-type"))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Rtc Offer
*/
async rtcOffer(
requestParameters: RtcOfferRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<any> {
const response = await this.rtcOfferRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Transcript Delete
*/
async v1TranscriptDeleteRaw(
requestParameters: V1TranscriptDeleteRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<DeletionStatus>> {
if (
requestParameters.transcriptId === null ||
requestParameters.transcriptId === undefined
) {
throw new runtime.RequiredError(
"transcriptId",
"Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptDelete.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts/{transcript_id}`.replace(
`{${"transcript_id"}}`,
encodeURIComponent(String(requestParameters.transcriptId)),
),
method: "DELETE",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
return new runtime.JSONApiResponse(response, (jsonValue) =>
DeletionStatusFromJSON(jsonValue),
);
}
/**
* Transcript Delete
*/
async v1TranscriptDelete(
requestParameters: V1TranscriptDeleteRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<DeletionStatus> {
const response = await this.v1TranscriptDeleteRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Transcript Get
*/
async v1TranscriptGetRaw(
requestParameters: V1TranscriptGetRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<GetTranscript>> {
if (
requestParameters.transcriptId === null ||
requestParameters.transcriptId === undefined
) {
throw new runtime.RequiredError(
"transcriptId",
"Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptGet.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts/{transcript_id}`.replace(
`{${"transcript_id"}}`,
encodeURIComponent(String(requestParameters.transcriptId)),
),
method: "GET",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
return new runtime.JSONApiResponse(response, (jsonValue) =>
GetTranscriptFromJSON(jsonValue),
);
}
/**
* Transcript Get
*/
async v1TranscriptGet(
requestParameters: V1TranscriptGetRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<GetTranscript> {
const response = await this.v1TranscriptGetRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Transcript Get Audio Mp3
*/
async v1TranscriptGetAudioMp3Raw(
requestParameters: V1TranscriptGetAudioMp3Request,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<any>> {
if (
requestParameters.transcriptId === null ||
requestParameters.transcriptId === undefined
) {
throw new runtime.RequiredError(
"transcriptId",
"Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptGetAudioMp3.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts/{transcript_id}/audio/mp3`.replace(
`{${"transcript_id"}}`,
encodeURIComponent(String(requestParameters.transcriptId)),
),
method: "GET",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
if (this.isJsonMime(response.headers.get("content-type"))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Transcript Get Audio Mp3
*/
async v1TranscriptGetAudioMp3(
requestParameters: V1TranscriptGetAudioMp3Request,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<any> {
const response = await this.v1TranscriptGetAudioMp3Raw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Transcript Get Audio Waveform
*/
async v1TranscriptGetAudioWaveformRaw(
requestParameters: V1TranscriptGetAudioWaveformRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<AudioWaveform>> {
if (
requestParameters.transcriptId === null ||
requestParameters.transcriptId === undefined
) {
throw new runtime.RequiredError(
"transcriptId",
"Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptGetAudioWaveform.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts/{transcript_id}/audio/waveform`.replace(
`{${"transcript_id"}}`,
encodeURIComponent(String(requestParameters.transcriptId)),
),
method: "GET",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
return new runtime.JSONApiResponse(response, (jsonValue) =>
AudioWaveformFromJSON(jsonValue),
);
}
/**
* Transcript Get Audio Waveform
*/
async v1TranscriptGetAudioWaveform(
requestParameters: V1TranscriptGetAudioWaveformRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<AudioWaveform> {
const response = await this.v1TranscriptGetAudioWaveformRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Transcript Get Topics
*/
async v1TranscriptGetTopicsRaw(
requestParameters: V1TranscriptGetTopicsRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<any>> {
if (
requestParameters.transcriptId === null ||
requestParameters.transcriptId === undefined
) {
throw new runtime.RequiredError(
"transcriptId",
"Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptGetTopics.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts/{transcript_id}/topics`.replace(
`{${"transcript_id"}}`,
encodeURIComponent(String(requestParameters.transcriptId)),
),
method: "GET",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
if (this.isJsonMime(response.headers.get("content-type"))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Transcript Get Topics
*/
async v1TranscriptGetTopics(
requestParameters: V1TranscriptGetTopicsRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<any> {
const response = await this.v1TranscriptGetTopicsRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Transcript Get Websocket Events
*/
async v1TranscriptGetWebsocketEventsRaw(
requestParameters: V1TranscriptGetWebsocketEventsRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<any>> {
if (
requestParameters.transcriptId === null ||
requestParameters.transcriptId === undefined
) {
throw new runtime.RequiredError(
"transcriptId",
"Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptGetWebsocketEvents.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
const response = await this.request(
{
path: `/v1/transcripts/{transcript_id}/events`.replace(
`{${"transcript_id"}}`,
encodeURIComponent(String(requestParameters.transcriptId)),
),
method: "GET",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
if (this.isJsonMime(response.headers.get("content-type"))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Transcript Get Websocket Events
*/
async v1TranscriptGetWebsocketEvents(
requestParameters: V1TranscriptGetWebsocketEventsRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<any> {
const response = await this.v1TranscriptGetWebsocketEventsRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Transcript Record Webrtc
*/
async v1TranscriptRecordWebrtcRaw(
requestParameters: V1TranscriptRecordWebrtcRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<any>> {
if (
requestParameters.transcriptId === null ||
requestParameters.transcriptId === undefined
) {
throw new runtime.RequiredError(
"transcriptId",
"Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptRecordWebrtc.",
);
}
if (
requestParameters.rtcOffer === null ||
requestParameters.rtcOffer === undefined
) {
throw new runtime.RequiredError(
"rtcOffer",
"Required parameter requestParameters.rtcOffer was null or undefined when calling v1TranscriptRecordWebrtc.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters["Content-Type"] = "application/json";
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts/{transcript_id}/record/webrtc`.replace(
`{${"transcript_id"}}`,
encodeURIComponent(String(requestParameters.transcriptId)),
),
method: "POST",
headers: headerParameters,
query: queryParameters,
body: RtcOfferToJSON(requestParameters.rtcOffer),
},
initOverrides,
);
if (this.isJsonMime(response.headers.get("content-type"))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* Transcript Record Webrtc
*/
async v1TranscriptRecordWebrtc(
requestParameters: V1TranscriptRecordWebrtcRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<any> {
const response = await this.v1TranscriptRecordWebrtcRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Transcript Update
*/
async v1TranscriptUpdateRaw(
requestParameters: V1TranscriptUpdateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<GetTranscript>> {
if (
requestParameters.transcriptId === null ||
requestParameters.transcriptId === undefined
) {
throw new runtime.RequiredError(
"transcriptId",
"Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptUpdate.",
);
}
if (
requestParameters.updateTranscript === null ||
requestParameters.updateTranscript === undefined
) {
throw new runtime.RequiredError(
"updateTranscript",
"Required parameter requestParameters.updateTranscript was null or undefined when calling v1TranscriptUpdate.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters["Content-Type"] = "application/json";
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts/{transcript_id}`.replace(
`{${"transcript_id"}}`,
encodeURIComponent(String(requestParameters.transcriptId)),
),
method: "PATCH",
headers: headerParameters,
query: queryParameters,
body: UpdateTranscriptToJSON(requestParameters.updateTranscript),
},
initOverrides,
);
return new runtime.JSONApiResponse(response, (jsonValue) =>
GetTranscriptFromJSON(jsonValue),
);
}
/**
* Transcript Update
*/
async v1TranscriptUpdate(
requestParameters: V1TranscriptUpdateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<GetTranscript> {
const response = await this.v1TranscriptUpdateRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Transcripts Create
*/
async v1TranscriptsCreateRaw(
requestParameters: V1TranscriptsCreateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<GetTranscript>> {
if (
requestParameters.createTranscript === null ||
requestParameters.createTranscript === undefined
) {
throw new runtime.RequiredError(
"createTranscript",
"Required parameter requestParameters.createTranscript was null or undefined when calling v1TranscriptsCreate.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters["Content-Type"] = "application/json";
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts`,
method: "POST",
headers: headerParameters,
query: queryParameters,
body: CreateTranscriptToJSON(requestParameters.createTranscript),
},
initOverrides,
);
return new runtime.JSONApiResponse(response, (jsonValue) =>
GetTranscriptFromJSON(jsonValue),
);
}
/**
* Transcripts Create
*/
async v1TranscriptsCreate(
requestParameters: V1TranscriptsCreateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<GetTranscript> {
const response = await this.v1TranscriptsCreateRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Transcripts List
*/
async v1TranscriptsListRaw(
requestParameters: V1TranscriptsListRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<PageGetTranscript>> {
const queryParameters: any = {};
if (requestParameters.page !== undefined) {
queryParameters["page"] = requestParameters.page;
}
if (requestParameters.size !== undefined) {
queryParameters["size"] = requestParameters.size;
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts`,
method: "GET",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
return new runtime.JSONApiResponse(response, (jsonValue) =>
PageGetTranscriptFromJSON(jsonValue),
);
}
/**
* Transcripts List
*/
async v1TranscriptsList(
requestParameters: V1TranscriptsListRequest = {},
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<PageGetTranscript> {
const response = await this.v1TranscriptsListRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* User Me
*/
async v1UserMeRaw(
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<any>> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/me`,
method: "GET",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
if (this.isJsonMime(response.headers.get("content-type"))) {
return new runtime.JSONApiResponse<any>(response);
} else {
return new runtime.TextApiResponse(response) as any;
}
}
/**
* User Me
*/
async v1UserMe(
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<any> {
const response = await this.v1UserMeRaw(initOverrides);
return await response.value();
}
}

View File

@@ -1,3 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export * from "./DefaultApi";

View File

@@ -0,0 +1,29 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly request: ApiRequestOptions;
constructor(
request: ApiRequestOptions,
response: ApiResult,
message: string,
) {
super(message);
this.name = "ApiError";
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}

View File

@@ -0,0 +1,24 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiRequestOptions = {
readonly method:
| "GET"
| "PUT"
| "POST"
| "DELETE"
| "OPTIONS"
| "HEAD"
| "PATCH";
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiResult = {
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
};

View File

@@ -0,0 +1,13 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { CancelablePromise } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
export abstract class BaseHttpRequest {
constructor(public readonly config: OpenAPIConfig) {}
public abstract request<T>(options: ApiRequestOptions): CancelablePromise<T>;
}

View File

@@ -0,0 +1,130 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = "CancelError";
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel,
) => void,
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
this.#resolve?.(value);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
this.#reject?.(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, "isResolved", {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, "isRejected", {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, "isCancelled", {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null,
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn("Cancellation threw an error", error);
return;
}
}
this.#cancelHandlers.length = 0;
this.#reject?.(new CancelError("Request aborted"));
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
}

View File

@@ -0,0 +1,25 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from "./ApiRequestOptions";
import { BaseHttpRequest } from "./BaseHttpRequest";
import type { CancelablePromise } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
import { request as __request } from "./request";
export class FetchHttpRequest extends BaseHttpRequest {
constructor(config: OpenAPIConfig) {
super(config);
}
/**
* Request method
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
public override request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return __request(this.config, options);
}
}

View File

@@ -0,0 +1,32 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from "./ApiRequestOptions";
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: "include" | "omit" | "same-origin";
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
ENCODE_PATH?: ((path: string) => string) | undefined;
};
export const OpenAPI: OpenAPIConfig = {
BASE: "",
VERSION: "0.1.0",
WITH_CREDENTIALS: false,
CREDENTIALS: "include",
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};

361
www/app/api/core/request.ts Normal file
View File

@@ -0,0 +1,361 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { ApiError } from "./ApiError";
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
import { CancelablePromise } from "./CancelablePromise";
import type { OnCancel } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
export const isDefined = <T>(
value: T | null | undefined,
): value is Exclude<T, null | undefined> => {
return value !== undefined && value !== null;
};
export const isString = (value: any): value is string => {
return typeof value === "string";
};
export const isStringWithValue = (value: any): value is string => {
return isString(value) && value !== "";
};
export const isBlob = (value: any): value is Blob => {
return (
typeof value === "object" &&
typeof value.type === "string" &&
typeof value.stream === "function" &&
typeof value.arrayBuffer === "function" &&
typeof value.constructor === "function" &&
typeof value.constructor.name === "string" &&
/^(Blob|File)$/.test(value.constructor.name) &&
/^(Blob|File)$/.test(value[Symbol.toStringTag])
);
};
export const isFormData = (value: any): value is FormData => {
return value instanceof FormData;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString("base64");
}
};
export const getQueryString = (params: Record<string, any>): string => {
const qs: string[] = [];
const append = (key: string, value: any) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const process = (key: string, value: any) => {
if (isDefined(value)) {
if (Array.isArray(value)) {
value.forEach((v) => {
process(key, v);
});
} else if (typeof value === "object") {
Object.entries(value).forEach(([k, v]) => {
process(`${key}[${k}]`, v);
});
} else {
append(key, value);
}
}
};
Object.entries(params).forEach(([key, value]) => {
process(key, value);
});
if (qs.length > 0) {
return `?${qs.join("&")}`;
}
return "";
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace("{api-version}", config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = `${config.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
};
export const getFormData = (
options: ApiRequestOptions,
): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: any) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([_, value]) => isDefined(value))
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
export const resolve = async <T>(
options: ApiRequestOptions,
resolver?: T | Resolver<T>,
): Promise<T | undefined> => {
if (typeof resolver === "function") {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
): Promise<Headers> => {
const token = await resolve(options, config.TOKEN);
const username = await resolve(options, config.USERNAME);
const password = await resolve(options, config.PASSWORD);
const additionalHeaders = await resolve(options, config.HEADERS);
const headers = Object.entries({
Accept: "application/json",
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce(
(headers, [key, value]) => ({
...headers,
[key]: String(value),
}),
{} as Record<string, string>,
);
if (isStringWithValue(token)) {
headers["Authorization"] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers["Authorization"] = `Basic ${credentials}`;
}
if (options.body) {
if (options.mediaType) {
headers["Content-Type"] = options.mediaType;
} else if (isBlob(options.body)) {
headers["Content-Type"] = options.body.type || "application/octet-stream";
} else if (isString(options.body)) {
headers["Content-Type"] = "text/plain";
} else if (!isFormData(options.body)) {
headers["Content-Type"] = "application/json";
}
}
return new Headers(headers);
};
export const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body !== undefined) {
if (options.mediaType?.includes("/json")) {
return JSON.stringify(options.body);
} else if (
isString(options.body) ||
isBlob(options.body) ||
isFormData(options.body)
) {
return options.body;
} else {
return JSON.stringify(options.body);
}
}
return undefined;
};
export const sendRequest = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel,
): Promise<Response> => {
const controller = new AbortController();
const request: RequestInit = {
headers,
body: body ?? formData,
method: options.method,
signal: controller.signal,
};
if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}
onCancel(() => controller.abort());
return await fetch(url, request);
};
export const getResponseHeader = (
response: Response,
responseHeader?: string,
): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = async (response: Response): Promise<any> => {
if (response.status !== 204) {
try {
const contentType = response.headers.get("Content-Type");
if (contentType) {
const jsonTypes = ["application/json", "application/problem+json"];
const isJSON = jsonTypes.some((type) =>
contentType.toLowerCase().startsWith(type),
);
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
}
return undefined;
};
export const catchErrorCodes = (
options: ApiRequestOptions,
result: ApiResult,
): void => {
const errors: Record<number, string> = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
500: "Internal Server Error",
502: "Bad Gateway",
503: "Service Unavailable",
...options.errors,
};
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? "unknown";
const errorStatusText = result.statusText ?? "unknown";
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(
options,
result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(
config: OpenAPIConfig,
options: ApiRequestOptions,
): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
const response = await sendRequest(
config,
options,
url,
body,
formData,
headers,
onCancel,
);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(
response,
options.responseHeader,
);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};

View File

@@ -1,5 +1,38 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export * from "./runtime";
export * from "./apis";
export * from "./models";
export { OpenApi } from "./OpenApi";
export { ApiError } from "./core/ApiError";
export { BaseHttpRequest } from "./core/BaseHttpRequest";
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
export { OpenAPI } from "./core/OpenAPI";
export type { OpenAPIConfig } from "./core/OpenAPI";
export type { AudioWaveform } from "./models/AudioWaveform";
export type { Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post } from "./models/Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post";
export type { CreateParticipant } from "./models/CreateParticipant";
export type { CreateTranscript } from "./models/CreateTranscript";
export type { DeletionStatus } from "./models/DeletionStatus";
export type { GetTranscript } from "./models/GetTranscript";
export type { GetTranscriptSegmentTopic } from "./models/GetTranscriptSegmentTopic";
export type { GetTranscriptTopic } from "./models/GetTranscriptTopic";
export type { GetTranscriptTopicWithWords } from "./models/GetTranscriptTopicWithWords";
export type { GetTranscriptTopicWithWordsPerSpeaker } from "./models/GetTranscriptTopicWithWordsPerSpeaker";
export type { HTTPValidationError } from "./models/HTTPValidationError";
export type { Page_GetTranscript_ } from "./models/Page_GetTranscript_";
export type { Participant } from "./models/Participant";
export type { RtcOffer } from "./models/RtcOffer";
export type { SpeakerAssignment } from "./models/SpeakerAssignment";
export type { SpeakerAssignmentStatus } from "./models/SpeakerAssignmentStatus";
export type { SpeakerMerge } from "./models/SpeakerMerge";
export type { SpeakerWords } from "./models/SpeakerWords";
export type { TranscriptParticipant } from "./models/TranscriptParticipant";
export type { UpdateParticipant } from "./models/UpdateParticipant";
export type { UpdateTranscript } from "./models/UpdateTranscript";
export type { UserInfo } from "./models/UserInfo";
export type { ValidationError } from "./models/ValidationError";
export type { Word } from "./models/Word";
export { DefaultService } from "./services/DefaultService";

View File

@@ -1,66 +1,8 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface AudioWaveform
*/
export interface AudioWaveform {
/**
*
* @type {any}
* @memberof AudioWaveform
*/
data: any | null;
}
/**
* Check if a given object implements the AudioWaveform interface.
*/
export function instanceOfAudioWaveform(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "data" in value;
return isInstance;
}
export function AudioWaveformFromJSON(json: any): AudioWaveform {
return AudioWaveformFromJSONTyped(json, false);
}
export function AudioWaveformFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): AudioWaveform {
if (json === undefined || json === null) {
return json;
}
return {
data: json["data"],
};
}
export function AudioWaveformToJSON(value?: AudioWaveform | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
data: value.data,
};
}
export type AudioWaveform = {
data: Array<number>;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post =
{
file: Blob;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateParticipant = {
speaker?: number | null;
name: string;
};

View File

@@ -1,86 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface CreateTranscript
*/
export interface CreateTranscript {
/**
*
* @type {any}
* @memberof CreateTranscript
*/
name: any | null;
/**
*
* @type {any}
* @memberof CreateTranscript
*/
sourceLanguage?: any | null;
/**
*
* @type {any}
* @memberof CreateTranscript
*/
targetLanguage?: any | null;
}
/**
* Check if a given object implements the CreateTranscript interface.
*/
export function instanceOfCreateTranscript(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "name" in value;
return isInstance;
}
export function CreateTranscriptFromJSON(json: any): CreateTranscript {
return CreateTranscriptFromJSONTyped(json, false);
}
export function CreateTranscriptFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): CreateTranscript {
if (json === undefined || json === null) {
return json;
}
return {
name: json["name"],
sourceLanguage: !exists(json, "source_language")
? undefined
: json["source_language"],
targetLanguage: !exists(json, "target_language")
? undefined
: json["target_language"],
};
}
export function CreateTranscriptToJSON(value?: CreateTranscript | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
name: value.name,
source_language: value.sourceLanguage,
target_language: value.targetLanguage,
};
}
export type CreateTranscript = {
name: string;
source_language?: string;
target_language?: string;
};

View File

@@ -1,66 +1,8 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface DeletionStatus
*/
export interface DeletionStatus {
/**
*
* @type {any}
* @memberof DeletionStatus
*/
status: any | null;
}
/**
* Check if a given object implements the DeletionStatus interface.
*/
export function instanceOfDeletionStatus(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "status" in value;
return isInstance;
}
export function DeletionStatusFromJSON(json: any): DeletionStatus {
return DeletionStatusFromJSONTyped(json, false);
}
export function DeletionStatusFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): DeletionStatus {
if (json === undefined || json === null) {
return json;
}
return {
status: json["status"],
};
}
export function DeletionStatusToJSON(value?: DeletionStatus | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
status: value.status,
};
}
export type DeletionStatus = {
status: string;
};

View File

@@ -1,156 +1,24 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface GetTranscript
*/
export interface GetTranscript {
/**
*
* @type {any}
* @memberof GetTranscript
*/
id: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
name: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
status: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
locked: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
duration: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
title: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
shortSummary: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
longSummary: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
createdAt: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
sourceLanguage: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
targetLanguage: any | null;
}
import type { TranscriptParticipant } from "./TranscriptParticipant";
/**
* Check if a given object implements the GetTranscript interface.
*/
export function instanceOfGetTranscript(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "id" in value;
isInstance = isInstance && "name" in value;
isInstance = isInstance && "status" in value;
isInstance = isInstance && "locked" in value;
isInstance = isInstance && "duration" in value;
isInstance = isInstance && "title" in value;
isInstance = isInstance && "shortSummary" in value;
isInstance = isInstance && "longSummary" in value;
isInstance = isInstance && "createdAt" in value;
isInstance = isInstance && "sourceLanguage" in value;
isInstance = isInstance && "targetLanguage" in value;
return isInstance;
}
export function GetTranscriptFromJSON(json: any): GetTranscript {
return GetTranscriptFromJSONTyped(json, false);
}
export function GetTranscriptFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): GetTranscript {
if (json === undefined || json === null) {
return json;
}
return {
id: json["id"],
name: json["name"],
status: json["status"],
locked: json["locked"],
duration: json["duration"],
title: json["title"],
shortSummary: json["short_summary"],
longSummary: json["long_summary"],
createdAt: json["created_at"],
sourceLanguage: json["source_language"],
targetLanguage: json["target_language"],
};
}
export function GetTranscriptToJSON(value?: GetTranscript | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
id: value.id,
name: value.name,
status: value.status,
locked: value.locked,
duration: value.duration,
title: value.title,
short_summary: value.shortSummary,
long_summary: value.longSummary,
created_at: value.createdAt,
source_language: value.sourceLanguage,
target_language: value.targetLanguage,
};
}
export type GetTranscript = {
id: string;
user_id: string | null;
name: string;
status: string;
locked: boolean;
duration: number;
title: string | null;
short_summary: string | null;
long_summary: string | null;
created_at: string;
share_mode?: string;
source_language: string | null;
target_language: string | null;
participants: Array<TranscriptParticipant> | null;
reviewed: boolean;
};

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GetTranscriptSegmentTopic = {
text: string;
start: number;
speaker: number;
};

View File

@@ -0,0 +1,16 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GetTranscriptSegmentTopic } from "./GetTranscriptSegmentTopic";
export type GetTranscriptTopic = {
id: string;
title: string;
summary: string;
timestamp: number;
duration: number | null;
transcript: string;
segments?: Array<GetTranscriptSegmentTopic>;
};

View File

@@ -0,0 +1,18 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GetTranscriptSegmentTopic } from "./GetTranscriptSegmentTopic";
import type { Word } from "./Word";
export type GetTranscriptTopicWithWords = {
id: string;
title: string;
summary: string;
timestamp: number;
duration: number | null;
transcript: string;
segments?: Array<GetTranscriptSegmentTopic>;
words?: Array<Word>;
};

View File

@@ -0,0 +1,18 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GetTranscriptSegmentTopic } from "./GetTranscriptSegmentTopic";
import type { SpeakerWords } from "./SpeakerWords";
export type GetTranscriptTopicWithWordsPerSpeaker = {
id: string;
title: string;
summary: string;
timestamp: number;
duration: number | null;
transcript: string;
segments?: Array<GetTranscriptSegmentTopic>;
words_per_speaker?: Array<SpeakerWords>;
};

View File

@@ -1,67 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface HTTPValidationError
*/
export interface HTTPValidationError {
/**
*
* @type {any}
* @memberof HTTPValidationError
*/
detail?: any | null;
}
import type { ValidationError } from "./ValidationError";
/**
* Check if a given object implements the HTTPValidationError interface.
*/
export function instanceOfHTTPValidationError(value: object): boolean {
let isInstance = true;
return isInstance;
}
export function HTTPValidationErrorFromJSON(json: any): HTTPValidationError {
return HTTPValidationErrorFromJSONTyped(json, false);
}
export function HTTPValidationErrorFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): HTTPValidationError {
if (json === undefined || json === null) {
return json;
}
return {
detail: !exists(json, "detail") ? undefined : json["detail"],
};
}
export function HTTPValidationErrorToJSON(
value?: HTTPValidationError | null,
): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
detail: value.detail,
};
}
export type HTTPValidationError = {
detail?: Array<ValidationError>;
};

View File

@@ -1,101 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface PageGetTranscript
*/
export interface PageGetTranscript {
/**
*
* @type {any}
* @memberof PageGetTranscript
*/
items: any | null;
/**
*
* @type {any}
* @memberof PageGetTranscript
*/
total: any | null;
/**
*
* @type {any}
* @memberof PageGetTranscript
*/
page: any | null;
/**
*
* @type {any}
* @memberof PageGetTranscript
*/
size: any | null;
/**
*
* @type {any}
* @memberof PageGetTranscript
*/
pages?: any | null;
}
/**
* Check if a given object implements the PageGetTranscript interface.
*/
export function instanceOfPageGetTranscript(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "items" in value;
isInstance = isInstance && "total" in value;
isInstance = isInstance && "page" in value;
isInstance = isInstance && "size" in value;
return isInstance;
}
export function PageGetTranscriptFromJSON(json: any): PageGetTranscript {
return PageGetTranscriptFromJSONTyped(json, false);
}
export function PageGetTranscriptFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): PageGetTranscript {
if (json === undefined || json === null) {
return json;
}
return {
items: json["items"],
total: json["total"],
page: json["page"],
size: json["size"],
pages: !exists(json, "pages") ? undefined : json["pages"],
};
}
export function PageGetTranscriptToJSON(value?: PageGetTranscript | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
items: value.items,
total: value.total,
page: value.page,
size: value.size,
pages: value.pages,
};
}

View File

@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GetTranscript } from "./GetTranscript";
export type Page_GetTranscript_ = {
items: Array<GetTranscript>;
total: number;
page: number | null;
size: number | null;
pages?: number | null;
};

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Participant = {
id: string;
speaker: number | null;
name: string;
};

View File

@@ -1,75 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface RtcOffer
*/
export interface RtcOffer {
/**
*
* @type {any}
* @memberof RtcOffer
*/
sdp: any | null;
/**
*
* @type {any}
* @memberof RtcOffer
*/
type: any | null;
}
/**
* Check if a given object implements the RtcOffer interface.
*/
export function instanceOfRtcOffer(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "sdp" in value;
isInstance = isInstance && "type" in value;
return isInstance;
}
export function RtcOfferFromJSON(json: any): RtcOffer {
return RtcOfferFromJSONTyped(json, false);
}
export function RtcOfferFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): RtcOffer {
if (json === undefined || json === null) {
return json;
}
return {
sdp: json["sdp"],
type: json["type"],
};
}
export function RtcOfferToJSON(value?: RtcOffer | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
sdp: value.sdp,
type: value.type,
};
}
export type RtcOffer = {
sdp: string;
type: string;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type SpeakerAssignment = {
speaker?: number | null;
participant?: string | null;
timestamp_from: number;
timestamp_to: number;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type SpeakerAssignmentStatus = {
status: string;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type SpeakerMerge = {
speaker_from: number;
speaker_to: number;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Word } from "./Word";
export type SpeakerWords = {
speaker: number;
words: Array<Word>;
};

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TranscriptParticipant = {
id?: string;
speaker: number | null;
name: string;
};

View File

@@ -1,101 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface TranscriptTopic
*/
export interface TranscriptTopic {
/**
*
* @type {any}
* @memberof TranscriptTopic
*/
id?: any | null;
/**
*
* @type {any}
* @memberof TranscriptTopic
*/
title: any | null;
/**
*
* @type {any}
* @memberof TranscriptTopic
*/
summary: any | null;
/**
*
* @type {any}
* @memberof TranscriptTopic
*/
transcript: any | null;
/**
*
* @type {any}
* @memberof TranscriptTopic
*/
timestamp: any | null;
}
/**
* Check if a given object implements the TranscriptTopic interface.
*/
export function instanceOfTranscriptTopic(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "title" in value;
isInstance = isInstance && "summary" in value;
isInstance = isInstance && "transcript" in value;
isInstance = isInstance && "timestamp" in value;
return isInstance;
}
export function TranscriptTopicFromJSON(json: any): TranscriptTopic {
return TranscriptTopicFromJSONTyped(json, false);
}
export function TranscriptTopicFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): TranscriptTopic {
if (json === undefined || json === null) {
return json;
}
return {
id: !exists(json, "id") ? undefined : json["id"],
title: json["title"],
summary: json["summary"],
transcript: json["transcript"],
timestamp: json["timestamp"],
};
}
export function TranscriptTopicToJSON(value?: TranscriptTopic | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
id: value.id,
title: value.title,
summary: value.summary,
transcript: value.transcript,
timestamp: value.timestamp,
};
}

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateParticipant = {
speaker?: number | null;
name?: string | null;
};

View File

@@ -1,101 +1,17 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface UpdateTranscript
*/
export interface UpdateTranscript {
/**
*
* @type {any}
* @memberof UpdateTranscript
*/
name?: any | null;
/**
*
* @type {any}
* @memberof UpdateTranscript
*/
locked?: any | null;
/**
*
* @type {any}
* @memberof UpdateTranscript
*/
title?: any | null;
/**
*
* @type {any}
* @memberof UpdateTranscript
*/
shortSummary?: any | null;
/**
*
* @type {any}
* @memberof UpdateTranscript
*/
longSummary?: any | null;
}
import type { TranscriptParticipant } from "./TranscriptParticipant";
/**
* Check if a given object implements the UpdateTranscript interface.
*/
export function instanceOfUpdateTranscript(value: object): boolean {
let isInstance = true;
return isInstance;
}
export function UpdateTranscriptFromJSON(json: any): UpdateTranscript {
return UpdateTranscriptFromJSONTyped(json, false);
}
export function UpdateTranscriptFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): UpdateTranscript {
if (json === undefined || json === null) {
return json;
}
return {
name: !exists(json, "name") ? undefined : json["name"],
locked: !exists(json, "locked") ? undefined : json["locked"],
title: !exists(json, "title") ? undefined : json["title"],
shortSummary: !exists(json, "short_summary")
? undefined
: json["short_summary"],
longSummary: !exists(json, "long_summary")
? undefined
: json["long_summary"],
};
}
export function UpdateTranscriptToJSON(value?: UpdateTranscript | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
name: value.name,
locked: value.locked,
title: value.title,
short_summary: value.shortSummary,
long_summary: value.longSummary,
};
}
export type UpdateTranscript = {
name?: string | null;
locked?: boolean | null;
title?: string | null;
short_summary?: string | null;
long_summary?: string | null;
share_mode?: "public" | "semi-private" | "private" | null;
participants?: Array<TranscriptParticipant> | null;
reviewed?: boolean | null;
};

View File

@@ -1,84 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface UserInfo
*/
export interface UserInfo {
/**
*
* @type {any}
* @memberof UserInfo
*/
sub: any | null;
/**
*
* @type {any}
* @memberof UserInfo
*/
email: any | null;
/**
*
* @type {any}
* @memberof UserInfo
*/
emailVerified: any | null;
}
/**
* Check if a given object implements the UserInfo interface.
*/
export function instanceOfUserInfo(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "sub" in value;
isInstance = isInstance && "email" in value;
isInstance = isInstance && "emailVerified" in value;
return isInstance;
}
export function UserInfoFromJSON(json: any): UserInfo {
return UserInfoFromJSONTyped(json, false);
}
export function UserInfoFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): UserInfo {
if (json === undefined || json === null) {
return json;
}
return {
sub: json["sub"],
email: json["email"],
emailVerified: json["email_verified"],
};
}
export function UserInfoToJSON(value?: UserInfo | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
sub: value.sub,
email: value.email,
email_verified: value.emailVerified,
};
}
export type UserInfo = {
sub: string;
email: string | null;
email_verified: boolean | null;
};

View File

@@ -1,84 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface ValidationError
*/
export interface ValidationError {
/**
*
* @type {any}
* @memberof ValidationError
*/
loc: any | null;
/**
*
* @type {any}
* @memberof ValidationError
*/
msg: any | null;
/**
*
* @type {any}
* @memberof ValidationError
*/
type: any | null;
}
/**
* Check if a given object implements the ValidationError interface.
*/
export function instanceOfValidationError(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "loc" in value;
isInstance = isInstance && "msg" in value;
isInstance = isInstance && "type" in value;
return isInstance;
}
export function ValidationErrorFromJSON(json: any): ValidationError {
return ValidationErrorFromJSONTyped(json, false);
}
export function ValidationErrorFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): ValidationError {
if (json === undefined || json === null) {
return json;
}
return {
loc: json["loc"],
msg: json["msg"],
type: json["type"],
};
}
export function ValidationErrorToJSON(value?: ValidationError | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
loc: value.loc,
msg: value.msg,
type: value.type,
};
}
export type ValidationError = {
loc: Array<string | number>;
msg: string;
type: string;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Word = {
text: string;
start: number;
end: number;
speaker?: number;
};

View File

@@ -1,13 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export * from "./AudioWaveform";
export * from "./CreateTranscript";
export * from "./DeletionStatus";
export * from "./GetTranscript";
export * from "./HTTPValidationError";
export * from "./PageGetTranscript";
export * from "./RtcOffer";
export * from "./TranscriptTopic";
export * from "./UpdateTranscript";
export * from "./UserInfo";
export * from "./ValidationError";

View File

@@ -0,0 +1,547 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { AudioWaveform } from "../models/AudioWaveform";
import type { Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post } from "../models/Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post";
import type { CreateParticipant } from "../models/CreateParticipant";
import type { CreateTranscript } from "../models/CreateTranscript";
import type { DeletionStatus } from "../models/DeletionStatus";
import type { GetTranscript } from "../models/GetTranscript";
import type { GetTranscriptTopic } from "../models/GetTranscriptTopic";
import type { GetTranscriptTopicWithWords } from "../models/GetTranscriptTopicWithWords";
import type { GetTranscriptTopicWithWordsPerSpeaker } from "../models/GetTranscriptTopicWithWordsPerSpeaker";
import type { Page_GetTranscript_ } from "../models/Page_GetTranscript_";
import type { Participant } from "../models/Participant";
import type { RtcOffer } from "../models/RtcOffer";
import type { SpeakerAssignment } from "../models/SpeakerAssignment";
import type { SpeakerAssignmentStatus } from "../models/SpeakerAssignmentStatus";
import type { SpeakerMerge } from "../models/SpeakerMerge";
import type { UpdateParticipant } from "../models/UpdateParticipant";
import type { UpdateTranscript } from "../models/UpdateTranscript";
import type { UserInfo } from "../models/UserInfo";
import type { CancelablePromise } from "../core/CancelablePromise";
import type { BaseHttpRequest } from "../core/BaseHttpRequest";
export class DefaultService {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
* Metrics
* Endpoint that serves Prometheus metrics.
* @returns any Successful Response
* @throws ApiError
*/
public metrics(): CancelablePromise<any> {
return this.httpRequest.request({
method: "GET",
url: "/metrics",
});
}
/**
* Transcripts List
* @param page Page number
* @param size Page size
* @returns Page_GetTranscript_ Successful Response
* @throws ApiError
*/
public v1TranscriptsList(
page: number = 1,
size: number = 50,
): CancelablePromise<Page_GetTranscript_> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts",
query: {
page: page,
size: size,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcripts Create
* @param requestBody
* @returns GetTranscript Successful Response
* @throws ApiError
*/
public v1TranscriptsCreate(
requestBody: CreateTranscript,
): CancelablePromise<GetTranscript> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts",
body: requestBody,
mediaType: "application/json",
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get
* @param transcriptId
* @returns GetTranscript Successful Response
* @throws ApiError
*/
public v1TranscriptGet(
transcriptId: string,
): CancelablePromise<GetTranscript> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}",
path: {
transcript_id: transcriptId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Update
* @param transcriptId
* @param requestBody
* @returns GetTranscript Successful Response
* @throws ApiError
*/
public v1TranscriptUpdate(
transcriptId: string,
requestBody: UpdateTranscript,
): CancelablePromise<GetTranscript> {
return this.httpRequest.request({
method: "PATCH",
url: "/v1/transcripts/{transcript_id}",
path: {
transcript_id: transcriptId,
},
body: requestBody,
mediaType: "application/json",
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Delete
* @param transcriptId
* @returns DeletionStatus Successful Response
* @throws ApiError
*/
public v1TranscriptDelete(
transcriptId: string,
): CancelablePromise<DeletionStatus> {
return this.httpRequest.request({
method: "DELETE",
url: "/v1/transcripts/{transcript_id}",
path: {
transcript_id: transcriptId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get Topics
* @param transcriptId
* @returns GetTranscriptTopic Successful Response
* @throws ApiError
*/
public v1TranscriptGetTopics(
transcriptId: string,
): CancelablePromise<Array<GetTranscriptTopic>> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/topics",
path: {
transcript_id: transcriptId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get Topics With Words
* @param transcriptId
* @returns GetTranscriptTopicWithWords Successful Response
* @throws ApiError
*/
public v1TranscriptGetTopicsWithWords(
transcriptId: string,
): CancelablePromise<Array<GetTranscriptTopicWithWords>> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/topics/with-words",
path: {
transcript_id: transcriptId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get Topics With Words Per Speaker
* @param transcriptId
* @param topicId
* @returns GetTranscriptTopicWithWordsPerSpeaker Successful Response
* @throws ApiError
*/
public v1TranscriptGetTopicsWithWordsPerSpeaker(
transcriptId: string,
topicId: string,
): CancelablePromise<GetTranscriptTopicWithWordsPerSpeaker> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker",
path: {
transcript_id: transcriptId,
topic_id: topicId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get Audio Mp3
* @param transcriptId
* @param token
* @returns any Successful Response
* @throws ApiError
*/
public v1TranscriptHeadAudioMp3(
transcriptId: string,
token?: string | null,
): CancelablePromise<any> {
return this.httpRequest.request({
method: "HEAD",
url: "/v1/transcripts/{transcript_id}/audio/mp3",
path: {
transcript_id: transcriptId,
},
query: {
token: token,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get Audio Mp3
* @param transcriptId
* @param token
* @returns any Successful Response
* @throws ApiError
*/
public v1TranscriptGetAudioMp3(
transcriptId: string,
token?: string | null,
): CancelablePromise<any> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/audio/mp3",
path: {
transcript_id: transcriptId,
},
query: {
token: token,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get Audio Waveform
* @param transcriptId
* @returns AudioWaveform Successful Response
* @throws ApiError
*/
public v1TranscriptGetAudioWaveform(
transcriptId: string,
): CancelablePromise<AudioWaveform> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/audio/waveform",
path: {
transcript_id: transcriptId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get Participants
* @param transcriptId
* @returns Participant Successful Response
* @throws ApiError
*/
public v1TranscriptGetParticipants(
transcriptId: string,
): CancelablePromise<Array<Participant>> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/participants",
path: {
transcript_id: transcriptId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Add Participant
* @param transcriptId
* @param requestBody
* @returns Participant Successful Response
* @throws ApiError
*/
public v1TranscriptAddParticipant(
transcriptId: string,
requestBody: CreateParticipant,
): CancelablePromise<Participant> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts/{transcript_id}/participants",
path: {
transcript_id: transcriptId,
},
body: requestBody,
mediaType: "application/json",
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get Participant
* @param transcriptId
* @param participantId
* @returns Participant Successful Response
* @throws ApiError
*/
public v1TranscriptGetParticipant(
transcriptId: string,
participantId: string,
): CancelablePromise<Participant> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/participants/{participant_id}",
path: {
transcript_id: transcriptId,
participant_id: participantId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Update Participant
* @param transcriptId
* @param participantId
* @param requestBody
* @returns Participant Successful Response
* @throws ApiError
*/
public v1TranscriptUpdateParticipant(
transcriptId: string,
participantId: string,
requestBody: UpdateParticipant,
): CancelablePromise<Participant> {
return this.httpRequest.request({
method: "PATCH",
url: "/v1/transcripts/{transcript_id}/participants/{participant_id}",
path: {
transcript_id: transcriptId,
participant_id: participantId,
},
body: requestBody,
mediaType: "application/json",
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Delete Participant
* @param transcriptId
* @param participantId
* @returns DeletionStatus Successful Response
* @throws ApiError
*/
public v1TranscriptDeleteParticipant(
transcriptId: string,
participantId: string,
): CancelablePromise<DeletionStatus> {
return this.httpRequest.request({
method: "DELETE",
url: "/v1/transcripts/{transcript_id}/participants/{participant_id}",
path: {
transcript_id: transcriptId,
participant_id: participantId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Assign Speaker
* @param transcriptId
* @param requestBody
* @returns SpeakerAssignmentStatus Successful Response
* @throws ApiError
*/
public v1TranscriptAssignSpeaker(
transcriptId: string,
requestBody: SpeakerAssignment,
): CancelablePromise<SpeakerAssignmentStatus> {
return this.httpRequest.request({
method: "PATCH",
url: "/v1/transcripts/{transcript_id}/speaker/assign",
path: {
transcript_id: transcriptId,
},
body: requestBody,
mediaType: "application/json",
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Merge Speaker
* @param transcriptId
* @param requestBody
* @returns SpeakerAssignmentStatus Successful Response
* @throws ApiError
*/
public v1TranscriptMergeSpeaker(
transcriptId: string,
requestBody: SpeakerMerge,
): CancelablePromise<SpeakerAssignmentStatus> {
return this.httpRequest.request({
method: "PATCH",
url: "/v1/transcripts/{transcript_id}/speaker/merge",
path: {
transcript_id: transcriptId,
},
body: requestBody,
mediaType: "application/json",
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Record Upload
* @param transcriptId
* @param formData
* @returns any Successful Response
* @throws ApiError
*/
public v1TranscriptRecordUpload(
transcriptId: string,
formData: Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post,
): CancelablePromise<any> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts/{transcript_id}/record/upload",
path: {
transcript_id: transcriptId,
},
formData: formData,
mediaType: "multipart/form-data",
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Get Websocket Events
* @param transcriptId
* @returns any Successful Response
* @throws ApiError
*/
public v1TranscriptGetWebsocketEvents(
transcriptId: string,
): CancelablePromise<any> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/events",
path: {
transcript_id: transcriptId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Transcript Record Webrtc
* @param transcriptId
* @param requestBody
* @returns any Successful Response
* @throws ApiError
*/
public v1TranscriptRecordWebrtc(
transcriptId: string,
requestBody: RtcOffer,
): CancelablePromise<any> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts/{transcript_id}/record/webrtc",
path: {
transcript_id: transcriptId,
},
body: requestBody,
mediaType: "application/json",
errors: {
422: `Validation Error`,
},
});
}
/**
* User Me
* @returns any Successful Response
* @throws ApiError
*/
public v1UserMe(): CancelablePromise<UserInfo | null> {
return this.httpRequest.request({
method: "GET",
url: "/v1/me",
});
}
}

View File

@@ -1,109 +0,0 @@
import "./styles/globals.scss";
import { Poppins } from "next/font/google";
import { Metadata } from "next";
import FiefWrapper from "./(auth)/fiefWrapper";
import UserInfo from "./(auth)/userInfo";
import { ErrorProvider } from "./(errors)/errorContext";
import ErrorMessage from "./(errors)/errorMessage";
import Image from "next/image";
import Link from "next/link";
import About from "./(aboutAndPrivacy)/about";
import Privacy from "./(aboutAndPrivacy)/privacy";
const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] });
export const metadata: Metadata = {
title: {
template: "%s Reflector",
default: "Reflector - AI-Powered Meeting Transcriptions by Monadical",
},
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise",
applicationName: "Reflector",
referrer: "origin-when-cross-origin",
keywords: ["Reflector", "Monadical", "AI", "Meetings", "Transcription"],
authors: [{ name: "Monadical Team", url: "https://monadical.com/team.html" }],
formatDetection: {
email: false,
address: false,
telephone: false,
},
openGraph: {
title: "Reflector",
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise.",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Reflector",
description:
"Reflector is an AI-powered tool that transcribes your meetings with unparalleled accuracy, divides content by topics, and provides insightful summaries. Maximize your productivity with Reflector, brought to you by Monadical. Capture the signal, not the noise.",
images: ["/r-icon.png"],
},
icons: {
icon: "/r-icon.png",
shortcut: "/r-icon.png",
apple: "/r-icon.png",
},
viewport: {
width: "device-width",
initialScale: 1,
maximumScale: 1,
},
robots: { index: false, follow: false, noarchive: true, noimageindex: true },
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={poppins.className + " h-screen relative"}>
<FiefWrapper>
<ErrorProvider>
<ErrorMessage />
<div
id="container"
className="items-center h-[100svh] w-[100svw] p-2 md:p-4 grid grid-rows-layout gap-2 md:gap-4"
>
<header className="flex justify-between items-center w-full">
{/* Logo on the left */}
<Link
href="/"
className="flex outline-blue-300 md:outline-none focus-visible:underline underline-offset-2 decoration-[.5px] decoration-gray-500"
>
<Image
src="/reach.png"
width={16}
height={16}
className="h-10 w-auto"
alt="Reflector"
/>
<div className="hidden flex-col ml-2 md:block">
<h1 className="text-[38px] font-bold tracking-wide leading-tight">
Reflector
</h1>
<p className="text-gray-500 text-xs tracking-tighter">
Capture the signal, not the noise
</p>
</div>
</Link>
<div>
{/* Text link on the right */}
<About buttonText="About" />
&nbsp;·&nbsp;
<Privacy buttonText="Privacy" />
</div>
</header>
{children}
</div>
</ErrorProvider>
</FiefWrapper>
</body>
</html>
);
}

47
www/app/lib/edgeConfig.ts Normal file
View File

@@ -0,0 +1,47 @@
import { get } from "@vercel/edge-config";
import { isDevelopment } from "./utils";
type EdgeConfig = {
[domainWithDash: string]: {
features: {
[featureName in
| "requireLogin"
| "privacy"
| "browse"
| "sendToZulip"]: boolean;
};
auth_callback_url: string;
websocket_url: string;
api_url: string;
zulip_streams: string;
};
};
export type DomainConfig = EdgeConfig["domainWithDash"];
// Edge config main keys can only be alphanumeric and _ or -
export function edgeKeyToDomain(key: string) {
return key.replaceAll("_", ".");
}
export function edgeDomainToKey(domain: string) {
return domain.replaceAll(".", "_");
}
// get edge config server-side (prefer DomainContext when available), domain is the hostname
export async function getConfig(domain: string) {
if (process.env.NEXT_PUBLIC_ENV === "development") {
return require("../../config").localConfig;
}
let config = await get(edgeDomainToKey(domain));
if (typeof config !== "object") {
console.warn("No config for this domain, falling back to default");
config = await get(edgeDomainToKey("default"));
}
if (typeof config !== "object") throw Error("Error fetching config");
return config as DomainConfig;
}

11
www/app/lib/errorUtils.ts Normal file
View File

@@ -0,0 +1,11 @@
function shouldShowError(error: Error | null | undefined) {
if (
error?.name == "ResponseError" &&
(error["response"].status == 404 || error["response"].status == 403)
)
return false;
if (error?.name == "FetchError") return false;
return true;
}
export { shouldShowError };

View File

@@ -1,6 +1,6 @@
"use client";
import { Fief, FiefUserInfo } from "@fief/fief";
import { FiefAuth, IUserInfoCache } from "@fief/fief/nextjs";
import { getConfig } from "./edgeConfig";
export const SESSION_COOKIE_NAME = "reflector-auth";
@@ -38,13 +38,38 @@ class MemoryUserInfoCache implements IUserInfoCache {
}
}
export const fiefAuth = new FiefAuth({
client: fiefClient,
sessionCookieName: SESSION_COOKIE_NAME,
redirectURI:
process.env.NEXT_PUBLIC_AUTH_CALLBACK_URL ||
"http://localhost:3000/auth-callback",
logoutRedirectURI:
process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000",
userInfoCache: new MemoryUserInfoCache(),
});
const FIEF_AUTHS = {} as { [domain: string]: FiefAuth };
export const getFiefAuth = async (url: URL) => {
if (FIEF_AUTHS[url.hostname]) {
return FIEF_AUTHS[url.hostname];
} else {
const config = url && (await getConfig(url.hostname));
if (config) {
FIEF_AUTHS[url.hostname] = new FiefAuth({
client: fiefClient,
sessionCookieName: SESSION_COOKIE_NAME,
redirectURI: config.auth_callback_url,
logoutRedirectURI: url.origin,
userInfoCache: new MemoryUserInfoCache(),
});
return FIEF_AUTHS[url.hostname];
} else {
throw new Error("Fief intanciation failed");
}
}
};
export const getFiefAuthMiddleware = async (url) => {
const protectedPaths = [
{
matcher: "/transcripts",
parameters: {},
},
{
matcher: "/browse",
parameters: {},
},
];
return (await getFiefAuth(url))?.middleware(protectedPaths);
};

View File

@@ -1,18 +0,0 @@
import { Configuration } from "../api/runtime";
import { DefaultApi } from "../api/apis/DefaultApi";
import { useFiefAccessTokenInfo } from "@fief/fief/nextjs/react";
export default function getApi(): DefaultApi {
const accessTokenInfo = useFiefAccessTokenInfo();
const apiConfiguration = new Configuration({
basePath: process.env.NEXT_PUBLIC_API_URL,
accessToken: accessTokenInfo
? "Bearer " + accessTokenInfo.access_token
: undefined,
});
const api = new DefaultApi(apiConfiguration);
return api;
}

7
www/app/lib/shareMode.ts Normal file
View File

@@ -0,0 +1,7 @@
export type ShareMode = "public" | "semi-private" | "private" | null;
export function toShareMode(value: string | undefined | null): ShareMode {
return value === "public" || value === "semi-private" || value === "private"
? value
: null;
}

32
www/app/lib/useApi.ts Normal file
View File

@@ -0,0 +1,32 @@
import { useFiefAccessTokenInfo } from "@fief/fief/nextjs/react";
import { useContext, useEffect, useState } from "react";
import { DomainContext, featureEnabled } from "../[domain]/domainContext";
import { CookieContext } from "../(auth)/fiefWrapper";
import { OpenApi, DefaultService } from "../api";
export default function useApi(): DefaultService | null {
const accessTokenInfo = useFiefAccessTokenInfo();
const api_url = useContext(DomainContext).api_url;
const requireLogin = featureEnabled("requireLogin");
const [api, setApi] = useState<OpenApi | null>(null);
const { hasAuthCookie } = useContext(CookieContext);
if (!api_url) throw new Error("no API URL");
useEffect(() => {
if (hasAuthCookie && requireLogin && !accessTokenInfo) {
return;
}
if (!accessTokenInfo) return;
const openApi = new OpenApi({
BASE: api_url,
TOKEN: accessTokenInfo?.access_token,
});
setApi(openApi);
}, [!accessTokenInfo, hasAuthCookie]);
return api?.default ?? null;
}

View File

@@ -1,3 +1,138 @@
export function isDevelopment() {
return process.env.NEXT_PUBLIC_ENV === "development";
}
// Function to calculate WCAG contrast ratio
export const getContrastRatio = (
foreground: [number, number, number],
background: [number, number, number],
) => {
const [r1, g1, b1] = foreground;
const [r2, g2, b2] = background;
const lum1 =
0.2126 * Math.pow(r1 / 255, 2.2) +
0.7152 * Math.pow(g1 / 255, 2.2) +
0.0722 * Math.pow(b1 / 255, 2.2);
const lum2 =
0.2126 * Math.pow(r2 / 255, 2.2) +
0.7152 * Math.pow(g2 / 255, 2.2) +
0.0722 * Math.pow(b2 / 255, 2.2);
return (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05);
};
// Function to hash string into 32-bit integer
// 🔴 DO NOT USE FOR CRYPTOGRAPHY PURPOSES 🔴
export function murmurhash3_32_gc(key: string, seed: number = 0) {
let remainder, bytes, h1, h1b, c1, c2, k1, i;
remainder = key.length & 3; // key.length % 4
bytes = key.length - remainder;
h1 = seed;
c1 = 0xcc9e2d51;
c2 = 0x1b873593;
i = 0;
while (i < bytes) {
k1 =
(key.charCodeAt(i) & 0xff) |
((key.charCodeAt(++i) & 0xff) << 8) |
((key.charCodeAt(++i) & 0xff) << 16) |
((key.charCodeAt(++i) & 0xff) << 24);
++i;
k1 =
((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 =
((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
h1 ^= k1;
h1 = (h1 << 13) | (h1 >>> 19);
h1b =
((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
}
k1 = 0;
switch (remainder) {
case 3:
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
case 2:
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
case 1:
k1 ^= key.charCodeAt(i) & 0xff;
k1 =
((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 =
((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
0xffffffff;
h1 ^= k1;
}
h1 ^= key.length;
h1 ^= h1 >>> 16;
h1 =
((h1 & 0xffff) * 0x85ebca6b +
((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
0xffffffff;
h1 ^= h1 >>> 13;
h1 =
((h1 & 0xffff) * 0xc2b2ae35 +
((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
0xffffffff;
h1 ^= h1 >>> 16;
return h1 >>> 0;
}
// Generates a color that is guaranteed to have high contrast with the given background color (optional)
export const generateHighContrastColor = (
name: string,
backgroundColor: [number, number, number],
) => {
let loopNumber = 0;
let minAcceptedContrast = 3.5;
while (true && /* Just as a safeguard */ loopNumber < 100) {
++loopNumber;
if (loopNumber > 5) minAcceptedContrast -= 0.5;
const hash = murmurhash3_32_gc(name + loopNumber);
let red = (hash & 0xff0000) >> 16;
let green = (hash & 0x00ff00) >> 8;
let blue = hash & 0x0000ff;
let contrast = getContrastRatio([red, green, blue], backgroundColor);
if (contrast > minAcceptedContrast) return `rgb(${red}, ${green}, ${blue})`;
// Try to invert the color to increase contrat - this works best the more away the color is from gray
red = Math.abs(255 - red);
green = Math.abs(255 - green);
blue = Math.abs(255 - blue);
contrast = getContrastRatio([red, green, blue], backgroundColor);
if (contrast > minAcceptedContrast) return `rgb(${red}, ${green}, ${blue})`;
}
};
export function extractDomain(url) {
try {
const parsedUrl = new URL(url);
return parsedUrl.host;
} catch (error) {
console.error("Invalid URL:", error.message);
return null;
}
}

80
www/app/lib/zulip.ts Normal file
View File

@@ -0,0 +1,80 @@
import { GetTranscript, GetTranscriptTopic } from "../api";
import { formatTime } from "./time";
import { extractDomain } from "./utils";
export async function sendZulipMessage(
stream: string,
topic: string,
message: string,
) {
console.log("Sendiing zulip message", stream, topic);
try {
const response = await fetch("/api/send-zulip-message", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ stream, topic, message }),
});
return await response.json();
} catch (error) {
console.error("Error:", error);
throw error;
}
}
export const ZULIP_MSG_MAX_LENGTH = 10000;
export function getZulipMessage(
transcript: GetTranscript,
topics: GetTranscriptTopic[] | null,
includeTopics: boolean,
) {
const date = new Date(transcript.created_at);
// Get the timezone offset in minutes and convert it to hours and minutes
const timezoneOffset = -date.getTimezoneOffset();
const offsetHours = String(
Math.floor(Math.abs(timezoneOffset) / 60),
).padStart(2, "0");
const offsetMinutes = String(Math.abs(timezoneOffset) % 60).padStart(2, "0");
const offsetSign = timezoneOffset >= 0 ? "+" : "-";
// Combine to get the formatted timezone offset
const formattedOffset = `${offsetSign}${offsetHours}:${offsetMinutes}`;
// Now you can format your date and time string using this offset
const formattedDate = date.toISOString().slice(0, 10);
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
const dateTimeString = `${formattedDate}T${hours}:${minutes}:${seconds}${formattedOffset}`;
const domain = window.location.origin; // Gives you "http://localhost:3000" or your deployment base URL
const link = `${domain}/transcripts/${transcript.id}`;
let headerText = `# Reflector ${transcript.title ?? "Unnamed recording"}
**Date**: <time:${dateTimeString}>
**Link**: [${extractDomain(link)}](${link})
**Duration**: ${formatTime(transcript.duration)}
`;
let topicText = "";
if (topics && includeTopics) {
topicText = "```spoiler Topics\n";
topics.forEach((topic) => {
topicText += `1. [${formatTime(topic.timestamp)}] ${topic.title}\n`;
});
topicText += "```\n\n";
}
let summary = "```spoiler Summary\n";
summary += transcript.long_summary;
summary += "```\n\n";
const message = headerText + summary + topicText + "-----\n";
return message;
}

View File

@@ -46,18 +46,18 @@ button.block {
}
/* Disabled styles */
input[type="button"][disabled],
button[disabled] {
border-color: #ccc;
background: #b8b8b8 !important;
cursor: not-allowed;
}
input[type="button"][disabled]:hover,
button[disabled]:hover {
background: #b8b8b8 !important;
cursor: not-allowed !important;
}
/* input[type="button"][disabled], */
/* button[disabled] { */
/* border-color: #ccc; */
/* background: #b8b8b8 !important; */
/* cursor: not-allowed; */
/* } */
/**/
/* input[type="button"][disabled]:hover, */
/* button[disabled]:hover { */
/* background: #b8b8b8 !important; */
/* cursor: not-allowed !important; */
/* } */
/* Red button states */
input[type="button"][data-color="red"],

View File

@@ -35,3 +35,8 @@ body.is-light-mode .input-container {
max-width: 100%;
width: auto;
}
body .select-search-container .select-search--top.select-search-select {
top: auto;
bottom: 46px;
}

View File

@@ -1,100 +0,0 @@
"use client";
import Modal from "../modal";
import getApi from "../../lib/getApi";
import useTranscript from "../useTranscript";
import useTopics from "../useTopics";
import useWaveform from "../useWaveform";
import { TopicList } from "../topicList";
import Recorder from "../recorder";
import { Topic } from "../webSocketTypes";
import React, { useEffect, useState } from "react";
import "../../styles/button.css";
import FinalSummary from "../finalSummary";
import ShareLink from "../shareLink";
import QRCode from "react-qr-code";
import TranscriptTitle from "../transcriptTitle";
type TranscriptDetails = {
params: {
transcriptId: string;
};
};
export default function TranscriptDetails(details: TranscriptDetails) {
const api = getApi();
const transcript = useTranscript(details.params.transcriptId);
const topics = useTopics(api, details.params.transcriptId);
const waveform = useWaveform(api, details.params.transcriptId);
const useActiveTopic = useState<Topic | null>(null);
if (transcript?.error || topics?.error || waveform?.error) {
return (
<Modal
title="Transcription Not Found"
text="A trascription with this ID does not exist."
/>
);
}
const fullTranscript =
topics.topics
?.map((topic) => topic.transcript)
.join("\n\n")
.replace(/ +/g, " ")
.trim() || "";
return (
<>
{transcript?.loading === true ||
waveform?.loading == true ||
topics?.loading == true ? (
<Modal title="Loading" text={"Loading transcript..."} />
) : (
<>
<div className="flex flex-col">
{transcript?.response?.title && (
<TranscriptTitle title={transcript.response.title} />
)}
<Recorder
topics={topics?.topics || []}
useActiveTopic={useActiveTopic}
waveform={waveform?.waveform}
isPastMeeting={true}
transcriptId={transcript?.response?.id}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-2 lg:grid-rows-1 gap-2 lg:gap-4 h-full">
<TopicList
topics={topics?.topics || []}
useActiveTopic={useActiveTopic}
autoscroll={false}
/>
<div className="w-full h-full grid grid-rows-layout-one grid-cols-1 gap-2 lg:gap-4">
<section className=" bg-blue-400/20 rounded-lg md:rounded-xl p-2 md:px-4 h-full">
{transcript?.response?.longSummary && (
<FinalSummary
fullTranscript={fullTranscript}
summary={transcript?.response?.longSummary}
/>
)}
</section>
<section className="flex items-center">
<div className="mr-4 hidden md:block h-auto">
<QRCode
value={`${process.env.NEXT_PUBLIC_SITE_URL}transcripts/${details.params.transcriptId}`}
level="L"
size={98}
/>
</div>
<div className="flex-grow max-w-full">
<ShareLink />
</div>
</section>
</div>
</div>
</>
)}
</>
);
}

View File

@@ -1,54 +0,0 @@
import { useEffect, useState } from "react";
import { DefaultApi, V1TranscriptsCreateRequest } from "../api/apis/DefaultApi";
import { GetTranscript } from "../api";
import { useError } from "../(errors)/errorContext";
import getApi from "../lib/getApi";
type CreateTranscript = {
response: GetTranscript | null;
loading: boolean;
error: Error | null;
create: (params: V1TranscriptsCreateRequest["createTranscript"]) => void;
};
const useCreateTranscript = (): CreateTranscript => {
const [response, setResponse] = useState<GetTranscript | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = getApi();
const create = (params: V1TranscriptsCreateRequest["createTranscript"]) => {
if (loading) return;
setLoading(true);
const requestParameters: V1TranscriptsCreateRequest = {
createTranscript: {
name: params.name || "Unnamed Transcript", // Default
targetLanguage: params.targetLanguage || "en", // Default
},
};
console.debug(
"POST - /v1/transcripts/ - Requesting new transcription creation",
requestParameters,
);
api
.v1TranscriptsCreate(requestParameters)
.then((result) => {
setResponse(result);
setLoading(false);
console.debug("New transcript created:", result);
})
.catch((err) => {
setError(err);
setErrorState(err);
setLoading(false);
});
};
return { response, loading, error, create };
};
export default useCreateTranscript;

View File

@@ -1,74 +0,0 @@
import { useRef, useState } from "react";
import React from "react";
import ReactDom from "react-dom";
import Markdown from "react-markdown";
import "../styles/markdown.css";
type FinalSummaryProps = {
summary: string;
fullTranscript: string;
};
export default function FinalSummary(props: FinalSummaryProps) {
const finalSummaryRef = useRef<HTMLParagraphElement>(null);
const [isCopiedSummary, setIsCopiedSummary] = useState(false);
const [isCopiedTranscript, setIsCopiedTranscript] = useState(false);
const handleCopySummaryClick = () => {
let text_to_copy = finalSummaryRef.current?.innerText;
text_to_copy &&
navigator.clipboard.writeText(text_to_copy).then(() => {
setIsCopiedSummary(true);
// Reset the copied state after 2 seconds
setTimeout(() => setIsCopiedSummary(false), 2000);
});
};
const handleCopyTranscriptClick = () => {
let text_to_copy = props.fullTranscript;
text_to_copy &&
navigator.clipboard.writeText(text_to_copy).then(() => {
setIsCopiedTranscript(true);
// Reset the copied state after 2 seconds
setTimeout(() => setIsCopiedTranscript(false), 2000);
});
};
return (
<div className="overflow-y-auto h-auto max-h-full">
<div className="flex flex-row flex-wrap-reverse justify-between items-center">
<h2 className="text-lg sm:text-xl md:text-2xl font-bold">
Final Summary
</h2>
<div className="ml-auto flex space-x-2 mb-2">
<button
onClick={handleCopyTranscriptClick}
className={
(isCopiedTranscript ? "bg-blue-500" : "bg-blue-400") +
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2"
}
style={{ minHeight: "30px" }}
>
{isCopiedTranscript ? "Copied!" : "Copy Full Transcript"}
</button>
<button
onClick={handleCopySummaryClick}
className={
(isCopiedSummary ? "bg-blue-500" : "bg-blue-400") +
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2"
}
style={{ minHeight: "30px" }}
>
{isCopiedSummary ? "Copied!" : "Copy Summary"}
</button>
</div>
</div>
<p ref={finalSummaryRef} className="markdown">
<Markdown>{props.summary}</Markdown>
</p>
</div>
);
}

View File

@@ -1,59 +0,0 @@
import React, { useState, useRef, useEffect, use } from "react";
const ShareLink = () => {
const [isCopied, setIsCopied] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const [currentUrl, setCurrentUrl] = useState<string>("");
useEffect(() => {
setCurrentUrl(window.location.href);
}, []);
const handleCopyClick = () => {
if (inputRef.current) {
let text_to_copy = inputRef.current.value;
text_to_copy &&
navigator.clipboard.writeText(text_to_copy).then(() => {
setIsCopied(true);
// Reset the copied state after 2 seconds
setTimeout(() => setIsCopied(false), 2000);
});
}
};
return (
<div
className="p-2 md:p-4 rounded"
style={{ background: "rgba(96, 165, 250, 0.2)" }}
>
<p className="text-sm mb-2">
You can share this link with others. Anyone with the link will have
access to the page, including the full audio recording, for the next 7
days.
</p>
<div className="flex items-center">
<input
type="text"
readOnly
value={currentUrl}
ref={inputRef}
onChange={() => {}}
className="border rounded-lg md:rounded-xl p-2 flex-grow flex-shrink overflow-auto mr-2 text-sm bg-slate-100 outline-slate-400"
/>
<button
onClick={handleCopyClick}
className={
(isCopied ? "bg-blue-500" : "bg-blue-400") +
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2"
}
style={{ minHeight: "38px" }}
>
{isCopied ? "Copied!" : "Copy"}
</button>
</div>
</div>
);
};
export default ShareLink;

View File

@@ -1,13 +0,0 @@
type TranscriptTitle = {
title: string;
};
const TranscriptTitle = (props: TranscriptTitle) => {
return (
<h2 className="text-2xl lg:text-4xl font-extrabold text-center mb-4">
{props.title}
</h2>
);
};
export default TranscriptTitle;

View File

@@ -1,48 +0,0 @@
import { useEffect, useState } from "react";
import {
DefaultApi,
V1TranscriptGetAudioMp3Request,
} from "../api/apis/DefaultApi";
import {} from "../api";
import { useError } from "../(errors)/errorContext";
type Mp3Response = {
url: string | null;
loading: boolean;
error: Error | null;
};
const useMp3 = (api: DefaultApi, id: string): Mp3Response => {
const [url, setUrl] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const getMp3 = (id: string) => {
if (!id) throw new Error("Transcript ID is required to get transcript Mp3");
setLoading(true);
const requestParameters: V1TranscriptGetAudioMp3Request = {
transcriptId: id,
};
api
.v1TranscriptGetAudioMp3(requestParameters)
.then((result) => {
setUrl(result);
setLoading(false);
console.debug("Transcript Mp3 loaded:", result);
})
.catch((err) => {
setError(err);
setErrorState(err);
});
};
useEffect(() => {
getMp3(id);
}, [id]);
return { url, loading, error };
};
export default useMp3;

View File

@@ -1,50 +0,0 @@
import { useEffect, useState } from "react";
import {
V1TranscriptGetRequest,
V1TranscriptsCreateRequest,
} from "../api/apis/DefaultApi";
import { GetTranscript } from "../api";
import { useError } from "../(errors)/errorContext";
import getApi from "../lib/getApi";
type Transcript = {
response: GetTranscript | null;
loading: boolean;
error: Error | null;
};
const useTranscript = (id: string | null): Transcript => {
const [response, setResponse] = useState<GetTranscript | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = getApi();
const getTranscript = (id: string | null) => {
if (!id) throw new Error("Transcript ID is required to get transcript");
setLoading(true);
const requestParameters: V1TranscriptGetRequest = {
transcriptId: id,
};
api
.v1TranscriptGet(requestParameters)
.then((result) => {
setResponse(result);
setLoading(false);
console.debug("Transcript Loaded:", result);
})
.catch((err) => {
setError(err);
setErrorState(err);
});
};
useEffect(() => {
getTranscript(id);
}, [id]);
return { response, loading, error };
};
export default useTranscript;

View File

@@ -1,49 +0,0 @@
import { useEffect, useState } from "react";
import {
DefaultApi,
V1TranscriptGetAudioWaveformRequest,
} from "../api/apis/DefaultApi";
import { AudioWaveform } from "../api";
import { useError } from "../(errors)/errorContext";
type AudioWaveFormResponse = {
waveform: AudioWaveform | null;
loading: boolean;
error: Error | null;
};
const useWaveform = (api: DefaultApi, id: string): AudioWaveFormResponse => {
const [waveform, setWaveform] = useState<AudioWaveform | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const getWaveform = (id: string) => {
if (!id)
throw new Error("Transcript ID is required to get transcript waveform");
setLoading(true);
const requestParameters: V1TranscriptGetAudioWaveformRequest = {
transcriptId: id,
};
api
.v1TranscriptGetAudioWaveform(requestParameters)
.then((result) => {
setWaveform(result);
setLoading(false);
console.debug("Transcript waveform loaded:", result);
})
.catch((err) => {
setError(err);
setErrorState(err);
});
};
useEffect(() => {
getWaveform(id);
}, [id]);
return { waveform, loading, error };
};
export default useWaveform;