diff --git a/www/app/(errors)/errorContext.tsx b/www/app/(errors)/errorContext.tsx new file mode 100644 index 00000000..31c5c505 --- /dev/null +++ b/www/app/(errors)/errorContext.tsx @@ -0,0 +1,31 @@ +"use client"; +import React, { createContext, useContext, useState } from "react"; + +interface ErrorContextProps { + error: string; + setError: React.Dispatch>; +} + +const ErrorContext = createContext(undefined); + +export const useError = () => { + const context = useContext(ErrorContext); + if (!context) { + throw new Error("useError must be used within an ErrorProvider"); + } + return context; +}; + +interface ErrorProviderProps { + children: React.ReactNode; +} + +export const ErrorProvider: React.FC = ({ children }) => { + const [error, setError] = useState(""); + + return ( + + {children} + + ); +}; diff --git a/www/app/(errors)/errorMessage.tsx b/www/app/(errors)/errorMessage.tsx new file mode 100644 index 00000000..874a5b8f --- /dev/null +++ b/www/app/(errors)/errorMessage.tsx @@ -0,0 +1,29 @@ +"use client"; +import { useError } from "./errorContext"; +import { useEffect, useState } from "react"; + +const ErrorMessage: React.FC = () => { + const { error, setError } = useError(); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (error) setIsVisible(true); + }, [error]); + + if (!isVisible || !error) return null; + + return ( +
{ + setIsVisible(false); + setError(""); + }} + className="max-w-xs z-50 fixed top-16 right-10 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-75 cursor-pointer transform hover:scale-105" + role="alert" + > + {error} +
+ ); +}; + +export default ErrorMessage; diff --git a/www/app/layout.tsx b/www/app/layout.tsx index fc7913f5..c15974a2 100644 --- a/www/app/layout.tsx +++ b/www/app/layout.tsx @@ -3,6 +3,8 @@ import { Roboto } 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"; const roboto = Roboto({ subsets: ["latin"], weight: "400" }); @@ -55,19 +57,24 @@ export default function RootLayout({ children }) { -
-
- + + +
+
+ -
-

Reflector

-

- Capture The Signal, Not The Noise -

+
+

+ Reflector +

+

+ Capture The Signal, Not The Noise +

