mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-23 12:31:30 +02:00
Prompt user for OpenAI key
This commit is contained in:
76
web/src/app/admin/keys/openai/page.tsx
Normal file
76
web/src/app/admin/keys/openai/page.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { LoadingAnimation } from "@/components/Loading";
|
||||
import { KeyIcon, TrashIcon } from "@/components/icons/icons";
|
||||
import { ApiKeyForm } from "@/components/openai/ApiKeyForm";
|
||||
import { OPENAI_API_KEY_URL } from "@/components/openai/constants";
|
||||
import { fetcher } from "@/lib/fetcher";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
const ExistingKeys = () => {
|
||||
const { data, isLoading, error } = useSWR<{ api_key: string }>(
|
||||
OPENAI_API_KEY_URL,
|
||||
fetcher
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingAnimation text="Loading" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-600">Error loading existing keys</div>;
|
||||
}
|
||||
|
||||
if (!data?.api_key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-2">Existing Key</h2>
|
||||
<div className="flex mb-1">
|
||||
<p className="text-sm italic my-auto">sk- ...{data?.api_key}</p>
|
||||
<button
|
||||
className="ml-1 my-auto hover:bg-gray-700 rounded-full p-1"
|
||||
onClick={async () => {
|
||||
await fetch(OPENAI_API_KEY_URL, {
|
||||
method: "DELETE",
|
||||
});
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
|
||||
<KeyIcon size="32" />
|
||||
<h1 className="text-3xl font-bold pl-2">OpenAI Keys</h1>
|
||||
</div>
|
||||
|
||||
<ExistingKeys />
|
||||
|
||||
<h2 className="text-lg font-bold mb-2">Update Key</h2>
|
||||
<p className="text-sm mb-2">
|
||||
Specify an OpenAI API key and click the "Submit" button.
|
||||
</p>
|
||||
<div className="border rounded-md border-gray-700 p-3">
|
||||
<ApiKeyForm
|
||||
handleResponse={(response) => {
|
||||
if (response.ok) {
|
||||
mutate(OPENAI_API_KEY_URL);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
@@ -6,6 +6,7 @@ import {
|
||||
GlobeIcon,
|
||||
GoogleDriveIcon,
|
||||
SlackIcon,
|
||||
KeyIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { DISABLE_AUTH } from "@/lib/constants";
|
||||
import { getCurrentUserSS } from "@/lib/userSS";
|
||||
@@ -89,6 +90,20 @@ export default async function AdminLayout({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Keys",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<KeyIcon size="16" />
|
||||
<div className="ml-1">OpenAI</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/keys/openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="px-12 min-h-screen bg-gray-900 text-gray-100 w-full">
|
||||
|
@@ -19,7 +19,7 @@ export default async function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${inter.variable} font-sans bg-gray-900`}>
|
||||
<body className={`${inter.variable} font-sans bg-gray-900 text-gray-100`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -4,6 +4,7 @@ import { getCurrentUserSS } from "@/lib/userSS";
|
||||
import { redirect } from "next/navigation";
|
||||
import { DISABLE_AUTH } from "@/lib/constants";
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { ApiKeyModal } from "@/components/openai/ApiKeyModal";
|
||||
|
||||
export default async function Home() {
|
||||
let user = null;
|
||||
@@ -13,12 +14,14 @@ export default async function Home() {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header user={user} />
|
||||
<div className="m-3">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<ApiKeyModal />
|
||||
<div className="px-24 pt-10 flex flex-col items-center min-h-screen bg-gray-900 text-gray-100">
|
||||
<div className="max-w-[800px] w-full">
|
||||
<SearchSection />
|
||||
|
@@ -3,16 +3,21 @@ import { ErrorMessage, Field } from "formik";
|
||||
interface TextFormFieldProps {
|
||||
name: string;
|
||||
label: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export const TextFormField = ({ name, label }: TextFormFieldProps) => {
|
||||
export const TextFormField = ({
|
||||
name,
|
||||
label,
|
||||
type = "text",
|
||||
}: TextFormFieldProps) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<label htmlFor={name} className="block mb-1">
|
||||
{label}
|
||||
</label>
|
||||
<Field
|
||||
type="text"
|
||||
type={type}
|
||||
name={name}
|
||||
id={name}
|
||||
className="border bg-slate-700 text-gray-200 border-gray-300 rounded w-full py-2 px-3"
|
||||
|
@@ -7,6 +7,8 @@ import {
|
||||
GithubLogo,
|
||||
GoogleDriveLogo,
|
||||
Notebook,
|
||||
Key,
|
||||
Trash,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
interface IconProps {
|
||||
@@ -23,6 +25,20 @@ export const NotebookIcon = ({
|
||||
return <Notebook size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const KeyIcon = ({
|
||||
size = "16",
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return <Key size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const TrashIcon = ({
|
||||
size = "16",
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return <Trash size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const GlobeIcon = ({
|
||||
size = "16",
|
||||
className = defaultTailwindCSS,
|
||||
|
86
web/src/components/openai/ApiKeyForm.tsx
Normal file
86
web/src/components/openai/ApiKeyForm.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Form, Formik } from "formik";
|
||||
import { Popup } from "../admin/connectors/Popup";
|
||||
import { useState } from "react";
|
||||
import { TextFormField } from "../admin/connectors/Field";
|
||||
import { OPENAI_API_KEY_URL } from "./constants";
|
||||
import { LoadingAnimation } from "../Loading";
|
||||
|
||||
interface Props {
|
||||
handleResponse?: (response: Response) => void;
|
||||
}
|
||||
|
||||
export const ApiKeyForm = ({ handleResponse }: Props) => {
|
||||
const [popup, setPopup] = useState<{
|
||||
message: string;
|
||||
type: "success" | "error";
|
||||
} | null>(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{popup && <Popup message={popup.message} type={popup.type} />}
|
||||
<Formik
|
||||
initialValues={{ apiKey: "" }}
|
||||
onSubmit={async ({ apiKey }, formikHelpers) => {
|
||||
const response = await fetch(OPENAI_API_KEY_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ api_key: apiKey }),
|
||||
});
|
||||
if (handleResponse) {
|
||||
handleResponse(response);
|
||||
}
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: "Updated API key!",
|
||||
type: "success",
|
||||
});
|
||||
formikHelpers.resetForm();
|
||||
} else {
|
||||
const body = await response.json();
|
||||
if (body.detail) {
|
||||
setPopup({ message: body.detail, type: "error" });
|
||||
} else {
|
||||
setPopup({
|
||||
message:
|
||||
"Unable to set API key. Check if the provided key is valid.",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
setPopup(null);
|
||||
}, 3000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
isSubmitting ? (
|
||||
<LoadingAnimation text="Validating API key" />
|
||||
) : (
|
||||
<Form>
|
||||
<TextFormField
|
||||
name="apiKey"
|
||||
type="password"
|
||||
label="OpenAI API Key:"
|
||||
/>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={
|
||||
"bg-slate-500 hover:bg-slate-700 text-white " +
|
||||
"font-bold py-2 px-4 rounded focus:outline-none " +
|
||||
"focus:shadow-outline w-full mx-auto"
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
</div>
|
||||
);
|
||||
};
|
55
web/src/components/openai/ApiKeyModal.tsx
Normal file
55
web/src/components/openai/ApiKeyModal.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { ApiKeyForm } from "./ApiKeyForm";
|
||||
|
||||
export const ApiKeyModal = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/openai-api-key/validate", {
|
||||
method: "HEAD",
|
||||
}).then((res) => {
|
||||
// show popup if either the API key is not set or the API key is invalid
|
||||
if (!res.ok && (res.status === 404 || res.status === 400)) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<p className="mb-2.5 font-bold">
|
||||
Can't find a valid registered OpenAI API key. Please provide
|
||||
one to be able to ask questions! Or if you'd rather just look
|
||||
around for now,{" "}
|
||||
<strong
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-blue-300 cursor-pointer"
|
||||
>
|
||||
skip this step
|
||||
</strong>
|
||||
.
|
||||
</p>
|
||||
<ApiKeyForm
|
||||
handleResponse={(response) => {
|
||||
if (response.ok) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
1
web/src/components/openai/constants.ts
Normal file
1
web/src/components/openai/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const OPENAI_API_KEY_URL = "/api/admin/openai-api-key";
|
Reference in New Issue
Block a user