Skip to content

Commit f550d21

Browse files
committed
feat: add MVP of codeium oauth process
1 parent 69b6049 commit f550d21

6 files changed

Lines changed: 139 additions & 2 deletions

File tree

packages/web/src/components/use-settings.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ export function SettingsProvider({ config, children }: ProviderPropsType) {
2424
const updateConfig = async (newConfig: Partial<SettingsType>) => {
2525
// Filter out null values and convert back to an object
2626
const changeSet = Object.fromEntries(
27-
Object.entries(newConfig).filter(([_, value]) => value !== null),
27+
Object.entries(newConfig).filter(([key, value]) => {
28+
if (key === 'codeiumApiKey') {
29+
return true;
30+
}
31+
return value !== null;
32+
}),
2833
);
2934

3035
await updateConfigServer(changeSet);

packages/web/src/lib/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ interface EditConfigRequestType {
240240
aiBaseUrl?: string;
241241
aiModel?: string;
242242
aiProvider?: AiProviderType;
243+
codeiumApiKey?: string;
243244
subscriptionEmail?: string | null;
244245
}
245246

packages/web/src/main.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Home, { loader as homeLoader } from './routes/home';
88
import Apps from './routes/apps';
99
import Session from './routes/session';
1010
import Settings from './routes/settings';
11+
import SettingsCodiumCallback from './routes/settings-codeium-callback';
1112
import Secrets from './routes/secrets';
1213
import ErrorPage from './error';
1314
import { DragAndDropSrcmdModal } from './components/drag-and-drop-srcmd-modal';
@@ -49,6 +50,11 @@ const router = createBrowserRouter([
4950
element: <Apps />,
5051
errorElement: <ErrorPage />,
5152
},
53+
{
54+
path: '/settings/codeium-callback',
55+
element: <SettingsCodiumCallback />,
56+
errorElement: <ErrorPage />,
57+
},
5258
{
5359
path: '/',
5460
element: (
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useState } from 'react';
2+
import { useNavigate, useSearchParams } from 'react-router-dom';
3+
import useEffectOnce from '@/components/use-effect-once';
4+
import { useSettings } from '@/components/use-settings';
5+
6+
export async function exchangeCodeiumAccessTokenForApiKey(accessToken: string): Promise<{
7+
api_key: string;
8+
name: string;
9+
}> {
10+
// from: https://github.com/Exafunction/codeium.vim/blob/d85a85ca7e12967db22b00fd5e9f1a095bb47c96/autoload/codeium/command.vim#L90
11+
const response = await fetch('https://api.codeium.com/register_user/', {
12+
method: 'POST',
13+
headers: { 'content-type': 'application/json' },
14+
body: JSON.stringify({ firebase_id_token: accessToken }),
15+
});
16+
17+
if (!response.ok) {
18+
console.error(response);
19+
throw new Error(`Error exchanging codeium access token for api key: ${response.status} ${await response.text()}`);
20+
}
21+
22+
return response.json();
23+
}
24+
25+
function SettingsCodeiumCallback() {
26+
const [status, setStatus] = useState<
27+
'loading' | 'already_set' | 'no_access_token' | 'token_exchange_error'
28+
>('loading');
29+
const { codeiumApiKey, updateConfig } = useSettings();
30+
31+
const navigate = useNavigate();
32+
const [queryParams] = useSearchParams();
33+
34+
useEffectOnce(() => {
35+
if (codeiumApiKey) {
36+
setStatus("already_set");
37+
return;
38+
}
39+
const accessToken = queryParams.get('access_token');
40+
if (!accessToken) {
41+
setStatus("no_access_token");
42+
return;
43+
}
44+
45+
exchangeCodeiumAccessTokenForApiKey(accessToken).then(async response => {
46+
const apiKey = response.api_key;
47+
await updateConfig({ codeiumApiKey: apiKey });
48+
49+
navigate('/settings');
50+
}).catch(err => {
51+
console.error(err);
52+
setStatus("token_exchange_error");
53+
});
54+
});
55+
56+
switch (status) {
57+
case "loading":
58+
return (
59+
<div>Loading...</div>
60+
);
61+
case "already_set":
62+
return (
63+
<div>Codeium credentials already set!</div>
64+
);
65+
case "no_access_token":
66+
return (
67+
<div>No <code>access_token</code> query parameter found!</div>
68+
);
69+
case "token_exchange_error":
70+
return (
71+
<div>Error exchanging codeium access token for api key!</div>
72+
);
73+
}
74+
};
75+
76+
export default SettingsCodeiumCallback;

packages/web/src/routes/settings.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useState } from 'react';
2-
import { CircleCheck, Loader2, CircleX } from 'lucide-react';
2+
import { CircleCheck, Loader2, CircleX, EyeIcon, EyeOffIcon } from 'lucide-react';
3+
import { Link } from 'react-router-dom';
34
import { aiHealthcheck, subscribeToMailingList } from '@/lib/server';
45
import { useSettings } from '@/components/use-settings';
56
import { AiProviderType, getDefaultModel, type CodeLanguageType } from '@srcbook/shared';
@@ -21,6 +22,7 @@ function Settings() {
2122
aiProvider,
2223
aiModel,
2324
aiBaseUrl,
25+
codeiumApiKey,
2426
openaiKey: configOpenaiKey,
2527
anthropicKey: configAnthropicKey,
2628
updateConfig: updateConfigContext,
@@ -35,6 +37,7 @@ function Settings() {
3537
const [model, setModel] = useState<string>(aiModel);
3638
const [baseUrl, setBaseUrl] = useState<string>(aiBaseUrl || '');
3739
const [email, setEmail] = useState<string>(isSubscribed ? subscriptionEmail : '');
40+
const [codeiumApiKeyVisible, setCodeiumApiKeyVisible] = useState(false);
3841

3942
const updateDefaultLanguage = (value: CodeLanguageType) => {
4043
updateConfigContext({ defaultLanguage: value });
@@ -81,6 +84,8 @@ function Settings() {
8184
}
8285
};
8386

87+
const codeiumCallbackUrl = `${window.location.href}/codeium-callback`;
88+
8489
return (
8590
<div>
8691
<h4 className="h4 mx-auto mb-6">Settings</h4>
@@ -206,6 +211,49 @@ function Settings() {
206211
)}
207212
</div>
208213
</div>
214+
<div>
215+
<h2 className="text-xl pb-2">AI Autocomplete</h2>
216+
<div className="flex flex-col">
217+
{codeiumApiKey ? (
218+
<div>
219+
<div className="opacity-70 text-sm pb-2">Codeium API Key:</div>
220+
<div className="flex justify-between items-center gap-2">
221+
<Input
222+
name="codeiumApiKey"
223+
type={codeiumApiKeyVisible ? "text" : "password"}
224+
value={codeiumApiKey}
225+
readOnly
226+
/>
227+
<Button size="icon" variant="secondary" onClick={() => setCodeiumApiKeyVisible(n => !n)}>
228+
{codeiumApiKeyVisible ? (
229+
<EyeIcon size={16} />
230+
) : (
231+
<EyeOffIcon size={16} />
232+
)}
233+
</Button>
234+
<Button variant="secondary" onClick={() => {
235+
updateConfigContext({ codeiumApiKey: null }).then(() => {
236+
toast.success('Removed Codeium api key.');
237+
}).catch(err => {
238+
console.error('Error removing Codeium api key:', err);
239+
toast.error('Error removing Codeium key!');
240+
});
241+
}}>
242+
Remove
243+
</Button>
244+
</div>
245+
</div>
246+
) : (
247+
<div>
248+
<Button asChild>
249+
<Link to={`https://www.codeium.com/profile?response_type=token&redirect_uri=${codeiumCallbackUrl}&state=a&scope=openid%20profile%20email&redirect_parameters_type=query`}>
250+
Start Codeium OAuth
251+
</Link>
252+
</Button>
253+
</div>
254+
)}
255+
</div>
256+
</div>
209257

210258
<div>
211259
<h2 className="text-xl pb-2">Get product updates</h2>

packages/web/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type SettingsType = {
1515
aiProvider: AiProviderType;
1616
aiModel: string;
1717
aiBaseUrl?: string | null;
18+
codeiumApiKey?: string | null;
1819
subscriptionEmail?: string | null;
1920
};
2021

0 commit comments

Comments
 (0)