Skip to content

Commit 974e786

Browse files
authored
feat: parse app config data (#20)
1 parent 74866d1 commit 974e786

File tree

6 files changed

+55
-37
lines changed

6 files changed

+55
-37
lines changed

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
LIVEKIT_API_KEY=
22
LIVEKIT_API_SECRET=
3-
LIVEKIT_URL=wss://myproject.livekit.cloud
3+
LIVEKIT_URL=
4+
NEXT_PUBLIC_APP_CONFIG_ENDPOINT=
5+
SANDBOX_ID=

app-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AppConfig } from './lib/types';
22

3-
export const APP_CONFIG: AppConfig = {
3+
export const APP_CONFIG_DEFAULTS: AppConfig = {
44
companyName: 'LiveKit',
55
pageTitle: 'Voice Assistant',
66
pageDescription: 'A voice assistant built with LiveKit',

app/(app)/layout.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { getAppConfig } from '@/lib/utils';
1+
import { headers } from 'next/headers';
2+
import { getAppConfig, getOrigin } from '@/lib/utils';
23

34
interface AppLayoutProps {
45
children: React.ReactNode;
56
}
67

78
export default async function AppLayout({ children }: AppLayoutProps) {
8-
const { companyName, logo, logoDark } = await getAppConfig();
9+
const hdrs = await headers();
10+
const origin = getOrigin(hdrs);
11+
const { companyName, logo, logoDark } = await getAppConfig(origin);
12+
913
return (
1014
<>
1115
<header className="fixed top-0 left-0 z-50 hidden w-full flex-row justify-between p-6 md:flex">

app/(app)/page.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
1-
import { Metadata, ResolvingMetadata } from 'next';
1+
import { headers } from 'next/headers';
22
import { App } from '@/components/app';
3-
import { getAppConfig } from '@/lib/utils';
4-
5-
export async function generateMetadata(_props: any, parent: ResolvingMetadata): Promise<Metadata> {
6-
const { pageTitle, pageDescription } = await getAppConfig();
7-
const parentMetadata = await parent;
8-
9-
return {
10-
...parentMetadata,
11-
title: pageTitle,
12-
description: pageDescription + '\n\nBuilt with LiveKit Agents.',
13-
} as Metadata;
14-
}
3+
import { getAppConfig, getOrigin } from '@/lib/utils';
154

165
export default async function Page() {
17-
const appConfig = await getAppConfig();
6+
const hdrs = await headers();
7+
const origin = getOrigin(hdrs);
8+
const appConfig = await getAppConfig(origin);
189

1910
return <App appConfig={appConfig} />;
2011
}

app/layout.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { Metadata } from 'next';
1+
import { headers } from 'next/headers';
22
import { Public_Sans } from 'next/font/google';
33
import localFont from 'next/font/local';
44
import { ApplyThemeScript, ThemeToggle } from '@/components/theme-toggle';
5-
import { getAppConfig } from '@/lib/utils';
5+
import { getAppConfig, getOrigin } from '@/lib/utils';
66
import './globals.css';
77

88
const publicSans = Public_Sans({
@@ -36,17 +36,14 @@ const commitMono = localFont({
3636
variable: '--font-commit-mono',
3737
});
3838

39-
export const metadata: Metadata = {
40-
title: 'Voice Assistant',
41-
description: 'A voice assistant built with LiveKit',
42-
};
43-
4439
interface RootLayoutProps {
4540
children: React.ReactNode;
4641
}
4742

4843
export default async function RootLayout({ children }: RootLayoutProps) {
49-
const { accent, accentDark } = await getAppConfig();
44+
const hdrs = await headers();
45+
const origin = getOrigin(hdrs);
46+
const { accent, accentDark, pageTitle, pageDescription } = await getAppConfig(origin);
5047

5148
const styles = [
5249
accent ? `:root { --primary: ${accent}; }` : '',
@@ -59,6 +56,8 @@ export default async function RootLayout({ children }: RootLayoutProps) {
5956
<html lang="en" suppressHydrationWarning className="scroll-smooth">
6057
<head>
6158
{styles && <style>{styles}</style>}
59+
<title>{pageTitle}</title>
60+
<meta name="description" content={pageDescription + '\n\nBuilt with LiveKit Agents.'} />
6261
<ApplyThemeScript />
6362
</head>
6463
<body

lib/utils.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import { type ClassValue, clsx } from 'clsx';
33
import { Room } from 'livekit-client';
44
import { twMerge } from 'tailwind-merge';
55
import type { ReceivedChatMessage, TextStreamData } from '@livekit/components-react';
6-
import { APP_CONFIG } from '@/app-config';
6+
import { APP_CONFIG_DEFAULTS } from '@/app-config';
77
import type { AppConfig, SandboxConfig } from './types';
88

9+
export const CONFIG_ENDPOINT = process.env.NEXT_PUBLIC_APP_CONFIG_ENDPOINT;
10+
export const SANDBOX_ID = process.env.SANDBOX_ID;
11+
912
export const THEME_STORAGE_KEY = 'theme-mode';
1013
export const THEME_MEDIA_QUERY = '(prefers-color-scheme: dark)';
1114

@@ -30,25 +33,44 @@ export function transcriptionToChatMessage(
3033
};
3134
}
3235

36+
export function getOrigin(headers: Headers): string {
37+
const host = headers.get('host');
38+
const proto = headers.get('x-forwarded-proto') || 'https';
39+
return `${proto}://${host}`;
40+
}
41+
3342
// https://react.dev/reference/react/cache#caveats
3443
// > React will invalidate the cache for all memoized functions for each server request.
35-
export const getAppConfig = cache(async (): Promise<AppConfig> => {
36-
if (process.env.NEXT_PUBLIC_APP_CONFIG_ENDPOINT) {
44+
export const getAppConfig = cache(async (origin: string): Promise<AppConfig> => {
45+
if (CONFIG_ENDPOINT) {
46+
const sandboxId = SANDBOX_ID ?? origin.split('.')[0];
47+
3748
try {
38-
const url = new URL(process.env.NEXT_PUBLIC_APP_CONFIG_ENDPOINT, window.location.origin);
39-
const response = await fetch(url.toString(), {
49+
const response = await fetch(CONFIG_ENDPOINT, {
4050
cache: 'no-store',
51+
headers: { 'X-Sandbox-ID': sandboxId },
4152
});
42-
const sandboxConfig: SandboxConfig = await response.json();
4353

44-
return {
45-
...APP_CONFIG,
46-
...sandboxConfig,
47-
};
54+
const remoteConfig: SandboxConfig = await response.json();
55+
const config: AppConfig = { ...APP_CONFIG_DEFAULTS };
56+
57+
for (const [key, entry] of Object.entries(remoteConfig)) {
58+
if (entry === null) continue;
59+
if (
60+
key in config &&
61+
typeof config[key as keyof AppConfig] === entry.type &&
62+
typeof config[key as keyof AppConfig] === typeof entry.value
63+
) {
64+
// @ts-expect-error I'm not sure quite how to appease TypeScript, but we've thoroughly checked types above
65+
config[key as keyof AppConfig] = entry.value as AppConfig[keyof AppConfig];
66+
}
67+
}
68+
69+
return config;
4870
} catch (error) {
4971
console.error('!!!', error);
5072
}
5173
}
5274

53-
return APP_CONFIG;
75+
return APP_CONFIG_DEFAULTS;
5476
});

0 commit comments

Comments
 (0)