From d19ee037a3f144795043667c859df27a9f1b30db Mon Sep 17 00:00:00 2001 From: Shakur Date: Tue, 12 Nov 2024 14:28:12 +0200 Subject: [PATCH 01/15] add server select component --- src/components/playground/ServerSelection.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/components/playground/ServerSelection.tsx diff --git a/src/components/playground/ServerSelection.tsx b/src/components/playground/ServerSelection.tsx new file mode 100644 index 0000000..508558d --- /dev/null +++ b/src/components/playground/ServerSelection.tsx @@ -0,0 +1,29 @@ +interface ServerSelectProps { + serverInfo: Array<{ name: string; description: string; url: string }>; + selectedServerInfo: { name: string; description: string; url: string }; + handleServerChange: (e: React.ChangeEvent) => void; +} + +const ServerSelect: React.FC = ({ + serverInfo, + selectedServerInfo, + handleServerChange, +}) => { + return ( +
+ +
+ ); +}; + +export default ServerSelect; From cb60537daf3096f229db42a73edae318e3621ad6 Mon Sep 17 00:00:00 2001 From: Shakur Date: Tue, 12 Nov 2024 15:05:29 +0200 Subject: [PATCH 02/15] add server info state --- src/pages/index.tsx | 77 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index c210ab2..d217e57 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -58,14 +58,61 @@ export function HomeInner() { const { config } = useConfig(); + const serverInfo = useMemo(() => { + const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "[]"; + try { + const parsed = JSON.parse(serverUrl); + if (Array.isArray(parsed)) { + return parsed as Array<{ + name: string; + description: string; + url: string; + }>; + } + } catch (e) { + console.error("Invalid server URL format", e); + } + return [ + { + name: config.title, + description: config.description, + url: serverUrl, + }, + ]; + }, [config.title, config.description]); + + const [selectedServerInfo, setSelectedServerInfo] = useState( + serverInfo[0] || {} + ); + const handleConnect = useCallback( - async (c: boolean, mode: ConnectionMode, opts?: { language?: string }) => { + async ( + c: boolean, + mode: ConnectionMode, + opts?: { language?: string; serverUrl?: string } + ) => { const m = process.env.NEXT_PUBLIC_LIVEKIT_URL ? "env" : mode; - c ? connect(m, opts) : disconnect(); + const serverUrl = opts?.serverUrl || selectedServerInfo.url; + c ? connect(m, { ...opts, serverUrl } as any) : disconnect(); }, - [connect, disconnect] + [connect, disconnect, selectedServerInfo.url] ); + const handleServerChange = (e: React.ChangeEvent) => { + const newServerInfo = serverInfo.find( + (server) => server.url === e.target.value + ) || { + name: "", + description: "", + url: "", + }; + + setSelectedServerInfo(newServerInfo); + disconnect(); + const m = process.env.NEXT_PUBLIC_LIVEKIT_URL ? "env" : mode; + handleConnect(true, m, { serverUrl: newServerInfo.url }); + }; + const showPG = useMemo(() => { if (process.env.NEXT_PUBLIC_LIVEKIT_URL) { return true; @@ -131,6 +178,9 @@ export function HomeInner() { const m = process.env.NEXT_PUBLIC_LIVEKIT_URL ? "env" : mode; handleConnect(c, m, opts); }} + serverInfo={serverInfo} + selectedServerInfo={selectedServerInfo} + handleServerChange={handleServerChange} /> @@ -144,6 +194,27 @@ export function HomeInner() { /> )} + ); } From 3864064055f84d60c51507283287f90c11f3088d Mon Sep 17 00:00:00 2001 From: Shakur Date: Tue, 12 Nov 2024 15:06:14 +0200 Subject: [PATCH 03/15] update server url --- src/hooks/useConnection.tsx | 90 +++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/src/hooks/useConnection.tsx b/src/hooks/useConnection.tsx index f18f1f2..c0cc764 100644 --- a/src/hooks/useConnection.tsx +++ b/src/hooks/useConnection.tsx @@ -1,11 +1,11 @@ -"use client" +"use client"; import { useCloud } from "@/cloud/useCloud"; import React, { createContext, useState } from "react"; import { useCallback } from "react"; import { useConfig } from "./useConfig"; -export type ConnectionMode = "cloud" | "manual" | "env" +export type ConnectionMode = "cloud" | "manual" | "env"; type TokenGeneratorData = { shouldConnect: boolean; @@ -13,10 +13,15 @@ type TokenGeneratorData = { token: string; mode: ConnectionMode; disconnect: () => Promise; - connect: (mode: ConnectionMode, opts?: { language?: string }) => Promise; + connect: ( + mode: ConnectionMode, + opts?: { language?: string } + ) => Promise; }; -const ConnectionContext = createContext(undefined); +const ConnectionContext = createContext( + undefined +); export const ConnectionProvider = ({ children, @@ -36,46 +41,45 @@ export const ConnectionProvider = ({ setConnectionDetails((prev) => ({ ...prev, shouldConnect: false })); }, []); + const connect = useCallback( + async ( + mode: ConnectionMode, + opts?: { language?: string; serverUrl?: string } + ) => { + console.log("Connect called with mode:", mode, "and opts:", opts); + let token = ""; + let url = opts?.serverUrl || ""; + if (mode === "cloud") { + token = await generateToken(); + url = cloudWSUrl; + } else if (mode === "env") { + if (!process.env.NEXT_PUBLIC_LIVEKIT_URL) { + throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not set"); + } - const connect = useCallback(async (mode: ConnectionMode, opts?: { language?: string }) => { - console.log("Connect called with mode:", mode, "and opts:", opts); - let token = ""; - let url = ""; - if (mode === "cloud") { - token = await generateToken(); - url = cloudWSUrl; - } else if (mode === "env") { - if(!process.env.NEXT_PUBLIC_LIVEKIT_URL) { - throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not set"); + if (!url) { + throw new Error("NEXT_PUBLIC_SERVER_URL is not set"); + } + const uuid = Math.random().toString(36).substring(4); + const serverUrl = new URL(url); + serverUrl.searchParams.append("uuid", uuid); + if (opts?.language) { + serverUrl.searchParams.append("language", opts.language); + } + console.log("Server URL with params:", serverUrl.toString()); + const response = await fetch(serverUrl.toString()); + const data = await response.json(); + token = data.token; + url = data.url; + } else { + token = config.settings.token; + url = config.settings.ws_url; } - - url = process.env.NEXT_PUBLIC_LIVEKIT_URL; - if(!process.env.NEXT_PUBLIC_SERVER_URL) { - throw new Error("NEXT_PUBLIC_SERVER_URL is not set"); - } - const uuid = Math.random().toString(36).substring(4); - const serverUrl = new URL(process.env.NEXT_PUBLIC_SERVER_URL); - serverUrl.searchParams.append('uuid', uuid); - if (opts?.language) { - serverUrl.searchParams.append('language', opts.language); - } - console.log("Server URL with params:", serverUrl.toString()); - const response = await fetch(serverUrl.toString()); - const data = await response.json(); - token = data.token; - url = data.url; - } else { - token = config.settings.token; - url = config.settings.ws_url; - } - console.log("Connecting with URL:", url, "and token:", token); - setConnectionDetails({ wsUrl: url, token, shouldConnect: true, mode }); - }, [ - cloudWSUrl, - config.settings.token, - config.settings.ws_url, - generateToken, - ]); + console.log("Connecting with URL:", url, "and token:", token); + setConnectionDetails({ wsUrl: url, token, shouldConnect: true, mode }); + }, + [cloudWSUrl, config.settings.token, config.settings.ws_url, generateToken] + ); return ( { throw new Error("useConnection must be used within a ConnectionProvider"); } return context; -} +}; From c6a43b6d4fa0b5deea3da2ed6e5fb204a856dacf Mon Sep 17 00:00:00 2001 From: Shakur Date: Tue, 12 Nov 2024 15:06:50 +0200 Subject: [PATCH 04/15] use server selection component --- src/components/playground/Playground.tsx | 82 +++++++++++++++++------- 1 file changed, 60 insertions(+), 22 deletions(-) diff --git a/src/components/playground/Playground.tsx b/src/components/playground/Playground.tsx index 5b44149..5f06dcd 100644 --- a/src/components/playground/Playground.tsx +++ b/src/components/playground/Playground.tsx @@ -25,11 +25,18 @@ import { Track, } from "livekit-client"; import { ReactNode, useEffect, useMemo, useState, useRef } from "react"; +import ServerSelect from "@/components/playground/ServerSelection"; export interface PlaygroundProps { logo?: ReactNode; themeColors: string[]; - onConnect: (connect: boolean, opts?: { token?: string; url?: string; language?: string }) => void; + onConnect: ( + connect: boolean, + opts?: { token?: string; url?: string; language?: string } + ) => void; + serverInfo: { name: string; description: string; url: string }[]; + selectedServerInfo: { name: string; description: string; url: string }; + handleServerChange: (e: React.ChangeEvent) => void; } const headerHeight = 56; @@ -38,11 +45,14 @@ export default function Playground({ logo, themeColors, onConnect, + serverInfo, + selectedServerInfo, + handleServerChange, }: PlaygroundProps) { const { config, setUserSettings } = useConfig(); const { name } = useRoomInfo(); const [selectedLanguage, setSelectedLanguage] = useState(() => { - const browserLanguage = navigator.language.split('-')[0]; + const browserLanguage = navigator.language.split("-")[0]; return config.settings.language || browserLanguage; }); const { localParticipant } = useLocalParticipant(); @@ -87,7 +97,8 @@ export default function Playground({ useEffect(() => { if (transcriptionRef.current) { - transcriptionRef.current.scrollTop = transcriptionRef.current.scrollHeight; + transcriptionRef.current.scrollTop = + transcriptionRef.current.scrollHeight; } }, [agentAudioTrack]); @@ -95,15 +106,24 @@ export default function Playground({ const disconnectedContent = (
{(() => { - const priorityLanguages = ['en', 'fi', 'sv', 'de']; - const browserLanguages = (navigator.languages || [navigator.language]).map(lang => lang.split('-')[0]); - const uniqueLanguages = Array.from(new Set([ - ...browserLanguages.filter(lang => priorityLanguages.includes(lang)), - ...priorityLanguages - ])); + const priorityLanguages = ["en", "fi", "sv", "de"]; + const browserLanguages = ( + navigator.languages || [navigator.language] + ).map((lang) => lang.split("-")[0]); + const uniqueLanguages = Array.from( + new Set([ + ...browserLanguages.filter((lang) => + priorityLanguages.includes(lang) + ), + ...priorityLanguages, + ]) + ); return ( @@ -138,7 +158,11 @@ export default function Playground({ const waitingContent = (
@@ -150,7 +174,10 @@ export default function Playground({ const visualizerContent = (
@@ -173,15 +200,19 @@ export default function Playground({ />
-
+
{agentAudioTrack && ( -
+
{ setUserSettings({ ...config.settings, - language: selectedLanguage + language: selectedLanguage, }); }, [selectedLanguage, setUserSettings, config.settings]); return ( <> + - onConnect(roomState === ConnectionState.Disconnected, { language: selectedLanguage }) + onConnect(roomState === ConnectionState.Disconnected, { + language: selectedLanguage, + }) } />
Date: Wed, 13 Nov 2024 10:44:57 +0200 Subject: [PATCH 05/15] add duration timestamp helpers --- src/lib/util.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/lib/util.ts b/src/lib/util.ts index 73fc79f..4884e2e 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -15,3 +15,39 @@ export function isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } + +export function formatDuration(start: Date, end?: Date): string { + const endTime = end || new Date(); + const durationInSeconds = Math.floor( + (endTime.getTime() - start.getTime()) / 1000 + ); + + const hours = Math.floor(durationInSeconds / 3600); + const minutes = Math.floor((durationInSeconds % 3600) / 60); + const seconds = durationInSeconds % 60; + + let result = ""; + if (hours > 0) { + result += `${hours}h `; + } + if (minutes > 0 || hours > 0) { + result += `${minutes}m `; + } + result += `${seconds}s`; + + return result.trim(); +} + +export function getSessionPeriod(): string | null { + const sessionKey = "lb_connection_session"; + const sessionStart = sessionStorage.getItem(sessionKey); + + if (sessionStart) { + const startDate = new Date(parseInt(sessionStart, 10)); // Ensure the timestamp is parsed as an integer + const duration = formatDuration(startDate); + sessionStorage.removeItem(sessionKey); + return duration; + } + + return null; +} From 3db268c864dc475d59a2460ac7ed1284d846a7e0 Mon Sep 17 00:00:00 2001 From: Shakur Date: Wed, 13 Nov 2024 10:45:30 +0200 Subject: [PATCH 06/15] add def img falback url --- src/hooks/useConfig.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/hooks/useConfig.tsx b/src/hooks/useConfig.tsx index d83081a..9071dfc 100644 --- a/src/hooks/useConfig.tsx +++ b/src/hooks/useConfig.tsx @@ -18,6 +18,7 @@ export type AppConfig = { video_fit?: "cover" | "contain"; settings: UserSettings; show_qr?: boolean; + bg_image_url: string; }; export type UserSettings = { @@ -42,6 +43,7 @@ const defaultConfig: AppConfig = { title: "SaunaBuddy", description: "Your friendly sauna companion", video_fit: "cover", + bg_image_url: "/saunabuddy.png", settings: { editable: true, theme_color: "cyan", @@ -56,7 +58,7 @@ const defaultConfig: AppConfig = { }, ws_url: "", token: "", - language: 'en', + language: "en", }, show_qr: false, }; @@ -124,7 +126,7 @@ export const ConfigProvider = ({ children }: { children: React.ReactNode }) => { }, ws_url: "", token: "", - language: "en" + language: "en", } as UserSettings; }, [appConfig]); From 3adc6150f46c6e01cc945f9e218e3919adf4afc5 Mon Sep 17 00:00:00 2001 From: Shakur Date: Wed, 13 Nov 2024 10:46:26 +0200 Subject: [PATCH 07/15] start session loggin on connected --- src/hooks/useConnection.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hooks/useConnection.tsx b/src/hooks/useConnection.tsx index c0cc764..232b061 100644 --- a/src/hooks/useConnection.tsx +++ b/src/hooks/useConnection.tsx @@ -76,6 +76,7 @@ export const ConnectionProvider = ({ url = config.settings.ws_url; } console.log("Connecting with URL:", url, "and token:", token); + sessionStorage.setItem("lb_connection_session", Date.now().toString()); setConnectionDetails({ wsUrl: url, token, shouldConnect: true, mode }); }, [cloudWSUrl, config.settings.token, config.settings.ws_url, generateToken] From 9b77a493f521b7f560290db1d086bddd3e487f18 Mon Sep 17 00:00:00 2001 From: Shakur Date: Wed, 13 Nov 2024 10:47:18 +0200 Subject: [PATCH 08/15] add session tracking on disconnect --- .../playground/PlaygroundHeader.tsx | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/components/playground/PlaygroundHeader.tsx b/src/components/playground/PlaygroundHeader.tsx index 8fe40a5..055d9de 100644 --- a/src/components/playground/PlaygroundHeader.tsx +++ b/src/components/playground/PlaygroundHeader.tsx @@ -1,10 +1,12 @@ import { Button } from "@/components/button/Button"; import { LoadingSVG } from "@/components/button/LoadingSVG"; import { PlaygroundDeviceSelector } from "@/components/playground/PlaygroundDeviceSelector"; +import { getSessionPeriod } from "@/lib/util"; import { useConfig } from "@/hooks/useConfig"; import { ConnectionState } from "livekit-client"; import { ReactNode } from "react"; +import { track } from "@vercel/analytics"; type PlaygroundHeader = { logo?: ReactNode; @@ -26,6 +28,16 @@ export const PlaygroundHeader = ({ connectionState, }: PlaygroundHeader) => { const { config } = useConfig(); + + const logSessionDuration = () => { + const sessionDuration = getSessionPeriod(); + if (sessionDuration) + track("Session duration", { + name: typeof title === "string" ? title : "", + sessionDuration, + }); + }; + return (
- - + +
{connectionState === ConnectionState.Connected && ( {error &&

{error}

} +
)}