mirror of
https://github.com/StanGirard/quivr.git
synced 2024-12-24 20:03:41 +03:00
Feat/model config (#223)
* feat(axios): add global manager * feat: add config page * feat(axios): add backendUrl overwrite * feat(brainConfig): add supabase url overwrite * feat(chat): change model config logic + add more model * feat: add openai and anthropic api key overwrite * feat(config): save config in local storage * feat(config): add reset button * feat: move vertexai to config page * ui: add brain config icon
This commit is contained in:
parent
2f6407ef9e
commit
6ff9309082
@ -93,6 +93,6 @@ const ChatMessage = forwardRef(
|
||||
}
|
||||
);
|
||||
|
||||
ChatMessage.displayName = 'ChatMessage';
|
||||
ChatMessage.displayName = "ChatMessage";
|
||||
|
||||
export default ChatMessages;
|
||||
|
@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
import axios from "axios";
|
||||
import { useBrainConfig } from "@/lib/context/BrainConfigProvider/hooks/useBrainConfig";
|
||||
import { useAxios } from "@/lib/useAxios";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MdMic, MdMicOff, MdSettings } from "react-icons/md";
|
||||
import Button from "../components/ui/Button";
|
||||
import Card from "../components/ui/Card";
|
||||
import Modal from "../components/ui/Modal";
|
||||
import PageHeading from "../components/ui/PageHeading";
|
||||
import { useSupabase } from "../supabase-provider";
|
||||
import ChatMessages from "./ChatMessages";
|
||||
@ -14,12 +15,13 @@ import { isSpeechRecognitionSupported } from "./helpers";
|
||||
export default function ChatPage() {
|
||||
const [question, setQuestion] = useState("");
|
||||
const [history, setHistory] = useState<Array<[string, string]>>([]);
|
||||
const [model, setModel] = useState("gpt-3.5-turbo");
|
||||
const [temperature, setTemperature] = useState(0);
|
||||
const [maxTokens, setMaxTokens] = useState(500);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const { session } = useSupabase();
|
||||
const { axiosInstance } = useAxios();
|
||||
const {
|
||||
config: { maxTokens, model, temperature },
|
||||
} = useBrainConfig();
|
||||
if (session === null) {
|
||||
redirect("/login");
|
||||
}
|
||||
@ -70,21 +72,14 @@ export default function ChatPage() {
|
||||
setHistory((hist) => [...hist, ["user", question]]);
|
||||
setIsPending(true);
|
||||
setIsListening(false);
|
||||
const response = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/`,
|
||||
{
|
||||
model,
|
||||
question,
|
||||
history,
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const response = await axiosInstance.post(`/chat/`, {
|
||||
model,
|
||||
question,
|
||||
history,
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
});
|
||||
setHistory(response.data.history);
|
||||
setQuestion("");
|
||||
setIsPending(false);
|
||||
@ -137,65 +132,11 @@ export default function ChatPage() {
|
||||
<MdMic className="text-2xl" />
|
||||
)}
|
||||
</Button>
|
||||
{/* Settings Button */}
|
||||
<Modal
|
||||
Trigger={
|
||||
<Button className="px-3" variant={"tertiary"}>
|
||||
<MdSettings className="text-2xl" />
|
||||
</Button>
|
||||
}
|
||||
title="Settings"
|
||||
desc="Modify your brain"
|
||||
>
|
||||
<form className="flex flex-col gap-5 py-5">
|
||||
<fieldset className="w-full flex">
|
||||
<label className="flex-1" htmlFor="model">
|
||||
Model:
|
||||
</label>
|
||||
<select
|
||||
name="model"
|
||||
id="model"
|
||||
value={model}
|
||||
className="px-5 py-2 dark:bg-gray-700 bg-gray-200 rounded-md"
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
>
|
||||
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
|
||||
<option value="gpt-4">gpt-4</option>
|
||||
<option value="vertexai">vertexai</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
<fieldset className="w-full flex">
|
||||
<label className="flex-1" htmlFor="temp">
|
||||
Temperature: {temperature}
|
||||
</label>
|
||||
<input
|
||||
name="temp"
|
||||
id="temp"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={temperature}
|
||||
onChange={(e) => setTemperature(+e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className="w-full flex">
|
||||
<label className="flex-1" htmlFor="tokens">
|
||||
Tokens: {maxTokens}
|
||||
</label>
|
||||
<input
|
||||
name="tokens"
|
||||
id="tokens"
|
||||
type="range"
|
||||
min="256"
|
||||
max="3000"
|
||||
step="1"
|
||||
value={maxTokens}
|
||||
onChange={(e) => setMaxTokens(+e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Modal>
|
||||
<Link href={"/config"}>
|
||||
<Button className="px-3" variant={"tertiary"}>
|
||||
<MdSettings className="text-2xl" />
|
||||
</Button>
|
||||
</Link>
|
||||
</form>
|
||||
</Card>
|
||||
</Card>
|
||||
|
@ -3,6 +3,7 @@ import { useSupabase } from "@/app/supabase-provider";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, FC, HTMLAttributes, ReactNode, SetStateAction } from "react";
|
||||
import { MdSettings } from "react-icons/md";
|
||||
import Button from "../ui/Button";
|
||||
import DarkModeToggle from "./DarkModeToggle";
|
||||
|
||||
@ -13,7 +14,8 @@ interface NavItemsProps extends HTMLAttributes<HTMLUListElement> {
|
||||
const NavItems: FC<NavItemsProps> = ({ className, setOpen, ...props }) => {
|
||||
const { session } = useSupabase();
|
||||
const isUserLoggedIn = session?.user !== undefined;
|
||||
const isLocal = (process.env.NEXT_PUBLIC_ENV === "local") || (session !== null);
|
||||
const isLocal = process.env.NEXT_PUBLIC_ENV === "local";
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={cn(
|
||||
@ -22,7 +24,7 @@ const NavItems: FC<NavItemsProps> = ({ className, setOpen, ...props }) => {
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{isLocal ? (
|
||||
{isLocal || isUserLoggedIn ? (
|
||||
<>
|
||||
<NavLink setOpen={setOpen} to="/upload">
|
||||
Upload
|
||||
@ -46,11 +48,21 @@ const NavItems: FC<NavItemsProps> = ({ className, setOpen, ...props }) => {
|
||||
)}
|
||||
<div className="flex sm:flex-1 sm:justify-end flex-col items-center justify-center sm:flex-row gap-5 sm:gap-2">
|
||||
{isUserLoggedIn && (
|
||||
<Link href={"/logout"}>
|
||||
<Button variant={"secondary"}>Logout</Button>
|
||||
</Link>
|
||||
<>
|
||||
<Link href={"/logout"}>
|
||||
<Button variant={"secondary"}>Logout</Button>
|
||||
</Link>
|
||||
<Link href={"/config"}>
|
||||
<Button
|
||||
variant={"tertiary"}
|
||||
className="focus:outline-none text-2xl"
|
||||
>
|
||||
<MdSettings />
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{!isLocal && (
|
||||
{!isLocal && !isUserLoggedIn && (
|
||||
<Link href={"https://try-quivr.streamlit.app"}>
|
||||
<Button variant={"secondary"}>Try Demo</Button>
|
||||
</Link>
|
||||
|
55
frontend/app/config/hooks/useConfig.ts
Normal file
55
frontend/app/config/hooks/useConfig.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useBrainConfig } from "@/lib/context/BrainConfigProvider/hooks/useBrainConfig";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useConfig = () => {
|
||||
const { config, updateConfig, resetConfig } = useBrainConfig();
|
||||
const { publish } = useToast();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
getValues,
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
} = useForm({
|
||||
defaultValues: config,
|
||||
});
|
||||
|
||||
const model = watch("model");
|
||||
const temperature = watch("temperature");
|
||||
const maxTokens = watch("maxTokens");
|
||||
|
||||
useEffect(() => {
|
||||
reset(config);
|
||||
}, [config, reset]);
|
||||
|
||||
const saveConfig = () => {
|
||||
updateConfig(getValues());
|
||||
publish({
|
||||
text: "Config saved",
|
||||
variant: "success",
|
||||
});
|
||||
};
|
||||
|
||||
const resetBrainConfig = () => {
|
||||
resetConfig();
|
||||
publish({
|
||||
text: "Config reset",
|
||||
variant: "success",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
saveConfig,
|
||||
maxTokens,
|
||||
temperature,
|
||||
isDirty,
|
||||
register,
|
||||
model,
|
||||
resetBrainConfig,
|
||||
};
|
||||
};
|
164
frontend/app/config/page.tsx
Normal file
164
frontend/app/config/page.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
"use client";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import {
|
||||
anthropicModels,
|
||||
models,
|
||||
} from "@/lib/context/BrainConfigProvider/types";
|
||||
import Button from "../components/ui/Button";
|
||||
import Field from "../components/ui/Field";
|
||||
import { useSupabase } from "../supabase-provider";
|
||||
import { useConfig } from "./hooks/useConfig";
|
||||
|
||||
export default function ExplorePage() {
|
||||
const { session } = useSupabase();
|
||||
const {
|
||||
handleSubmit,
|
||||
isDirty,
|
||||
maxTokens,
|
||||
saveConfig,
|
||||
register,
|
||||
temperature,
|
||||
model,
|
||||
resetBrainConfig,
|
||||
} = useConfig();
|
||||
|
||||
if (session === null) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen w-full flex flex-col">
|
||||
<section className="w-full outline-none pt-32 flex flex-col gap-5 items-center justify-center p-6">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl font-bold text-center">Configuration</h1>
|
||||
<h2 className="opacity-50">
|
||||
Here, you can choose your model, set your credentials...
|
||||
</h2>
|
||||
</div>
|
||||
<form
|
||||
className="flex flex-col gap-5 py-5 w-1/2"
|
||||
onSubmit={handleSubmit(saveConfig)}
|
||||
>
|
||||
<div className="border-b border-gray-300 mt-8 mb-8">
|
||||
<p className="text-center text-gray-600 uppercase tracking-wide font-semibold">
|
||||
Model config
|
||||
</p>
|
||||
</div>
|
||||
<Field
|
||||
type="text"
|
||||
placeholder="Open AI Key"
|
||||
className="w-full"
|
||||
label="Open AI Key"
|
||||
{...register("openAiKey")}
|
||||
/>
|
||||
<fieldset className="w-full flex flex flex-col">
|
||||
<label className="flex-1 text-sm" htmlFor="model">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
id="model"
|
||||
{...register("model")}
|
||||
className="px-5 py-2 dark:bg-gray-700 bg-gray-200 rounded-md"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option value={model} key={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</fieldset>
|
||||
{(anthropicModels as readonly string[]).includes(model) && (
|
||||
<Field
|
||||
type="text"
|
||||
placeholder="Anthropic API Key"
|
||||
className="w-full"
|
||||
label="Anthropic API Key"
|
||||
{...register("anthropicKey")}
|
||||
/>
|
||||
)}
|
||||
<fieldset className="w-full flex">
|
||||
<label className="flex-1" htmlFor="temp">
|
||||
Temperature: {temperature}
|
||||
</label>
|
||||
<input
|
||||
id="temp"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={temperature}
|
||||
{...register("temperature")}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className="w-full flex">
|
||||
<label className="flex-1" htmlFor="tokens">
|
||||
Tokens: {maxTokens}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="256"
|
||||
max="3000"
|
||||
step="1"
|
||||
value={maxTokens}
|
||||
{...register("maxTokens")}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="border-b border-gray-300 mt-8 mb-8">
|
||||
<p className="text-center text-gray-600 uppercase tracking-wide font-semibold">
|
||||
Backend config
|
||||
</p>
|
||||
</div>
|
||||
<Field
|
||||
type="text"
|
||||
placeholder="Backend URL"
|
||||
className="w-full"
|
||||
label="Backend URL"
|
||||
{...register("backendUrl")}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
placeholder="Supabase URL"
|
||||
className="w-full"
|
||||
label="Supabase URL"
|
||||
{...register("supabaseUrl")}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
placeholder="Supabase key"
|
||||
className="w-full"
|
||||
label="Supabase key"
|
||||
{...register("supabaseKey")}
|
||||
/>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked
|
||||
name="keepLocal"
|
||||
onChange={() => alert("Coming soon")}
|
||||
className="form-checkbox h-5 w-5 text-indigo-600 rounded focus:ring-2 focus:ring-indigo-400"
|
||||
/>
|
||||
<span className="ml-2 text-gray-700">Keep in local</span>
|
||||
</label>
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
variant="danger"
|
||||
className="self-end"
|
||||
type="button"
|
||||
onClick={resetBrainConfig}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!isDirty}
|
||||
variant="secondary"
|
||||
className="self-end"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
@ -1,27 +1,35 @@
|
||||
import axios from "axios";
|
||||
import { useAxios } from "@/lib/useAxios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSupabase } from "../../supabase-provider";
|
||||
|
||||
interface DocumentDataProps {
|
||||
documentName: string;
|
||||
}
|
||||
|
||||
const DocumentData = async ({ documentName }: DocumentDataProps) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type DocumentDetails = any;
|
||||
//TODO: review this component logic, types and purposes
|
||||
|
||||
const DocumentData = ({ documentName }: DocumentDataProps): JSX.Element => {
|
||||
const { session } = useSupabase();
|
||||
const { axiosInstance } = useAxios();
|
||||
|
||||
const [documents, setDocuments] = useState<DocumentDetails[]>([]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("User session not found");
|
||||
}
|
||||
|
||||
const res = await axios.get(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${documentName}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
// TODO: review the logic of this part and try to use unknown instead of any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const documents = res.data.documents as any[];
|
||||
useEffect(() => {
|
||||
const fetchDocuments = async () => {
|
||||
const res = await axiosInstance.get<{ documents: DocumentDetails[] }>(
|
||||
`/explore/${documentName}`
|
||||
);
|
||||
setDocuments(res.data.documents);
|
||||
};
|
||||
fetchDocuments();
|
||||
}, [axiosInstance, documentName]);
|
||||
|
||||
return (
|
||||
<div className="prose">
|
||||
<p>No. of documents: {documents.length}</p>
|
||||
|
@ -1,13 +1,11 @@
|
||||
"use client";
|
||||
import Spinner from "@/app/components/ui/Spinner";
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
import axios from "axios";
|
||||
import { useAxios } from "@/lib/useAxios";
|
||||
import {
|
||||
Dispatch,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
Suspense,
|
||||
forwardRef,
|
||||
useState,
|
||||
} from "react";
|
||||
@ -27,6 +25,8 @@ const DocumentItem = forwardRef(
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const { publish } = useToast();
|
||||
const { session } = useSupabase();
|
||||
const { axiosInstance } = useAxios();
|
||||
|
||||
if (!session) {
|
||||
throw new Error("User session not found");
|
||||
}
|
||||
@ -34,14 +34,7 @@ const DocumentItem = forwardRef(
|
||||
const deleteDocument = async (name: string) => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await axios.delete(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${name}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
await axiosInstance.delete(`/explore/${name}`);
|
||||
setDocuments((docs) => docs.filter((doc) => doc.name !== name)); // Optimistic update
|
||||
publish({ variant: "success", text: `${name} deleted.` });
|
||||
} catch (error) {
|
||||
@ -61,19 +54,14 @@ const DocumentItem = forwardRef(
|
||||
>
|
||||
<p className="text-lg leading-tight max-w-sm">{document.name}</p>
|
||||
<div className="flex gap-2 self-end">
|
||||
{/* VIEW MODAL */}
|
||||
<Modal
|
||||
title={document.name}
|
||||
desc={""}
|
||||
Trigger={<Button className="">View</Button>}
|
||||
>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{/* @ts-expect-error TODO: check if DocumentData component can be sync */}
|
||||
<DocumentData documentName={document.name} />
|
||||
</Suspense>
|
||||
<DocumentData documentName={document.name} />
|
||||
</Modal>
|
||||
|
||||
{/* DELETE MODAL */}
|
||||
<Modal
|
||||
title={"Confirm"}
|
||||
desc={`Do you really want to delete?`}
|
||||
|
@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import axios from "axios";
|
||||
import { useAxios } from "@/lib/useAxios";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@ -14,6 +14,8 @@ export default function ExplorePage() {
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
const [isPending, setIsPending] = useState(true);
|
||||
const { session } = useSupabase();
|
||||
const { axiosInstance } = useAxios();
|
||||
|
||||
if (session === null) {
|
||||
redirect("/login");
|
||||
}
|
||||
@ -25,13 +27,8 @@ export default function ExplorePage() {
|
||||
console.log(
|
||||
`Fetching documents from ${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`
|
||||
);
|
||||
const response = await axios.get<{ documents: Document[] }>(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
const response = await axiosInstance.get<{ documents: Document[] }>(
|
||||
"/explore"
|
||||
);
|
||||
setDocuments(response.data.documents);
|
||||
} catch (error) {
|
||||
|
@ -2,6 +2,7 @@ import { createServerComponentSupabaseClient } from "@supabase/auth-helpers-next
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { Inter } from "next/font/google";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { BrainConfigProvider } from "../lib/context/BrainConfigProvider/brain-config-provider";
|
||||
import NavBar from "./components/NavBar";
|
||||
import { ToastProvider } from "./components/ui/Toast";
|
||||
import "./globals.css";
|
||||
@ -36,8 +37,10 @@ export default async function RootLayout({
|
||||
>
|
||||
<ToastProvider>
|
||||
<SupabaseProvider session={session}>
|
||||
<NavBar />
|
||||
{children}
|
||||
<BrainConfigProvider>
|
||||
<NavBar />
|
||||
{children}
|
||||
</BrainConfigProvider>
|
||||
</SupabaseProvider>
|
||||
</ToastProvider>
|
||||
<Analytics />
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
import axios from "axios";
|
||||
import { useAxios } from "@/lib/useAxios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { isValidUrl } from "../helpers/isValidUrl";
|
||||
@ -10,6 +10,8 @@ export const useCrawler = () => {
|
||||
const urlInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const { session } = useSupabase();
|
||||
const { publish } = useToast();
|
||||
const { axiosInstance } = useAxios();
|
||||
|
||||
if (session === null) {
|
||||
redirect("/login");
|
||||
}
|
||||
@ -39,15 +41,7 @@ export const useCrawler = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/crawl`,
|
||||
config,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await axiosInstance.post(`/crawl`, config);
|
||||
|
||||
publish({
|
||||
variant: response.data.type,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
import axios from "axios";
|
||||
import { useAxios } from "@/lib/useAxios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import { FileRejection, useDropzone } from "react-dropzone";
|
||||
@ -12,6 +12,8 @@ export const useFileUploader = () => {
|
||||
const [pendingFileIndex, setPendingFileIndex] = useState<number>(0);
|
||||
const { session } = useSupabase();
|
||||
|
||||
const { axiosInstance } = useAxios();
|
||||
|
||||
if (session === null) {
|
||||
redirect("/login");
|
||||
}
|
||||
@ -21,15 +23,7 @@ export const useFileUploader = () => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await axiosInstance.post(`/upload`, formData);
|
||||
|
||||
publish({
|
||||
variant: response.data.type,
|
||||
|
@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { setEmptyStringsUndefined } from "@/lib/helpers/setEmptyStringsUndefined";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import {
|
||||
getBrainConfigFromLocalStorage,
|
||||
saveBrainConfigInLocalStorage,
|
||||
} from "./helpers/brainConfigLocalStorage";
|
||||
import { BrainConfig, ConfigContext } from "./types";
|
||||
|
||||
export const BrainConfigContext = createContext<ConfigContext | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const defaultBrainConfig: BrainConfig = {
|
||||
model: "gpt-3.5-turbo",
|
||||
temperature: 0,
|
||||
maxTokens: 500,
|
||||
keepLocal: true,
|
||||
anthropicKey: undefined,
|
||||
backendUrl: undefined,
|
||||
openAiKey: undefined,
|
||||
supabaseKey: undefined,
|
||||
supabaseUrl: undefined,
|
||||
};
|
||||
|
||||
export const BrainConfigProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const [brainConfig, setBrainConfig] =
|
||||
useState<BrainConfig>(defaultBrainConfig);
|
||||
|
||||
const updateConfig = (newConfig: Partial<BrainConfig>) => {
|
||||
setBrainConfig((config) => {
|
||||
const updatedConfig: BrainConfig = {
|
||||
...config,
|
||||
...setEmptyStringsUndefined(newConfig),
|
||||
};
|
||||
saveBrainConfigInLocalStorage(updatedConfig);
|
||||
|
||||
return updatedConfig;
|
||||
});
|
||||
};
|
||||
|
||||
const resetConfig = () => {
|
||||
updateConfig(defaultBrainConfig);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setBrainConfig(getBrainConfigFromLocalStorage() ?? defaultBrainConfig);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BrainConfigContext.Provider
|
||||
value={{ config: brainConfig, updateConfig, resetConfig }}
|
||||
>
|
||||
{children}
|
||||
</BrainConfigContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { BrainConfig } from "../types";
|
||||
|
||||
const BRAIN_CONFIG_LOCAL_STORAGE_KEY = "userBrainConfig";
|
||||
|
||||
export const saveBrainConfigInLocalStorage = (updatedConfig: BrainConfig) => {
|
||||
localStorage.setItem(
|
||||
BRAIN_CONFIG_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(updatedConfig)
|
||||
);
|
||||
};
|
||||
export const getBrainConfigFromLocalStorage = (): BrainConfig | undefined => {
|
||||
const persistedBrainConfig = localStorage.getItem(
|
||||
BRAIN_CONFIG_LOCAL_STORAGE_KEY
|
||||
);
|
||||
if (persistedBrainConfig === null) return;
|
||||
return JSON.parse(persistedBrainConfig);
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
import { useContext } from "react";
|
||||
import { BrainConfigContext } from "../brain-config-provider";
|
||||
|
||||
export const useBrainConfig = () => {
|
||||
const context = useContext(BrainConfigContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useConfig must be used inside SupabaseProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
38
frontend/lib/context/BrainConfigProvider/types.ts
Normal file
38
frontend/lib/context/BrainConfigProvider/types.ts
Normal file
@ -0,0 +1,38 @@
|
||||
export type BrainConfig = {
|
||||
model: Model;
|
||||
temperature: number;
|
||||
maxTokens: number;
|
||||
keepLocal: boolean;
|
||||
backendUrl?: string;
|
||||
openAiKey?: string;
|
||||
anthropicKey?: string;
|
||||
supabaseUrl?: string;
|
||||
supabaseKey?: string;
|
||||
};
|
||||
|
||||
type OptionalConfig = { [K in keyof BrainConfig]?: BrainConfig[K] | undefined };
|
||||
|
||||
export type ConfigContext = {
|
||||
config: BrainConfig;
|
||||
updateConfig: (config: OptionalConfig) => void;
|
||||
resetConfig: () => void;
|
||||
};
|
||||
|
||||
export const openAiModels = ["gpt-3.5-turbo", "gpt-4"] as const;
|
||||
|
||||
export const anthropicModels = [
|
||||
"claude-v1",
|
||||
"claude-v1.3",
|
||||
"claude-instant-v1-100k",
|
||||
"claude-instant-v1.1-100k",
|
||||
] as const;
|
||||
|
||||
export const googleModels = ["vertexai"] as const;
|
||||
|
||||
export const models = [
|
||||
...openAiModels,
|
||||
...anthropicModels,
|
||||
...googleModels,
|
||||
] as const;
|
||||
|
||||
export type Model = (typeof models)[number];
|
11
frontend/lib/helpers/setEmptyStringsUndefined.ts
Normal file
11
frontend/lib/helpers/setEmptyStringsUndefined.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const setEmptyStringsUndefined = (
|
||||
obj: Record<string, unknown>
|
||||
): Record<string, unknown> => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (obj[key] === "") {
|
||||
obj[key] = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
};
|
31
frontend/lib/useAxios.ts
Normal file
31
frontend/lib/useAxios.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import axios from "axios";
|
||||
import { useBrainConfig } from "./context/BrainConfigProvider/hooks/useBrainConfig";
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: `${process.env.NEXT_PUBLIC_BACKEND_URL}`,
|
||||
});
|
||||
|
||||
export const useAxios = () => {
|
||||
const { session } = useSupabase();
|
||||
const {
|
||||
config: { backendUrl },
|
||||
} = useBrainConfig();
|
||||
axiosInstance.interceptors.request.clear();
|
||||
axiosInstance.interceptors.request.use(
|
||||
async (config) => {
|
||||
config.headers["Authorization"] = "Bearer " + session?.access_token;
|
||||
config.baseURL = backendUrl ?? config.baseURL;
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
console.error({ error });
|
||||
void Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
axiosInstance,
|
||||
};
|
||||
};
|
@ -37,6 +37,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.44.3",
|
||||
"react-markdown": "^8.0.7",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
|
@ -3199,6 +3199,11 @@ react-dropzone@^14.2.3:
|
||||
file-selector "^0.6.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-hook-form@^7.44.3:
|
||||
version "7.44.3"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.44.3.tgz#a99e560c6ef2b668db1daaebc4f98267331b6828"
|
||||
integrity sha512-/tHId6p2ViAka1wECMw8FEPn/oz/w226zehHrJyQ1oIzCBNMIJCaj6ZkQcv+MjDxYh9MWR7RQic7Qqwe4a5nkw==
|
||||
|
||||
react-icons@^4.8.0:
|
||||
version "4.8.0"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445"
|
||||
|
Loading…
Reference in New Issue
Block a user