mirror of
https://github.com/StanGirard/quivr.git
synced 2024-09-11 13:15:41 +03:00
feat: add user openai api key input
This commit is contained in:
parent
2972092900
commit
73e7e093c6
@ -5,7 +5,12 @@ import Button from "@/lib/components/ui/Button";
|
||||
import { useApiKeyConfig } from "./hooks/useApiKeyConfig";
|
||||
|
||||
export const ApiKeyConfig = (): JSX.Element => {
|
||||
const { apiKey, handleCopyClick, handleCreateClick } = useApiKeyConfig();
|
||||
const {
|
||||
apiKey,
|
||||
handleCopyClick,
|
||||
|
||||
handleCreateClick,
|
||||
} = useApiKeyConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -6,6 +6,7 @@ import { useEventTracking } from "@/services/analytics/useEventTracking";
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useApiKeyConfig = () => {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [openAiApiKey, setOpenAiApiKey] = useState("");
|
||||
const { track } = useEventTracking();
|
||||
const { createApiKey } = useAuthApi();
|
||||
|
||||
@ -38,5 +39,7 @@ export const useApiKeyConfig = () => {
|
||||
handleCreateClick,
|
||||
apiKey,
|
||||
handleCopyClick,
|
||||
openAiApiKey,
|
||||
setOpenAiApiKey,
|
||||
};
|
||||
};
|
||||
|
@ -1,12 +1,25 @@
|
||||
/* eslint-disable max-lines */
|
||||
"use client";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { Divider } from "@/lib/components/ui/Divider";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
|
||||
import { useApiKeyConfig } from "./hooks/useApiKeyConfig";
|
||||
|
||||
export const ApiKeyConfig = (): JSX.Element => {
|
||||
const { apiKey, handleCopyClick, handleCreateClick } = useApiKeyConfig();
|
||||
const {
|
||||
apiKey,
|
||||
handleCopyClick,
|
||||
handleCreateClick,
|
||||
openAiApiKey,
|
||||
setOpenAiApiKey,
|
||||
changeOpenAiApiKey,
|
||||
changeOpenAiApiKeyRequestPending,
|
||||
userIdentity,
|
||||
removeOpenAiApiKey,
|
||||
hasOpenAiApiKey,
|
||||
} = useApiKeyConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -36,6 +49,42 @@ export const ApiKeyConfig = (): JSX.Element => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider text="OpenAI Key" className="mt-4 mb-4" />
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void changeOpenAiApiKey();
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
name="openAiApiKey"
|
||||
placeholder="Open AI Key"
|
||||
className="w-full"
|
||||
value={openAiApiKey ?? ""}
|
||||
data-testid="open-ai-api-key"
|
||||
onChange={(e) => setOpenAiApiKey(e.target.value)}
|
||||
/>
|
||||
<div className="mt-4 flex flex-row justify-between">
|
||||
{hasOpenAiApiKey && (
|
||||
<Button
|
||||
isLoading={changeOpenAiApiKeyRequestPending}
|
||||
variant="secondary"
|
||||
onClick={() => void removeOpenAiApiKey()}
|
||||
>
|
||||
Remove Key
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
data-testid="save-open-ai-api-key"
|
||||
isLoading={changeOpenAiApiKeyRequestPending}
|
||||
disabled={openAiApiKey === userIdentity?.openai_api_key}
|
||||
>
|
||||
Save Key
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -22,8 +22,11 @@ describe("ApiKeyConfig", () => {
|
||||
});
|
||||
|
||||
it("should render ApiConfig Component", () => {
|
||||
const { getByText } = render(<ApiKeyConfig />);
|
||||
const { getByText, getByTestId } = render(<ApiKeyConfig />);
|
||||
expect(getByText("API Key Config")).toBeDefined();
|
||||
expect(getByText("OpenAI Key")).toBeDefined();
|
||||
expect(getByTestId("open-ai-api-key")).toBeDefined();
|
||||
expect(getByTestId("save-open-ai-api-key")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders 'Create New Key' button when apiKey is empty", () => {
|
||||
|
@ -6,6 +6,12 @@ import { useApiKeyConfig } from "../useApiKeyConfig";
|
||||
const createApiKeyMock = vi.fn(() => "dummyApiKey");
|
||||
const trackMock = vi.fn((props: unknown) => ({ props }));
|
||||
|
||||
const mockUseSupabase = vi.fn(() => ({
|
||||
session: {
|
||||
user: {},
|
||||
},
|
||||
}));
|
||||
|
||||
const useAuthApiMock = vi.fn(() => ({
|
||||
createApiKey: () => createApiKeyMock(),
|
||||
}));
|
||||
@ -20,6 +26,31 @@ vi.mock("@/lib/api/auth/useAuthApi", () => ({
|
||||
vi.mock("@/services/analytics/useEventTracking", () => ({
|
||||
useEventTracking: () => useEventTrackingMock(),
|
||||
}));
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => mockUseSupabase(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/hooks", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/lib/hooks")>(
|
||||
"@/lib/hooks"
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useAxios: () => ({
|
||||
axiosInstance: {
|
||||
put: vi.fn(() => ({})),
|
||||
get: vi.fn(() => ({})),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/context/BrainConfigProvider", () => ({
|
||||
useBrainConfig: () => ({
|
||||
config: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("useApiKeyConfig", () => {
|
||||
afterEach(() => {
|
||||
|
@ -1,13 +1,32 @@
|
||||
import { useState } from "react";
|
||||
/* eslint-disable max-lines */
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useAuthApi } from "@/lib/api/auth/useAuthApi";
|
||||
import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||
import { UserIdentity } from "@/lib/api/user/user";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
import { useEventTracking } from "@/services/analytics/useEventTracking";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useApiKeyConfig = () => {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [openAiApiKey, setOpenAiApiKey] = useState<string | null>();
|
||||
const [
|
||||
changeOpenAiApiKeyRequestPending,
|
||||
setChangeOpenAiApiKeyRequestPending,
|
||||
] = useState(false);
|
||||
const { updateUserIdentity, getUserIdentity } = useUserApi();
|
||||
const { track } = useEventTracking();
|
||||
const { createApiKey } = useAuthApi();
|
||||
const { publish } = useToast();
|
||||
const [userIdentity, setUserIdentity] = useState<UserIdentity>();
|
||||
|
||||
const fetchUserIdentity = async () => {
|
||||
setUserIdentity(await getUserIdentity());
|
||||
};
|
||||
useEffect(() => {
|
||||
void fetchUserIdentity();
|
||||
}, []);
|
||||
|
||||
const handleCreateClick = async () => {
|
||||
try {
|
||||
@ -34,9 +53,65 @@ export const useApiKeyConfig = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const changeOpenAiApiKey = async () => {
|
||||
try {
|
||||
setChangeOpenAiApiKeyRequestPending(true);
|
||||
await updateUserIdentity({
|
||||
openai_api_key: openAiApiKey,
|
||||
});
|
||||
void fetchUserIdentity();
|
||||
publish({
|
||||
variant: "success",
|
||||
text: "OpenAI API Key updated",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setChangeOpenAiApiKeyRequestPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeOpenAiApiKey = async () => {
|
||||
try {
|
||||
setChangeOpenAiApiKeyRequestPending(true);
|
||||
await updateUserIdentity({
|
||||
openai_api_key: null,
|
||||
});
|
||||
|
||||
publish({
|
||||
variant: "success",
|
||||
text: "OpenAI API Key removed",
|
||||
});
|
||||
|
||||
void fetchUserIdentity();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setChangeOpenAiApiKeyRequestPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userIdentity?.openai_api_key !== undefined) {
|
||||
setOpenAiApiKey(userIdentity.openai_api_key);
|
||||
}
|
||||
}, [userIdentity]);
|
||||
|
||||
const hasOpenAiApiKey =
|
||||
userIdentity?.openai_api_key !== null &&
|
||||
userIdentity?.openai_api_key !== undefined &&
|
||||
userIdentity.openai_api_key !== "";
|
||||
|
||||
return {
|
||||
handleCreateClick,
|
||||
apiKey,
|
||||
handleCopyClick,
|
||||
openAiApiKey,
|
||||
setOpenAiApiKey,
|
||||
changeOpenAiApiKey,
|
||||
changeOpenAiApiKeyRequestPending,
|
||||
userIdentity,
|
||||
removeOpenAiApiKey,
|
||||
hasOpenAiApiKey,
|
||||
};
|
||||
};
|
||||
|
48
frontend/lib/api/user/__tests__/useUserApi.test.ts
Normal file
48
frontend/lib/api/user/__tests__/useUserApi.test.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useUserApi } from "../useUserApi";
|
||||
import { UserIdentityUpdatableProperties } from "../user";
|
||||
|
||||
const axiosPutMock = vi.fn(() => ({}));
|
||||
const axiosGetMock = vi.fn(() => ({}));
|
||||
|
||||
vi.mock("@/lib/hooks", () => ({
|
||||
useAxios: () => ({
|
||||
axiosInstance: {
|
||||
put: axiosPutMock,
|
||||
get: axiosGetMock,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("useUserApi", () => {
|
||||
it("should call updateUserIdentity with the correct parameters", async () => {
|
||||
const {
|
||||
result: {
|
||||
current: { updateUserIdentity },
|
||||
},
|
||||
} = renderHook(() => useUserApi());
|
||||
const userUpdatableProperties: UserIdentityUpdatableProperties = {
|
||||
openai_api_key: "sk-xxx",
|
||||
};
|
||||
await updateUserIdentity(userUpdatableProperties);
|
||||
|
||||
expect(axiosPutMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosPutMock).toHaveBeenCalledWith(
|
||||
`/user/identity`,
|
||||
userUpdatableProperties
|
||||
);
|
||||
});
|
||||
it("should call getUserIdentity with the correct parameters", async () => {
|
||||
const {
|
||||
result: {
|
||||
current: { getUserIdentity },
|
||||
},
|
||||
} = renderHook(() => useUserApi());
|
||||
await getUserIdentity();
|
||||
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosGetMock).toHaveBeenCalledWith(`/user/identity`);
|
||||
});
|
||||
});
|
19
frontend/lib/api/user/useUserApi.ts
Normal file
19
frontend/lib/api/user/useUserApi.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useAxios } from "@/lib/hooks";
|
||||
|
||||
import {
|
||||
getUserIdentity,
|
||||
updateUserIdentity,
|
||||
UserIdentityUpdatableProperties,
|
||||
} from "./user";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useUserApi = () => {
|
||||
const { axiosInstance } = useAxios();
|
||||
|
||||
return {
|
||||
updateUserIdentity: async (
|
||||
userIdentityUpdatableProperties: UserIdentityUpdatableProperties
|
||||
) => updateUserIdentity(userIdentityUpdatableProperties, axiosInstance),
|
||||
getUserIdentity: async () => getUserIdentity(axiosInstance),
|
||||
};
|
||||
};
|
25
frontend/lib/api/user/user.ts
Normal file
25
frontend/lib/api/user/user.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { AxiosInstance } from "axios";
|
||||
import { UUID } from "crypto";
|
||||
|
||||
export type UserIdentityUpdatableProperties = {
|
||||
openai_api_key?: string | null;
|
||||
};
|
||||
|
||||
export type UserIdentity = {
|
||||
openai_api_key?: string | null;
|
||||
user_id: UUID;
|
||||
};
|
||||
|
||||
export const updateUserIdentity = async (
|
||||
userUpdatableProperties: UserIdentityUpdatableProperties,
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<UserIdentity> =>
|
||||
axiosInstance.put(`/user/identity`, userUpdatableProperties);
|
||||
|
||||
export const getUserIdentity = async (
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<UserIdentity> => {
|
||||
const { data } = await axiosInstance.get<UserIdentity>(`/user/identity`);
|
||||
|
||||
return data;
|
||||
};
|
Loading…
Reference in New Issue
Block a user