+
+ {children}
- {children}
-
+
diff --git a/www/app/transcripts/new/page.tsx b/www/app/transcripts/new/page.tsx index 85e7deb8..dbf5c653 100644 --- a/www/app/transcripts/new/page.tsx +++ b/www/app/transcripts/new/page.tsx @@ -9,6 +9,7 @@ import useAudioDevice from "../useAudioDevice"; import "../../styles/button.css"; import { Topic } from "../webSocketTypes"; import getApi from "../../lib/getApi"; +import { useError } from "../../(errors)/errorContext"; const App = () => { const [stream, setStream] = useState(null); diff --git a/www/app/transcripts/useTranscript.ts b/www/app/transcripts/useTranscript.ts index 07b614f4..8b50c889 100644 --- a/www/app/transcripts/useTranscript.ts +++ b/www/app/transcripts/useTranscript.ts @@ -1,19 +1,18 @@ import { useEffect, useState } from "react"; import { DefaultApi, V1TranscriptsCreateRequest } from "../api/apis/DefaultApi"; import { GetTranscript } from "../api"; -import getApi from "../lib/getApi"; +import { useError } from "../(errors)/errorContext"; type UseTranscript = { response: GetTranscript | null; loading: boolean; - error: string | null; createTranscript: () => void; }; const useTranscript = (api: DefaultApi): UseTranscript => { const [response, setResponse] = useState(null); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const { setError } = useError(); const createTranscript = () => { setLoading(true); @@ -48,7 +47,7 @@ const useTranscript = (api: DefaultApi): UseTranscript => { createTranscript(); }, []); - return { response, loading, error, createTranscript }; + return { response, loading, createTranscript }; }; export default useTranscript; diff --git a/www/app/transcripts/useWebRTC.ts b/www/app/transcripts/useWebRTC.ts index 62694de8..387c5ac4 100644 --- a/www/app/transcripts/useWebRTC.ts +++ b/www/app/transcripts/useWebRTC.ts @@ -4,7 +4,7 @@ import { DefaultApi, V1TranscriptRecordWebrtcRequest, } from "../api/apis/DefaultApi"; -import { Configuration } from "../api/runtime"; +import { useError } from "../(errors)/errorContext"; const useWebRTC = ( stream: MediaStream | null, @@ -12,13 +12,25 @@ const useWebRTC = ( api: DefaultApi, ): Peer => { const [peer, setPeer] = useState(null); + const { setError } = useError(); useEffect(() => { if (!stream || !transcriptId) { return; } - let p: Peer = new Peer({ initiator: true, stream: stream }); + let p: Peer; + + try { + p = new Peer({ initiator: true, stream: stream }); + } catch (error) { + setError(`Failed to create WebRTC Peer: ${error.message}`); + return; + } + + p.on("error", (err) => { + setError(`WebRTC error: ${err.message}`); + }); p.on("signal", (data: any) => { if ("sdp" in data) { @@ -33,10 +45,18 @@ const useWebRTC = ( api .v1TranscriptRecordWebrtc(requestParameters) .then((answer) => { - p.signal(answer); + try { + p.signal(answer); + } catch (error) { + setError(`Failed to signal answer: ${error.message}`); + } }) .catch((err) => { - console.error("WebRTC signaling error:", err); + const errorString = + "WebRTC signaling error: " + + (err.response || err.message || "Unknown error"); + setError(errorString); + console.error(errorString); }); } }); diff --git a/www/app/transcripts/useWebSockets.ts b/www/app/transcripts/useWebSockets.ts index 726aed22..ce8b2ea3 100644 --- a/www/app/transcripts/useWebSockets.ts +++ b/www/app/transcripts/useWebSockets.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { Topic, FinalSummary, Status } from "./webSocketTypes"; +import { useError } from "../(errors)/errorContext"; type UseWebSockets = { transcriptText: string; @@ -15,6 +16,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { summary: "", }); const [status, setStatus] = useState({ value: "disconnected" }); + const { setError } = useError(); useEffect(() => { document.onkeyup = (e) => { @@ -77,41 +79,50 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { ws.onmessage = (event) => { const message = JSON.parse(event.data); - switch (message.event) { - case "TRANSCRIPT": - if (message.data.text) { - setTranscriptText((message.data.text ?? "").trim()); - console.debug("TRANSCRIPT event:", message.data); - } - break; + try { + switch (message.event) { + case "TRANSCRIPT": + if (message.data.text) { + setTranscriptText((message.data.text ?? "").trim()); + console.debug("TRANSCRIPT event:", message.data); + } + break; - case "TOPIC": - setTopics((prevTopics) => [...prevTopics, message.data]); - console.debug("TOPIC event:", message.data); - break; + case "TOPIC": + setTopics((prevTopics) => [...prevTopics, message.data]); + console.debug("TOPIC event:", message.data); + break; - case "FINAL_SUMMARY": - if (message.data) { - setFinalSummary(message.data); - console.debug("FINAL_SUMMARY event:", message.data); - } - break; + case "FINAL_SUMMARY": + if (message.data) { + setFinalSummary(message.data); + console.debug("FINAL_SUMMARY event:", message.data); + } + break; - case "STATUS": - setStatus(message.data); - break; + case "STATUS": + setStatus(message.data); + break; - default: - console.error("Unknown event:", message.event); + default: + console.error("Unknown event:", message.event); + setError(`Received unknown WebSocket event: ${message.event}`); + } + } catch (error) { + setError(`Failed to process WebSocket message: ${error.message}`); } }; ws.onerror = (error) => { console.error("WebSocket error:", error); + setError("A WebSocket error occurred."); }; - ws.onclose = () => { + ws.onclose = (event) => { console.debug("WebSocket connection closed"); + if (event.code !== 1000) { + setError(`WebSocket closed unexpectedly with code: ${event.code}`); + } }; return () => {