Allowing settings to be changed in URL hash (#45)
Co-authored-by: mattherzog <Herzog.Matt@gmail.com> Co-authored-by: Neil Dwyer <neildwyer1991@gmail.com>
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
import jsYaml from "js-yaml";
|
||||
|
||||
const APP_CONFIG = process.env.NEXT_PUBLIC_APP_CONFIG;
|
||||
|
||||
export type AppConfig = {
|
||||
title: string;
|
||||
description: string;
|
||||
github_link?: string;
|
||||
theme_color?: string;
|
||||
video_fit?: "cover" | "contain";
|
||||
outputs: {
|
||||
audio: boolean;
|
||||
video: boolean;
|
||||
chat: boolean;
|
||||
};
|
||||
inputs: {
|
||||
mic: boolean;
|
||||
camera: boolean;
|
||||
};
|
||||
show_qr?: boolean;
|
||||
};
|
||||
|
||||
// Fallback if NEXT_PUBLIC_APP_CONFIG is not set
|
||||
const defaultConfig: AppConfig = {
|
||||
title: "Agents Playground",
|
||||
description: "A playground for testing LiveKit Agents",
|
||||
theme_color: "cyan",
|
||||
video_fit: "cover",
|
||||
outputs: {
|
||||
audio: true,
|
||||
video: true,
|
||||
chat: true,
|
||||
},
|
||||
inputs: {
|
||||
mic: true,
|
||||
camera: true,
|
||||
},
|
||||
show_qr: false,
|
||||
};
|
||||
|
||||
export const useAppConfig = (): AppConfig => {
|
||||
if (APP_CONFIG) {
|
||||
try {
|
||||
const parsedConfig = jsYaml.load(APP_CONFIG);
|
||||
console.log("parsedConfig:", parsedConfig);
|
||||
return parsedConfig as AppConfig;
|
||||
} catch (e) {
|
||||
console.error("Error parsing app config:", e);
|
||||
return defaultConfig;
|
||||
}
|
||||
} else {
|
||||
return defaultConfig;
|
||||
}
|
||||
};
|
||||
198
src/hooks/useConfig.tsx
Normal file
198
src/hooks/useConfig.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
"use client"
|
||||
|
||||
import { getCookie, setCookie } from "cookies-next";
|
||||
import jsYaml from "js-yaml";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { createContext, useCallback, useMemo, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export type AppConfig = {
|
||||
title: string;
|
||||
description: string;
|
||||
github_link?: string;
|
||||
video_fit?: "cover" | "contain";
|
||||
settings: UserSettings;
|
||||
show_qr?: boolean;
|
||||
};
|
||||
|
||||
export type UserSettings = {
|
||||
theme_color: string;
|
||||
chat: boolean;
|
||||
inputs: {
|
||||
camera: boolean;
|
||||
mic: boolean;
|
||||
};
|
||||
outputs: {
|
||||
audio: boolean;
|
||||
video: boolean;
|
||||
};
|
||||
ws_url: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
// Fallback if NEXT_PUBLIC_APP_CONFIG is not set
|
||||
const defaultConfig: AppConfig = {
|
||||
title: "LiveKit Agents Playground",
|
||||
description: "A playground for testing LiveKit Agents",
|
||||
video_fit: "cover",
|
||||
settings: {
|
||||
theme_color: "cyan",
|
||||
chat: true,
|
||||
inputs: {
|
||||
camera: true,
|
||||
mic: true,
|
||||
},
|
||||
outputs: {
|
||||
audio: true,
|
||||
video: true,
|
||||
},
|
||||
ws_url: "",
|
||||
token: ""
|
||||
},
|
||||
show_qr: false,
|
||||
};
|
||||
|
||||
const useAppConfig = (): AppConfig => {
|
||||
return useMemo(() => {
|
||||
if (process.env.NEXT_PUBLIC_APP_CONFIG) {
|
||||
try {
|
||||
const parsedConfig = jsYaml.load(
|
||||
process.env.NEXT_PUBLIC_APP_CONFIG
|
||||
) as AppConfig;
|
||||
return parsedConfig;
|
||||
} catch (e) {
|
||||
console.error("Error parsing app config:", e);
|
||||
}
|
||||
}
|
||||
return defaultConfig;
|
||||
}, []);
|
||||
};
|
||||
|
||||
type ConfigData = {
|
||||
config: AppConfig;
|
||||
setUserSettings: (settings: UserSettings) => void;
|
||||
};
|
||||
|
||||
const ConfigContext = createContext<ConfigData | undefined>(undefined);
|
||||
|
||||
export const ConfigProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const appConfig = useAppConfig();
|
||||
const router = useRouter();
|
||||
|
||||
const getSettingsFromUrl = useCallback(() => {
|
||||
if(typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
if (!window.location.hash) {
|
||||
return null;
|
||||
}
|
||||
const params = new URLSearchParams(window.location.hash.replace("#", ""));
|
||||
return {
|
||||
chat: params.get("chat") === "1",
|
||||
theme_color: params.get("theme_color"),
|
||||
inputs: {
|
||||
camera: params.get("cam") === "1",
|
||||
mic: params.get("mic") === "1",
|
||||
},
|
||||
outputs: {
|
||||
audio: params.get("audio") === "1",
|
||||
video: params.get("video") === "1",
|
||||
chat: params.get("chat") === "1",
|
||||
},
|
||||
ws_url: "",
|
||||
token: ""
|
||||
} as UserSettings;
|
||||
}, [])
|
||||
|
||||
const getSettingsFromCookies = useCallback(() => {
|
||||
const jsonSettings = getCookie("lk_settings");
|
||||
if (!jsonSettings) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(jsonSettings) as UserSettings;
|
||||
}, [])
|
||||
|
||||
const setUrlSettings = useCallback((us: UserSettings) => {
|
||||
const obj = new URLSearchParams({
|
||||
cam: boolToString(us.inputs.camera),
|
||||
mic: boolToString(us.inputs.mic),
|
||||
video: boolToString(us.outputs.video),
|
||||
audio: boolToString(us.outputs.audio),
|
||||
chat: boolToString(us.chat),
|
||||
theme_color: us.theme_color || "cyan",
|
||||
});
|
||||
// Note: We don't set ws_url and token to the URL on purpose
|
||||
router.replace("/#" + obj.toString());
|
||||
}, [router])
|
||||
|
||||
const setCookieSettings = useCallback((us: UserSettings) => {
|
||||
const json = JSON.stringify(us);
|
||||
setCookie("lk_settings", json);
|
||||
}, [])
|
||||
|
||||
const getConfig = useCallback(() => {
|
||||
const appConfigFromSettings = appConfig;
|
||||
const cookieSettigs = getSettingsFromCookies();
|
||||
const urlSettings = getSettingsFromUrl();
|
||||
if(!cookieSettigs) {
|
||||
if(urlSettings) {
|
||||
setCookieSettings(urlSettings);
|
||||
}
|
||||
}
|
||||
if(!urlSettings) {
|
||||
if(cookieSettigs) {
|
||||
setUrlSettings(cookieSettigs);
|
||||
}
|
||||
}
|
||||
const newCookieSettings = getSettingsFromCookies();
|
||||
if(!newCookieSettings) {
|
||||
return appConfigFromSettings;
|
||||
}
|
||||
appConfigFromSettings.settings = newCookieSettings;
|
||||
return {...appConfigFromSettings};
|
||||
}, [
|
||||
appConfig,
|
||||
getSettingsFromCookies,
|
||||
getSettingsFromUrl,
|
||||
setCookieSettings,
|
||||
setUrlSettings,
|
||||
]);
|
||||
|
||||
const setUserSettings = useCallback((settings: UserSettings) => {
|
||||
setUrlSettings(settings);
|
||||
setCookieSettings(settings);
|
||||
_setConfig((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
settings: settings,
|
||||
};
|
||||
})
|
||||
}, [setCookieSettings, setUrlSettings]);
|
||||
|
||||
const [config, _setConfig] = useState<AppConfig>(getConfig());
|
||||
|
||||
// Run things client side because we use cookies
|
||||
useEffect(() => {
|
||||
_setConfig(getConfig());
|
||||
}, [getConfig]);
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ config, setUserSettings }}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useConfig = () => {
|
||||
const context = React.useContext(ConfigContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useConfig must be used within a ConfigProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
const boolToString = (b: boolean) => (b ? "1" : "0");
|
||||
89
src/hooks/useTokenGenerator.tsx
Normal file
89
src/hooks/useTokenGenerator.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useState } from "react";
|
||||
import { useConfig } from "./useConfig";
|
||||
import { useCallback } from "react";
|
||||
|
||||
// Note: cloud mode is only used in our private, hosted version
|
||||
export type Mode = "cloud" | "env" | "manual";
|
||||
|
||||
type TokenGeneratorData = {
|
||||
shouldConnect: boolean;
|
||||
wsUrl: string;
|
||||
token: string;
|
||||
disconnect: () => Promise<void>;
|
||||
connect: (mode: Mode) => Promise<void>;
|
||||
};
|
||||
|
||||
const TokenGeneratorContext = createContext<TokenGeneratorData | undefined>(undefined);
|
||||
|
||||
export const TokenGeneratorProvider = ({
|
||||
children,
|
||||
generateConnectionDetails,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
// generateConnectionDetails is only required in cloud mode
|
||||
generateConnectionDetails?: () => Promise<{ wsUrl: string; token: string }>;
|
||||
}) => {
|
||||
const { config } = useConfig();
|
||||
const [token, setToken] = useState("");
|
||||
const [wsUrl, setWsUrl] = useState("");
|
||||
const [shouldConnect, setShouldConnect] = useState(false);
|
||||
const connect = useCallback(
|
||||
async (mode: Mode) => {
|
||||
if (mode === "cloud") {
|
||||
if (!generateConnectionDetails) {
|
||||
throw new Error(
|
||||
"generateConnectionDetails must be provided in cloud mode"
|
||||
);
|
||||
}
|
||||
const { wsUrl, token } = await generateConnectionDetails();
|
||||
setWsUrl(wsUrl);
|
||||
setToken(token);
|
||||
} else if (mode === "env") {
|
||||
const url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
||||
if (!url) {
|
||||
throw new Error("NEXT_PUBLIC_LIVEKIT_URL must be set in env mode");
|
||||
}
|
||||
const res = await fetch("/api/token");
|
||||
const { accessToken } = await res.json();
|
||||
setWsUrl(url);
|
||||
setToken(accessToken);
|
||||
} else if (mode === "manual") {
|
||||
setWsUrl(config.settings.ws_url);
|
||||
setToken(config.settings.token);
|
||||
}
|
||||
setShouldConnect(true);
|
||||
},
|
||||
[
|
||||
config.settings.token,
|
||||
config.settings.ws_url,
|
||||
generateConnectionDetails,
|
||||
]
|
||||
);
|
||||
const disconnect = useCallback(async () => {
|
||||
setShouldConnect(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TokenGeneratorContext.Provider
|
||||
value={{
|
||||
wsUrl,
|
||||
token,
|
||||
shouldConnect,
|
||||
connect,
|
||||
disconnect,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TokenGeneratorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTokenGenerator = () => {
|
||||
const context = React.useContext(TokenGeneratorContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTokenGenerator must be used within a TokenGeneratorProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user