mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
Merge branch 'main' into reenable-non-latin-languages
This commit is contained in:
2
www/.env_template
Normal file
2
www/.env_template
Normal file
@@ -0,0 +1,2 @@
|
||||
FIEF_CLIENT_SECRET=<omitted, ask in zulip>
|
||||
ZULIP_API_KEY=<omitted, ask in zulip>
|
||||
2
www/.gitignore
vendored
2
www/.gitignore
vendored
@@ -39,3 +39,5 @@ next-env.d.ts
|
||||
|
||||
# Sentry Auth Token
|
||||
.sentryclirc
|
||||
|
||||
config.ts
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
94
www/app/[domain]/browse/page.tsx
Normal file
94
www/app/[domain]/browse/page.tsx
Normal 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
|
||||
<Link href="/transcripts/new" className="underline">
|
||||
record a meeting
|
||||
</Link>
|
||||
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>
|
||||
);
|
||||
}
|
||||
75
www/app/[domain]/browse/pagination.tsx
Normal file
75
www/app/[domain]/browse/pagination.tsx
Normal 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"><</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">></i>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
www/app/[domain]/domainContext.tsx
Normal file
50
www/app/[domain]/domainContext.tsx
Normal 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
166
www/app/[domain]/layout.tsx
Normal 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 ? (
|
||||
<>
|
||||
·
|
||||
<Link
|
||||
href="/browse"
|
||||
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
|
||||
prefetch={false}
|
||||
>
|
||||
Browse
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
·
|
||||
<About buttonText="About" />
|
||||
{privacy ? (
|
||||
<>
|
||||
·
|
||||
<Privacy buttonText="Privacy" />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{requireLogin ? (
|
||||
<>
|
||||
·
|
||||
<UserInfo />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</ErrorProvider>
|
||||
</ErrorBoundary>
|
||||
</DomainContextProvider>
|
||||
</FiefWrapper>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
154
www/app/[domain]/transcripts/[transcriptId]/page.tsx
Normal file
154
www/app/[domain]/transcripts/[transcriptId]/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
159
www/app/[domain]/transcripts/[transcriptId]/shareModal.tsx
Normal file
159
www/app/[domain]/transcripts/[transcriptId]/shareModal.tsx
Normal 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;
|
||||
44
www/app/[domain]/transcripts/createTranscript.ts
Normal file
44
www/app/[domain]/transcripts/createTranscript.ts
Normal 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;
|
||||
50
www/app/[domain]/transcripts/fileUploadButton.tsx
Normal file
50
www/app/[domain]/transcripts/fileUploadButton.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
186
www/app/[domain]/transcripts/finalSummary.tsx
Normal file
186
www/app/[domain]/transcripts/finalSummary.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
166
www/app/[domain]/transcripts/player.tsx
Normal file
166
www/app/[domain]/transcripts/player.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
153
www/app/[domain]/transcripts/shareLink.tsx
Normal file
153
www/app/[domain]/transcripts/shareLink.tsx
Normal 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;
|
||||
@@ -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>
|
||||
))}
|
||||
86
www/app/[domain]/transcripts/transcriptTitle.tsx
Normal file
86
www/app/[domain]/transcripts/transcriptTitle.tsx
Normal 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;
|
||||
64
www/app/[domain]/transcripts/useMp3.ts
Normal file
64
www/app/[domain]/transcripts/useMp3.ts
Normal 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;
|
||||
@@ -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;
|
||||
63
www/app/[domain]/transcripts/useTranscript.ts
Normal file
63
www/app/[domain]/transcripts/useTranscript.ts
Normal 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;
|
||||
40
www/app/[domain]/transcripts/useTranscriptList.ts
Normal file
40
www/app/[domain]/transcripts/useTranscriptList.ts
Normal 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;
|
||||
44
www/app/[domain]/transcripts/useWaveform.ts
Normal file
44
www/app/[domain]/transcripts/useWaveform.ts
Normal 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;
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
11
www/app/[domain]/transcripts/waveformLoading.tsx
Normal file
11
www/app/[domain]/transcripts/waveformLoading.tsx
Normal 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>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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
36
www/app/api/Api.ts
Normal 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
36
www/app/api/OpenApi.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export * from "./DefaultApi";
|
||||
29
www/app/api/core/ApiError.ts
Normal file
29
www/app/api/core/ApiError.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
24
www/app/api/core/ApiRequestOptions.ts
Normal file
24
www/app/api/core/ApiRequestOptions.ts
Normal 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>;
|
||||
};
|
||||
11
www/app/api/core/ApiResult.ts
Normal file
11
www/app/api/core/ApiResult.ts
Normal 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;
|
||||
};
|
||||
13
www/app/api/core/BaseHttpRequest.ts
Normal file
13
www/app/api/core/BaseHttpRequest.ts
Normal 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>;
|
||||
}
|
||||
130
www/app/api/core/CancelablePromise.ts
Normal file
130
www/app/api/core/CancelablePromise.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
25
www/app/api/core/FetchHttpRequest.ts
Normal file
25
www/app/api/core/FetchHttpRequest.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
32
www/app/api/core/OpenAPI.ts
Normal file
32
www/app/api/core/OpenAPI.ts
Normal 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
361
www/app/api/core/request.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
9
www/app/api/models/CreateParticipant.ts
Normal file
9
www/app/api/models/CreateParticipant.ts
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
10
www/app/api/models/GetTranscriptSegmentTopic.ts
Normal file
10
www/app/api/models/GetTranscriptSegmentTopic.ts
Normal 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;
|
||||
};
|
||||
16
www/app/api/models/GetTranscriptTopic.ts
Normal file
16
www/app/api/models/GetTranscriptTopic.ts
Normal 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>;
|
||||
};
|
||||
18
www/app/api/models/GetTranscriptTopicWithWords.ts
Normal file
18
www/app/api/models/GetTranscriptTopicWithWords.ts
Normal 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>;
|
||||
};
|
||||
18
www/app/api/models/GetTranscriptTopicWithWordsPerSpeaker.ts
Normal file
18
www/app/api/models/GetTranscriptTopicWithWordsPerSpeaker.ts
Normal 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>;
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
14
www/app/api/models/Page_GetTranscript_.ts
Normal file
14
www/app/api/models/Page_GetTranscript_.ts
Normal 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;
|
||||
};
|
||||
10
www/app/api/models/Participant.ts
Normal file
10
www/app/api/models/Participant.ts
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
11
www/app/api/models/SpeakerAssignment.ts
Normal file
11
www/app/api/models/SpeakerAssignment.ts
Normal 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;
|
||||
};
|
||||
8
www/app/api/models/SpeakerAssignmentStatus.ts
Normal file
8
www/app/api/models/SpeakerAssignmentStatus.ts
Normal 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;
|
||||
};
|
||||
9
www/app/api/models/SpeakerMerge.ts
Normal file
9
www/app/api/models/SpeakerMerge.ts
Normal 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;
|
||||
};
|
||||
11
www/app/api/models/SpeakerWords.ts
Normal file
11
www/app/api/models/SpeakerWords.ts
Normal 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>;
|
||||
};
|
||||
10
www/app/api/models/TranscriptParticipant.ts
Normal file
10
www/app/api/models/TranscriptParticipant.ts
Normal 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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
9
www/app/api/models/UpdateParticipant.ts
Normal file
9
www/app/api/models/UpdateParticipant.ts
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
11
www/app/api/models/Word.ts
Normal file
11
www/app/api/models/Word.ts
Normal 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;
|
||||
};
|
||||
@@ -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";
|
||||
547
www/app/api/services/DefaultService.ts
Normal file
547
www/app/api/services/DefaultService.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
·
|
||||
<Privacy buttonText="Privacy" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</ErrorProvider>
|
||||
</FiefWrapper>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
47
www/app/lib/edgeConfig.ts
Normal file
47
www/app/lib/edgeConfig.ts
Normal 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
11
www/app/lib/errorUtils.ts
Normal 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 };
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
7
www/app/lib/shareMode.ts
Normal 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
32
www/app/lib/useApi.ts
Normal 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;
|
||||
}
|
||||
@@ -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
80
www/app/lib/zulip.ts
Normal 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;
|
||||
}
|
||||
@@ -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"],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
12
www/config-template.ts
Normal file
12
www/config-template.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const localConfig = {
|
||||
features: {
|
||||
requireLogin: true,
|
||||
privacy: true,
|
||||
browse: true,
|
||||
sendToZulip: true,
|
||||
},
|
||||
api_url: "http://127.0.0.1:1250",
|
||||
websocket_url: "ws://127.0.0.1:1250",
|
||||
auth_callback_url: "http://localhost:3000/auth-callback",
|
||||
zulip_streams: "", // Find the value on zulip
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user