Skip to content
18 changes: 14 additions & 4 deletions src/components/login/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { Analytics } from "@vercel/analytics/react";
import { track } from "@vercel/analytics";
import { isValidEmail } from "@/lib/util";
import { useConfig } from "@/hooks/useConfig";

export default function Login({
onAuthenticated,
Expand All @@ -12,6 +12,7 @@ export default function Login({
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const { config } = useConfig();

useEffect(() => {
// Attempt to verify the session by making a request to the server
Expand Down Expand Up @@ -82,16 +83,15 @@ export default function Login({
<div
className="flex justify-center items-center h-screen bg-gray-100"
style={{
backgroundImage: "url('/saunabuddy.png')",
backgroundImage: `url('${config.bg_image_url || "/saunabuddy.png"}')`,
backgroundSize: "cover",
objectFit: "cover",
}}
>
<Analytics />
{loading ? (
<div className="text-white">Loading...</div>
) : (
<div className="bg-white p-10 rounded-lg shadow-lg max-w-md w-full text-center">
<div className="bg-white px-10 pt-10 rounded-lg shadow-lg max-w-md w-full text-center">
<h1 className="text-2xl font-bold mb-6 text-gray-800">
Login To SaunaBuddy
</h1>
Expand All @@ -116,6 +116,16 @@ export default function Login({
Login
</button>
{error && <p className="text-red-500 mt-4">{error}</p>}
<div className="my-5">
<a
href="https://docs.google.com/forms/d/e/1FAIpQLSdh1AUrn2pDBcyRMgWpCj8G90DiGwov5wbcSZ6RRQneQowN_g/viewform"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 underline"
>
Request access to the SaunaBuddy beta
</a>
</div>
</div>
)}
</div>
Expand Down
77 changes: 55 additions & 22 deletions src/components/playground/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLSelectElement>) => void;
}

const headerHeight = 56;
Expand All @@ -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();
Expand Down Expand Up @@ -87,23 +97,35 @@ export default function Playground({

useEffect(() => {
if (transcriptionRef.current) {
transcriptionRef.current.scrollTop = transcriptionRef.current.scrollHeight;
transcriptionRef.current.scrollTop =
transcriptionRef.current.scrollHeight;
}
}, [agentAudioTrack]);

const backgroundStyle = {
backgroundImage: `url('${config.bg_image_url || "/saunabuddy.png"}')`,
maxWidth: "1024px",
};

const audioTileContent = useMemo(() => {
const disconnectedContent = (
<div
className="flex flex-col items-center justify-center w-full h-full bg-cover bg-center mx-auto"
style={{ backgroundImage: "url('/saunabuddy.png')", maxWidth: "1024px" }}
style={backgroundStyle}
>
{(() => {
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 (
<select
Expand All @@ -113,14 +135,14 @@ export default function Playground({
setSelectedLanguage(newLanguage);
setUserSettings({
...config.settings,
language: newLanguage
language: newLanguage,
});
}}
value={selectedLanguage}
>
{priorityLanguages.map(lang => (
{priorityLanguages.map((lang) => (
<option key={lang} value={lang}>
{new Intl.DisplayNames([lang], { type: 'language' }).of(lang)}
{new Intl.DisplayNames([lang], { type: "language" }).of(lang)}
</option>
))}
</select>
Expand All @@ -138,7 +160,7 @@ export default function Playground({
const waitingContent = (
<div
className="flex items-center justify-center w-full h-full bg-cover bg-center mx-auto"
style={{ backgroundImage: "url('/saunabuddy.png')", maxWidth: "1024px", maxHeight: "100%" }}
style={{ ...backgroundStyle, maxHeight: "100%" }}
>
<div className="bg-white bg-opacity-70 p-4 rounded">
<LoadingSVG />
Expand All @@ -150,7 +172,7 @@ export default function Playground({
const visualizerContent = (
<div
className="flex flex-col items-center justify-between w-full h-full bg-cover bg-center mx-auto"
style={{ backgroundImage: "url('/saunabuddy.png')", maxWidth: "1024px" }}
style={backgroundStyle}
>
<div className="flex overflow-y-auto items-center h-32 mt-16 md:mt-32">
<div className="bg-white bg-opacity-70 p-4 rounded relative mr-4">
Expand All @@ -173,15 +195,19 @@ export default function Playground({
/>
</div>

<div ref={transcriptionRef} className="bg-black bg-opacity-50 p-5 w-full mt-auto relative" style={{ maxHeight: '50%', overflowY: 'auto' }}>
<div
ref={transcriptionRef}
className="bg-black bg-opacity-50 p-5 w-full mt-auto relative"
style={{ maxHeight: "50%", overflowY: "auto" }}
>
<button
onClick={() => setShowTranscription(!showTranscription)}
className="absolute top-2 right-2 px-2 py-1 bg-gray-700 text-gray-300 rounded-sm hover:bg-gray-600"
>
{showTranscription ? 'Hide' : 'Show'}
{showTranscription ? "Hide" : "Show"}
</button>
{agentAudioTrack && (
<div style={{ display: showTranscription ? 'block' : 'none' }}>
<div style={{ display: showTranscription ? "block" : "none" }}>
<TranscriptionTile
agentAudioTrack={agentAudioTrack}
accentColor={config.settings.theme_color}
Expand Down Expand Up @@ -214,26 +240,33 @@ export default function Playground({
useEffect(() => {
setUserSettings({
...config.settings,
language: selectedLanguage
language: selectedLanguage,
});
}, [selectedLanguage, setUserSettings, config.settings]);

return (
<>
<ServerSelect
serverInfo={serverInfo}
selectedServerInfo={selectedServerInfo}
handleServerChange={handleServerChange}
/>
<PlaygroundHeader
title={config.title}
title={selectedServerInfo.name}
logo={logo}
githubLink={config.github_link}
height={headerHeight}
accentColor={config.settings.theme_color}
connectionState={roomState}
onConnectClicked={() =>
onConnect(roomState === ConnectionState.Disconnected, { language: selectedLanguage })
onConnect(roomState === ConnectionState.Disconnected, {
language: selectedLanguage,
})
}
/>
<div
className={`flex gap-4 py-4 grow w-full selection:bg-${config.settings.theme_color}-900`}
style={{ height: '100%', maxHeight: '100%' }}
style={{ height: "100%", maxHeight: "100%" }}
>
<PlaygroundTile
title=""
Expand Down
21 changes: 16 additions & 5 deletions src/components/playground/PlaygroundHeader.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
<div
className={`flex flex-col sm:flex-row gap-4 pt-4 text-${accentColor}-500 justify-between items-center shrink-0`}
Expand All @@ -41,14 +53,15 @@ export const PlaygroundHeader = ({
</div>
<div className="flex flex-col sm:flex-row basis-1/4 justify-end items-center gap-2">
<div className="flex gap-2">
<PlaygroundDeviceSelector kind='audiooutput' />
<PlaygroundDeviceSelector kind='audioinput' />
<PlaygroundDeviceSelector kind="audiooutput" />
<PlaygroundDeviceSelector kind="audioinput" />
</div>
{connectionState === ConnectionState.Connected && (
<Button
accentColor="red"
onClick={() => {
onConnectClicked();
logSessionDuration();
}}
>
Disconnect
Expand All @@ -59,6 +72,4 @@ export const PlaygroundHeader = ({
);
};

const LKLogo = () => (
<img src="/logo.svg" alt="Logo" width="80" height="80" />
);
const LKLogo = () => <img src="/logo.svg" alt="Logo" width="80" height="80" />;
29 changes: 29 additions & 0 deletions src/components/playground/ServerSelection.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLSelectElement>) => void;
}

const ServerSelect: React.FC<ServerSelectProps> = ({
serverInfo,
selectedServerInfo,
handleServerChange,
}) => {
return (
<div className="text-right mt-4">
<select
className="px-4 py-2 bg-gray-800 text-gray-300 border border-gray-700 rounded-sm"
value={selectedServerInfo.url}
onChange={handleServerChange}
>
{serverInfo.map((server) => (
<option key={server.name} value={server.url}>
{server.name}
</option>
))}
</select>
</div>
);
};

export default ServerSelect;
6 changes: 4 additions & 2 deletions src/hooks/useConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type AppConfig = {
video_fit?: "cover" | "contain";
settings: UserSettings;
show_qr?: boolean;
bg_image_url?: string;
};

export type UserSettings = {
Expand All @@ -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",
Expand All @@ -56,7 +58,7 @@ const defaultConfig: AppConfig = {
},
ws_url: "",
token: "",
language: 'en',
language: "en",
},
show_qr: false,
};
Expand Down Expand Up @@ -124,7 +126,7 @@ export const ConfigProvider = ({ children }: { children: React.ReactNode }) => {
},
ws_url: "",
token: "",
language: "en"
language: "en",
} as UserSettings;
}, [appConfig]);

Expand Down
Loading