Skip to content

Commit

Permalink
feat: onboarding modal
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelint committed Oct 1, 2024
1 parent 64416e7 commit 8c0e386
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 6 deletions.
3 changes: 2 additions & 1 deletion webapp/src/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const api_url = 'http://localhost:1234';
const openai_api_url = `${api_url}/openai/v1`;

const AVAILABLE_LLM_MODELS: readonly [string, ...string[]] = ['openai:gpt-4o', 'openai:gpt-4o-mini'];
const LLM_API_KEYS_KEYS: readonly [string, ...string[]] = ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY'];

const appConfig = {
app_title: 'AI Likes Human',
Expand All @@ -11,5 +12,5 @@ const appConfig = {
available_llm_models: AVAILABLE_LLM_MODELS,
};

export { api_url, openai_api_url };
export { api_url, openai_api_url, LLM_API_KEYS_KEYS };
export default appConfig;
46 changes: 46 additions & 0 deletions webapp/src/app/_onboarding/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import OnBoarding from './onboarding';
import { Button } from '@/components/ui/button';
import { useMustOnboard } from './use-must-onboard';
import { useEffect, useState } from 'react';

export function OnBoardingModal() {
const { value: mustOnboard } = useMustOnboard();
const [openOnboarding, setOpenOnboarding] = useState(false);

useEffect(() => {
if (mustOnboard) {
setOpenOnboarding(mustOnboard);
}
}, [mustOnboard]);

const onOpenChange = (open: boolean) => {
setOpenOnboarding(open);
};

return (
<Dialog open={openOnboarding}>
<DialogContent>
<DialogHeader>
<DialogTitle>Onboarding</DialogTitle>
<DialogDescription>
At least one API Key of one of the providers must be set.
</DialogDescription>
</DialogHeader>
<div className='max-w-full'>
<OnBoarding />
</div>
<DialogFooter>
<Button type="submit" disabled={mustOnboard} onClick={() => onOpenChange(false)}>Done</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
9 changes: 9 additions & 0 deletions webapp/src/app/_onboarding/onboarding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import ApiKeysSection from '../settings/_components/api-keys.section';

export default function OnBoarding() {
return (
<div>
<ApiKeysSection />
</div>
);
}
23 changes: 23 additions & 0 deletions webapp/src/app/_onboarding/use-must-onboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { isAtLeastOneLlmApiKeySet } from '@/lib/api/llm';
import { useAsyncRetry, useInterval } from 'react-use';

interface Props {
refreshInterval?: number;
}

export function useMustOnboard({ refreshInterval = 1000 }: Props = {}) {

const state = useAsyncRetry(async () => {
const response = await isAtLeastOneLlmApiKeySet();
return !response;
}, []);

useInterval(
() => {
state.retry();
},
state.value === true ? refreshInterval : null
);

return state;
}
2 changes: 2 additions & 0 deletions webapp/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import NewThreadPrompt from './_components/new-thread-prompt';
import RecentThreads from './_components/recent-threads';
import { OnBoardingModal } from './_onboarding/modal';


export default function Home() {
return (
<main className="h-full flex flex-col">
<RecentThreads />
<NewThreadPrompt />
<OnBoardingModal />
</main>
);
}
5 changes: 2 additions & 3 deletions webapp/src/app/settings/_components/api-keys.section.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Section } from '@/components/section';
import { ConfigurationKvEditor } from './configuration-kv-editor';
import { LLM_API_KEYS_KEYS } from '@/app.config';


export default function ApiKeysSection() {
return (
<Section id='api-keys' title="API Keys">
<ConfigurationKvEditor kv_key="OPENAI_API_KEY" isSecret />
<ConfigurationKvEditor kv_key="ANTHROPIC_API_KEY" isSecret />
<ConfigurationKvEditor kv_key="SERPER_API_KEY" isSecret />
{ LLM_API_KEYS_KEYS.map((key) => <ConfigurationKvEditor key={key} kv_key={key} isSecret />) }
</Section>
);
}
2 changes: 1 addition & 1 deletion webapp/src/components/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function Section({ id, title, description, children }: Props) {
<CardTitle>{title}</CardTitle>
{ description && <CardDescription>{description}</CardDescription> }
</CardHeader>
<CardContent>
<CardContent className='flex flex-col gap-4'>
{ children }
</CardContent>
</Card>
Expand Down
37 changes: 37 additions & 0 deletions webapp/src/lib/api/llm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { when } from 'jest-when';
import { isAtLeastOneLlmApiKeySet } from './llm';
import { findConfiguration } from './tauri';
import { LLM_API_KEYS_KEYS } from '@/app.config';

vi.mock('./tauri');
describe('isAtLeastOneLlmApiKeySet', () => {

it('should return false when each api key value is empty', async () => {
LLM_API_KEYS_KEYS.forEach((key) => {
when(findConfiguration).mockResolvedValue({
key: key,
value: '',
});
});

const result = await isAtLeastOneLlmApiKeySet();

expect(result).toBe(false);
});

it('should return true when one of api key value is not empty', async () => {
when(findConfiguration).mockResolvedValue({
key: '',
value: '',
});

when(findConfiguration).calledWith(LLM_API_KEYS_KEYS[0]).mockResolvedValue({
key: '',
value: 'some',
});

const result = await isAtLeastOneLlmApiKeySet();

expect(result).toBe(true);
});
});
13 changes: 13 additions & 0 deletions webapp/src/lib/api/llm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { LLM_API_KEYS_KEYS } from '@/app.config';
import { findConfiguration } from './tauri';

export async function isAtLeastOneLlmApiKeySet(): Promise<boolean> {
for (const key of LLM_API_KEYS_KEYS) {
const config = await findConfiguration(key);
if (config?.value) {
return true;
}
}

return false;
}
2 changes: 1 addition & 1 deletion webapp/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default defineConfig({
global: {},
},
test: {
include: ['**/*.test.tsx'],
include: ['**/*.test.ts*'],
environment: 'jsdom',
setupFiles: ['./vitest-setup.js'],
globals: true,
Expand Down

0 comments on commit 8c0e386

Please sign in to comment.