mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-08-17 02:10:31 +03:00
fix: bugs (#818)
* feat: add chat config modal * feat: save chat config in localStorage * feat: remove <ConfigPage/> * feat: overwrite chat config with brain * fix(SettingsPage): upload payload keys * fix: update default brain marker logic * feat: set new created brain as current selected brain
This commit is contained in:
parent
130a3e0938
commit
edcbb30e97
@ -2,11 +2,12 @@ from typing import Any, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from logger import get_logger
|
||||
from models.settings import BrainRateLimiting, CommonsDep, common_dependencies
|
||||
from models.users import User
|
||||
from pydantic import BaseModel
|
||||
from utils.vectors import get_unique_files_from_vector_ids
|
||||
|
||||
from models.settings import BrainRateLimiting, CommonsDep, common_dependencies
|
||||
from models.users import User
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@ -111,7 +112,9 @@ class Brain(BaseModel):
|
||||
.filter("brain_id", "eq", self.id)
|
||||
.execute()
|
||||
)
|
||||
return response.data
|
||||
if response.data == []:
|
||||
return None
|
||||
return response.data[0]
|
||||
|
||||
def delete_brain(self, user_id):
|
||||
results = (
|
||||
|
@ -20,7 +20,9 @@ def resend_invitation_email(
|
||||
brain_url = get_brain_url(origin, brain_subscription.brain_id)
|
||||
|
||||
invitation_brain_client = Brain(id=brain_subscription.brain_id)
|
||||
invitation_brain = invitation_brain_client.get_brain_details()[0]
|
||||
invitation_brain = invitation_brain_client.get_brain_details()
|
||||
if invitation_brain is None:
|
||||
raise Exception("Brain not found")
|
||||
brain_name = invitation_brain["name"]
|
||||
|
||||
html_body = f"""
|
||||
|
@ -73,15 +73,16 @@ async def get_brain_endpoint(
|
||||
history, which includes the brain messages exchanged in the brain.
|
||||
"""
|
||||
brain = Brain(id=brain_id)
|
||||
brains = brain.get_brain_details()
|
||||
if len(brains) > 0:
|
||||
return brains[0]
|
||||
else:
|
||||
return HTTPException(
|
||||
|
||||
brain_details = brain.get_brain_details()
|
||||
if brain_details is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Brain not found",
|
||||
detail="Brain details not found",
|
||||
)
|
||||
|
||||
return brain_details
|
||||
|
||||
|
||||
# create new brain
|
||||
@brain_router.post("/brains/", dependencies=[Depends(AuthBearer())], tags=["Brain"])
|
||||
|
@ -9,7 +9,7 @@ from auth import AuthBearer, get_current_user
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from llm.openai import OpenAIBrainPicking
|
||||
from models.brains import get_default_user_brain_or_create_new
|
||||
from models.brains import Brain, get_default_user_brain_or_create_new
|
||||
from models.chat import Chat, ChatHistory
|
||||
from models.chats import ChatQuestion
|
||||
from models.settings import LLMSettings, common_dependencies
|
||||
@ -19,12 +19,12 @@ from repository.chat.get_chat_by_id import get_chat_by_id
|
||||
from repository.chat.get_chat_history import get_chat_history
|
||||
from repository.chat.get_user_chats import get_user_chats
|
||||
from repository.chat.update_chat import ChatUpdatableProperties, update_chat
|
||||
from repository.user_identity.get_user_identity import get_user_identity
|
||||
|
||||
chat_router = APIRouter()
|
||||
|
||||
|
||||
class NullableUUID(UUID):
|
||||
|
||||
@classmethod
|
||||
def __get_validators__(cls):
|
||||
yield cls.validate
|
||||
@ -180,10 +180,38 @@ async def create_question_handler(
|
||||
| None = Query(..., description="The ID of the brain"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> ChatHistory:
|
||||
"""
|
||||
Add a new question to the chat.
|
||||
"""
|
||||
# Retrieve user's OpenAI API key
|
||||
current_user.user_openai_api_key = request.headers.get("Openai-Api-Key")
|
||||
brain = Brain(id=brain_id)
|
||||
|
||||
if not current_user.user_openai_api_key:
|
||||
brain_details = brain.get_brain_details()
|
||||
if brain_details:
|
||||
current_user.user_openai_api_key = brain_details["openai_api_key"]
|
||||
|
||||
if not current_user.user_openai_api_key:
|
||||
user_identity = get_user_identity(current_user.id)
|
||||
|
||||
if user_identity is not None:
|
||||
current_user.user_openai_api_key = user_identity.openai_api_key
|
||||
|
||||
# Retrieve chat model (temperature, max_tokens, model)
|
||||
if (
|
||||
not chat_question.model
|
||||
or not chat_question.temperature
|
||||
or not chat_question.max_tokens
|
||||
):
|
||||
# TODO: create ChatConfig class (pick config from brain or user or chat) and use it here
|
||||
chat_question.model = chat_question.model or brain.model or "gpt-3.5-turbo-0613"
|
||||
chat_question.temperature = chat_question.temperature or brain.temperature or 0
|
||||
chat_question.max_tokens = chat_question.max_tokens or brain.max_tokens or 256
|
||||
|
||||
try:
|
||||
check_user_limit(current_user)
|
||||
llm_settings = LLMSettings()
|
||||
LLMSettings()
|
||||
|
||||
if not brain_id:
|
||||
brain_id = get_default_user_brain_or_create_new(current_user).id
|
||||
@ -227,14 +255,38 @@ async def create_stream_question_handler(
|
||||
) -> StreamingResponse:
|
||||
# TODO: check if the user has access to the brain
|
||||
|
||||
# Retrieve user's OpenAI API key
|
||||
current_user.user_openai_api_key = request.headers.get("Openai-Api-Key")
|
||||
brain = Brain(id=brain_id)
|
||||
|
||||
if not current_user.user_openai_api_key:
|
||||
brain_details = brain.get_brain_details()
|
||||
if brain_details:
|
||||
current_user.user_openai_api_key = brain_details["openai_api_key"]
|
||||
|
||||
if not current_user.user_openai_api_key:
|
||||
user_identity = get_user_identity(current_user.id)
|
||||
|
||||
if user_identity is not None:
|
||||
current_user.user_openai_api_key = user_identity.openai_api_key
|
||||
|
||||
# Retrieve chat model (temperature, max_tokens, model)
|
||||
if (
|
||||
not chat_question.model
|
||||
or not chat_question.temperature
|
||||
or not chat_question.max_tokens
|
||||
):
|
||||
# TODO: create ChatConfig class (pick config from brain or user or chat) and use it here
|
||||
chat_question.model = chat_question.model or brain.model or "gpt-3.5-turbo-0613"
|
||||
chat_question.temperature = chat_question.temperature or brain.temperature or 0
|
||||
chat_question.max_tokens = chat_question.max_tokens or brain.max_tokens or 256
|
||||
|
||||
try:
|
||||
user_openai_api_key = request.headers.get("Openai-Api-Key")
|
||||
logger.info(f"Streaming request for {chat_question.model}")
|
||||
check_user_limit(current_user)
|
||||
if not brain_id:
|
||||
brain_id = get_default_user_brain_or_create_new(current_user).id
|
||||
|
||||
|
||||
gpt_answer_generator = OpenAIBrainPicking(
|
||||
chat_id=str(chat_id),
|
||||
model=chat_question.model,
|
||||
|
@ -173,13 +173,13 @@ def get_user_invitation(brain_id: UUID, current_user: User = Depends(get_current
|
||||
brain = Brain(id=brain_id)
|
||||
brain_details = brain.get_brain_details()
|
||||
|
||||
if len(brain_details) == 0:
|
||||
if brain_details is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Brain not found while trying to get invitation",
|
||||
)
|
||||
|
||||
return {"name": brain_details[0]["name"], "rights": invitation["rights"]}
|
||||
return {"name": brain_details["name"], "rights": invitation["rights"]}
|
||||
|
||||
|
||||
@subscription_router.post(
|
||||
|
@ -62,7 +62,9 @@ async def upload_file(
|
||||
else:
|
||||
openai_api_key = request.headers.get("Openai-Api-Key", None)
|
||||
if openai_api_key is None:
|
||||
openai_api_key = brain.get_brain_details()["openai_api_key"]
|
||||
brain_details = brain.get_brain_details()
|
||||
if brain_details:
|
||||
openai_api_key = brain_details["openai_api_key"]
|
||||
|
||||
if openai_api_key is None:
|
||||
openai_api_key = get_user_identity(current_user.id).openai_api_key
|
||||
|
@ -8,7 +8,6 @@ import { useForm } from "react-hook-form";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
import { useBrainConfig } from "@/lib/context/BrainConfigProvider";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useBrainProvider } from "@/lib/context/BrainProvider/hooks/useBrainProvider";
|
||||
import { Brain } from "@/lib/context/BrainProvider/types";
|
||||
import { defineMaxTokens } from "@/lib/helpers/defineMexTokens";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
@ -25,7 +24,8 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const { setAsDefaultBrain, getBrain, updateBrain } = useBrainApi();
|
||||
const { config } = useBrainConfig();
|
||||
const { fetchAllBrains, fetchDefaultBrain } = useBrainContext();
|
||||
const { fetchAllBrains, fetchDefaultBrain, defaultBrainId } =
|
||||
useBrainContext();
|
||||
|
||||
const defaultValues = {
|
||||
...config,
|
||||
@ -58,15 +58,21 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (brainKey === "max_tokens") {
|
||||
if (brain["max_tokens"] !== undefined) {
|
||||
setValue("maxTokens", brain["max_tokens"]);
|
||||
}
|
||||
} else {
|
||||
// @ts-expect-error bad type inference from typescript
|
||||
// eslint-disable-next-line
|
||||
setValue(key, brain[key]);
|
||||
if (brainKey === "max_tokens" && brain["max_tokens"] !== undefined) {
|
||||
setValue("maxTokens", brain["max_tokens"]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
brainKey === "openai_api_key" &&
|
||||
brain["openai_api_key"] !== undefined
|
||||
) {
|
||||
setValue("openAiKey", brain["openai_api_key"]);
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error bad type inference from typescript
|
||||
// eslint-disable-next-line
|
||||
setValue(key, brain[key]);
|
||||
}
|
||||
};
|
||||
void fetchBrain();
|
||||
@ -131,7 +137,17 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
|
||||
await updateBrain(brainId, getValues());
|
||||
const {
|
||||
maxTokens: max_tokens,
|
||||
openAiKey: openai_api_key,
|
||||
...otherConfigs
|
||||
} = getValues();
|
||||
|
||||
await updateBrain(brainId, {
|
||||
...otherConfigs,
|
||||
max_tokens,
|
||||
openai_api_key,
|
||||
});
|
||||
|
||||
publish({
|
||||
variant: "success",
|
||||
@ -160,7 +176,6 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
const { defaultBrainId } = useBrainProvider();
|
||||
const isDefaultBrain = defaultBrainId === brainId;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,10 +1,31 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { BrainProvider } from "@/lib/context";
|
||||
import { BrainConfigProvider } from "@/lib/context/BrainConfigProvider";
|
||||
|
||||
import { ChatInput } from "../index";
|
||||
|
||||
const addQuestionMock = vi.fn((...params: unknown[]) => ({ params }));
|
||||
|
||||
vi.mock("@/lib/hooks", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/lib/hooks")>(
|
||||
"@/lib/hooks"
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useAxios: () => ({
|
||||
axiosInstance: {
|
||||
get: vi.fn(() => ({
|
||||
data: {},
|
||||
})),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/app/chat/[chatId]/hooks/useChat", () => ({
|
||||
useChat: () => ({
|
||||
addQuestion: (...params: unknown[]) => addQuestionMock(...params),
|
||||
@ -12,14 +33,28 @@ vi.mock("@/app/chat/[chatId]/hooks/useChat", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
addQuestionMock.mockClear();
|
||||
});
|
||||
const mockUseSupabase = vi.fn(() => ({
|
||||
session: {},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => mockUseSupabase(),
|
||||
}));
|
||||
|
||||
describe("ChatInput", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should render correctly", () => {
|
||||
// Rendering the ChatInput component
|
||||
const { getByTestId } = render(<ChatInput />);
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
|
||||
const chatInputForm = getByTestId("chat-input-form");
|
||||
expect(chatInputForm).toBeDefined();
|
||||
@ -30,15 +65,18 @@ describe("ChatInput", () => {
|
||||
const submitButton = getByTestId("submit-button");
|
||||
expect(submitButton).toBeDefined();
|
||||
|
||||
const configButton = getByTestId("config-button");
|
||||
expect(configButton).toBeDefined();
|
||||
|
||||
const micButton = getByTestId("mic-button");
|
||||
expect(micButton).toBeDefined();
|
||||
});
|
||||
|
||||
it("should not call addQuestion on form submit when message is empty", () => {
|
||||
const { getByTestId } = render(<ChatInput />);
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
const chatInputForm = getByTestId("chat-input-form");
|
||||
fireEvent.submit(chatInputForm);
|
||||
|
||||
@ -47,7 +85,13 @@ describe("ChatInput", () => {
|
||||
});
|
||||
|
||||
it("should call addQuestion once on form submit when message is not empty", () => {
|
||||
const { getByTestId } = render(<ChatInput />);
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
const chatInput = getByTestId("chat-input");
|
||||
fireEvent.change(chatInput, { target: { value: "Test question" } });
|
||||
const chatInputForm = getByTestId("chat-input-form");
|
||||
@ -65,7 +109,13 @@ describe("ChatInput", () => {
|
||||
// Mocking the addQuestion function
|
||||
|
||||
// Rendering the ChatInput component with the mock function
|
||||
const { getByTestId } = render(<ChatInput />);
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
const chatInput = getByTestId("chat-input");
|
||||
|
||||
fireEvent.change(chatInput, { target: { value: "Another test question" } });
|
||||
@ -80,7 +130,13 @@ describe("ChatInput", () => {
|
||||
});
|
||||
|
||||
it('should not submit a question when "Enter" key is pressed with shift', () => {
|
||||
const { getByTestId } = render(<ChatInput />);
|
||||
const { getByTestId } = render(
|
||||
<BrainConfigProvider>
|
||||
<BrainProvider>
|
||||
<ChatInput />
|
||||
</BrainProvider>
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
|
||||
const inputElement = getByTestId("chat-input");
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { MdSettings } from "react-icons/md";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
|
||||
export const ConfigButton = (): JSX.Element => {
|
||||
return (
|
||||
<Link href={"/config"}>
|
||||
<Button
|
||||
className="p-2 sm:px-3"
|
||||
variant={"tertiary"}
|
||||
data-testid="config-button"
|
||||
>
|
||||
<MdSettings className="text-lg sm:text-xl lg:text-2xl" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -0,0 +1,101 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { MdCheck, MdSettings } from "react-icons/md";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { Modal } from "@/lib/components/ui/Modal";
|
||||
import { models } from "@/lib/context/BrainConfigProvider/types";
|
||||
import { defineMaxTokens } from "@/lib/helpers/defineMexTokens";
|
||||
|
||||
import { useConfigModal } from "./hooks/useConfigModal";
|
||||
|
||||
export const ConfigModal = ({ chatId }: { chatId?: string }): JSX.Element => {
|
||||
const {
|
||||
handleSubmit,
|
||||
isConfigModalOpen,
|
||||
setIsConfigModalOpen,
|
||||
register,
|
||||
temperature,
|
||||
maxTokens,
|
||||
model,
|
||||
} = useConfigModal(chatId);
|
||||
|
||||
if (chatId === undefined) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
Trigger={
|
||||
<Button
|
||||
className="p-2 sm:px-3"
|
||||
variant={"tertiary"}
|
||||
data-testid="config-button"
|
||||
>
|
||||
<MdSettings className="text-lg sm:text-xl lg:text-2xl" />
|
||||
</Button>
|
||||
}
|
||||
title="Chat configuration"
|
||||
desc="Adjust your chat settings"
|
||||
isOpen={isConfigModalOpen}
|
||||
setOpen={setIsConfigModalOpen}
|
||||
CloseTrigger={<div />}
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
void handleSubmit(e);
|
||||
setIsConfigModalOpen(false);
|
||||
}}
|
||||
className="mt-10 flex flex-col items-center gap-2"
|
||||
>
|
||||
<fieldset className="w-full 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((availableModel) => (
|
||||
<option value={availableModel} key={availableModel}>
|
||||
{availableModel}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="w-full flex mt-4">
|
||||
<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 mt-4">
|
||||
<label className="flex-1" htmlFor="tokens">
|
||||
Max tokens: {maxTokens}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max={defineMaxTokens(model ?? "gpt-3.5-turbo-0613")}
|
||||
value={maxTokens}
|
||||
{...register("maxTokens")}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<Button className="mt-12 self-end" type="submit">
|
||||
Save
|
||||
<MdCheck className="text-xl" />
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -0,0 +1,109 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
import {
|
||||
getChatConfigFromLocalStorage,
|
||||
saveChatConfigInLocalStorage,
|
||||
} from "@/lib/api/chat/chat.local";
|
||||
import { useBrainConfig } from "@/lib/context/BrainConfigProvider";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { ChatConfig } from "@/lib/context/ChatProvider/types";
|
||||
import { defineMaxTokens } from "@/lib/helpers/defineMexTokens";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useConfigModal = (chatId?: string) => {
|
||||
const { publish } = useToast();
|
||||
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
|
||||
const { config } = useBrainConfig();
|
||||
const { getBrain } = useBrainApi();
|
||||
const { currentBrain } = useBrainContext();
|
||||
|
||||
const defaultValues: ChatConfig = {};
|
||||
|
||||
const { register, watch, setValue } = useForm({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const model = watch("model");
|
||||
const temperature = watch("temperature");
|
||||
const maxTokens = watch("maxTokens");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChatConfig = async () => {
|
||||
if (chatId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatConfig = getChatConfigFromLocalStorage(chatId);
|
||||
if (chatConfig !== undefined) {
|
||||
setValue("model", chatConfig.model);
|
||||
setValue("temperature", chatConfig.temperature);
|
||||
setValue("maxTokens", chatConfig.maxTokens);
|
||||
} else {
|
||||
if (currentBrain === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relatedBrainConfig = await getBrain(currentBrain.id);
|
||||
if (relatedBrainConfig === undefined) {
|
||||
return;
|
||||
}
|
||||
setValue("model", relatedBrainConfig.model ?? config.model);
|
||||
setValue(
|
||||
"temperature",
|
||||
relatedBrainConfig.temperature ?? config.temperature
|
||||
);
|
||||
setValue(
|
||||
"maxTokens",
|
||||
relatedBrainConfig.max_tokens ?? config.maxTokens
|
||||
);
|
||||
}
|
||||
};
|
||||
void fetchChatConfig();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (maxTokens === undefined || model === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue("maxTokens", Math.min(maxTokens, defineMaxTokens(model)));
|
||||
}, [maxTokens, model, setValue]);
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (chatId === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
saveChatConfigInLocalStorage(chatId, {
|
||||
maxTokens,
|
||||
model,
|
||||
temperature,
|
||||
});
|
||||
|
||||
publish({
|
||||
variant: "success",
|
||||
text: "Chat config successfully updated",
|
||||
});
|
||||
} catch (err) {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: "An error occured while updating chat config",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isConfigModalOpen,
|
||||
setIsConfigModalOpen,
|
||||
handleSubmit,
|
||||
register,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./ConfigModal";
|
@ -4,12 +4,12 @@ import Button from "@/lib/components/ui/Button";
|
||||
|
||||
import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
|
||||
import { useState } from "react";
|
||||
import { ConfigButton } from "./components/ConfigButton";
|
||||
import { ConfigModal } from "./components/ConfigModal";
|
||||
import { MicButton } from "./components/MicButton/MicButton";
|
||||
|
||||
export const ChatInput = (): JSX.Element => {
|
||||
const [message, setMessage] = useState<string>(""); // for optimistic updates
|
||||
const { addQuestion, generatingAnswer } = useChat();
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const { addQuestion, generatingAnswer, chatId } = useChat();
|
||||
|
||||
const submitQuestion = () => {
|
||||
if (message.length === 0) return;
|
||||
@ -52,7 +52,7 @@ export const ChatInput = (): JSX.Element => {
|
||||
</Button>
|
||||
<div className="flex items-center">
|
||||
<MicButton setMessage={setMessage} />
|
||||
<ConfigButton />
|
||||
<ConfigModal chatId={chatId} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -3,20 +3,15 @@ import { AxiosError } from "axios";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
||||
import { getChatConfigFromLocalStorage } from "@/lib/api/chat/chat.local";
|
||||
import { useChatApi } from "@/lib/api/chat/useChatApi";
|
||||
import { useBrainConfig } from "@/lib/context/BrainConfigProvider/hooks/useBrainConfig";
|
||||
import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
import { useEventTracking } from "@/services/analytics/useEventTracking";
|
||||
|
||||
|
||||
import { useQuestion } from "./useQuestion";
|
||||
import { ChatQuestion } from "../types";
|
||||
|
||||
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useChat = () => {
|
||||
const { track } = useEventTracking();
|
||||
@ -25,9 +20,7 @@ export const useChat = () => {
|
||||
params?.chatId as string | undefined
|
||||
);
|
||||
const [generatingAnswer, setGeneratingAnswer] = useState(false);
|
||||
const {
|
||||
config: { maxTokens, model, temperature },
|
||||
} = useBrainConfig();
|
||||
|
||||
const { history, setHistory } = useChatContext();
|
||||
const { publish } = useToast();
|
||||
const { createChat, getHistory } = useChatApi();
|
||||
@ -51,13 +44,6 @@ export const useChat = () => {
|
||||
}, [chatId, setHistory]);
|
||||
|
||||
const addQuestion = async (question: string, callback?: () => void) => {
|
||||
const chatQuestion: ChatQuestion = {
|
||||
model,
|
||||
question,
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
};
|
||||
|
||||
try {
|
||||
setGeneratingAnswer(true);
|
||||
|
||||
@ -72,10 +58,16 @@ export const useChat = () => {
|
||||
}
|
||||
|
||||
void track("QUESTION_ASKED");
|
||||
const chatConfig = getChatConfigFromLocalStorage(currentChatId);
|
||||
|
||||
const chatQuestion: ChatQuestion = {
|
||||
model: chatConfig?.model,
|
||||
question,
|
||||
temperature: chatConfig?.temperature,
|
||||
max_tokens: chatConfig?.maxTokens,
|
||||
};
|
||||
|
||||
|
||||
await addStreamQuestion(currentChatId, chatQuestion);
|
||||
|
||||
|
||||
callback?.();
|
||||
} catch (error) {
|
||||
@ -103,5 +95,6 @@ export const useChat = () => {
|
||||
history,
|
||||
addQuestion,
|
||||
generatingAnswer,
|
||||
chatId,
|
||||
};
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { UUID } from "crypto";
|
||||
|
||||
export type ChatQuestion = {
|
||||
model: string;
|
||||
model?: string;
|
||||
question?: string;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
};
|
||||
export type ChatHistory = {
|
||||
chat_id: string;
|
||||
|
@ -1,73 +0,0 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { BrainConfigProvider } from "@/lib/context/BrainConfigProvider";
|
||||
|
||||
import ConfigPage from "../page";
|
||||
|
||||
// Mocks ConfirmButton as JSX.Element
|
||||
|
||||
const ConfirmFormMock = vi.fn<[], JSX.Element>(() => <div />);
|
||||
|
||||
const ConfirmTitleMock = vi.fn(() => <div />);
|
||||
const ApiKeyConfig = vi.fn(() => <div />);
|
||||
|
||||
const redirectMock = vi.fn((props: unknown) => ({ props }));
|
||||
|
||||
const useSupabaseMock = vi.fn(() => ({
|
||||
session: null,
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: (props: unknown) => redirectMock(props),
|
||||
useRouter: vi.fn(() => ({
|
||||
redirect: redirectMock,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => useSupabaseMock(),
|
||||
}));
|
||||
|
||||
describe("ConfigPage", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should redirect to /login if session is null", () => {
|
||||
render(
|
||||
<BrainConfigProvider>
|
||||
<ConfigPage />
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
|
||||
expect(redirectMock).toHaveBeenCalledTimes(1);
|
||||
expect(redirectMock).toHaveBeenCalledWith("/login");
|
||||
});
|
||||
|
||||
it("should render config page if user is connected", () => {
|
||||
useSupabaseMock.mockReturnValue({
|
||||
// @ts-ignore we don't actually need parameters
|
||||
session: {
|
||||
user: {},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mock("../components", () => ({
|
||||
ConfigForm: () => ConfirmFormMock(),
|
||||
ConfigTitle: () => ConfirmTitleMock(),
|
||||
ApiKeyConfig: () => ApiKeyConfig(),
|
||||
}));
|
||||
|
||||
render(
|
||||
<BrainConfigProvider>
|
||||
<ConfigPage />
|
||||
</BrainConfigProvider>
|
||||
);
|
||||
|
||||
expect(redirectMock).not.toHaveBeenCalled();
|
||||
expect(ConfirmTitleMock).toHaveBeenCalledTimes(1);
|
||||
expect(ConfirmFormMock).toHaveBeenCalledTimes(1);
|
||||
expect(ApiKeyConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
@ -1,49 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
|
||||
import { useApiKeyConfig } from "./hooks/useApiKeyConfig";
|
||||
|
||||
export const ApiKeyConfig = (): JSX.Element => {
|
||||
const {
|
||||
apiKey,
|
||||
handleCopyClick,
|
||||
|
||||
handleCreateClick,
|
||||
} = useApiKeyConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-gray-300 w-full max-w-xl mb-8">
|
||||
<p className="text-center text-gray-600 uppercase tracking-wide font-semibold">
|
||||
API Key Config
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center space-x-4">
|
||||
{apiKey === "" && (
|
||||
<Button
|
||||
data-testid="create-new-key"
|
||||
variant="secondary"
|
||||
onClick={() => void handleCreateClick()}
|
||||
>
|
||||
Create New Key
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{apiKey !== "" && (
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-gray-600">{apiKey}</span>
|
||||
<Button
|
||||
data-testid="copy-api-key-button"
|
||||
variant="secondary"
|
||||
onClick={handleCopyClick}
|
||||
>
|
||||
📋
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,52 +0,0 @@
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ApiKeyConfig } from "../ApiKeyConfig";
|
||||
|
||||
const handleCreateClickMock = vi.fn(() => ({}));
|
||||
const handleCopyClickMock = vi.fn(() => ({}));
|
||||
|
||||
const useApiKeyConfigMock = vi.fn(() => ({
|
||||
apiKey: "",
|
||||
handleCreateClick: () => handleCreateClickMock(),
|
||||
handleCopyClick: () => handleCopyClickMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../hooks/useApiKeyConfig", () => ({
|
||||
useApiKeyConfig: () => useApiKeyConfigMock(),
|
||||
}));
|
||||
|
||||
describe("ApiKeyConfig", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should render ApiConfig Component", () => {
|
||||
const { getByText } = render(<ApiKeyConfig />);
|
||||
expect(getByText("API Key Config")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders 'Create New Key' button when apiKey is empty", () => {
|
||||
const { getByTestId } = render(<ApiKeyConfig />);
|
||||
|
||||
const createButton = getByTestId("create-new-key");
|
||||
expect(createButton).toBeDefined();
|
||||
|
||||
fireEvent.click(createButton);
|
||||
expect(handleCreateClickMock).toHaveBeenCalledTimes(1);
|
||||
expect(handleCreateClickMock).toHaveBeenCalledWith();
|
||||
});
|
||||
it('renders "Copy" button when apiKey is not empty', () => {
|
||||
useApiKeyConfigMock.mockReturnValue({
|
||||
apiKey: "123456789",
|
||||
handleCreateClick: () => handleCreateClickMock(),
|
||||
handleCopyClick: () => handleCopyClickMock(),
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<ApiKeyConfig />);
|
||||
const copyButton = getByTestId("copy-api-key-button");
|
||||
expect(copyButton).toBeDefined();
|
||||
fireEvent.click(copyButton);
|
||||
expect(handleCopyClickMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
@ -1,67 +0,0 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useApiKeyConfig } from "../useApiKeyConfig";
|
||||
|
||||
const createApiKeyMock = vi.fn(() => "dummyApiKey");
|
||||
const trackMock = vi.fn((props: unknown) => ({ props }));
|
||||
|
||||
const useAuthApiMock = vi.fn(() => ({
|
||||
createApiKey: () => createApiKeyMock(),
|
||||
}));
|
||||
|
||||
const useEventTrackingMock = vi.fn(() => ({
|
||||
track: (props: unknown) => trackMock(props),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/auth/useAuthApi", () => ({
|
||||
useAuthApi: () => useAuthApiMock(),
|
||||
}));
|
||||
vi.mock("@/services/analytics/useEventTracking", () => ({
|
||||
useEventTracking: () => useEventTrackingMock(),
|
||||
}));
|
||||
|
||||
describe("useApiKeyConfig", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should set the apiKey when handleCreateClick is called", async () => {
|
||||
const { result } = renderHook(() => useApiKeyConfig());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleCreateClick();
|
||||
});
|
||||
|
||||
expect(createApiKeyMock).toHaveBeenCalledTimes(1);
|
||||
expect(trackMock).toHaveBeenCalledWith("CREATE_API_KEY");
|
||||
expect(result.current.apiKey).toBe("dummyApiKey");
|
||||
});
|
||||
|
||||
it("should call copyToClipboard when handleCopyClick is called with a non-empty apiKey", () => {
|
||||
vi.mock("react", async () => {
|
||||
const actual = await vi.importActual<typeof import("react")>("react");
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useState: () => ["dummyApiKey", vi.fn()],
|
||||
};
|
||||
});
|
||||
//@ts-ignore - clipboard is not actually readonly
|
||||
global.navigator.clipboard = {
|
||||
writeText: vi.fn(),
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useApiKeyConfig());
|
||||
|
||||
act(() => result.current.handleCopyClick());
|
||||
|
||||
expect(trackMock).toHaveBeenCalledTimes(1);
|
||||
expect(trackMock).toHaveBeenCalledWith("COPY_API_KEY");
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
expect(global.navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
"dummyApiKey"
|
||||
);
|
||||
});
|
||||
});
|
@ -1,45 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useAuthApi } from "@/lib/api/auth/useAuthApi";
|
||||
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();
|
||||
|
||||
const handleCreateClick = async () => {
|
||||
try {
|
||||
void track("CREATE_API_KEY");
|
||||
const createdApiKey = await createApiKey();
|
||||
setApiKey(createdApiKey);
|
||||
} catch (error) {
|
||||
console.error("Error creating API key: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
void track("COPY_API_KEY");
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyClick = () => {
|
||||
if (apiKey !== "") {
|
||||
void copyToClipboard(apiKey);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleCreateClick,
|
||||
apiKey,
|
||||
handleCopyClick,
|
||||
openAiApiKey,
|
||||
setOpenAiApiKey,
|
||||
};
|
||||
};
|
@ -1,56 +0,0 @@
|
||||
/* eslint-disable */
|
||||
"use client";
|
||||
|
||||
import { UseFormRegister } from "react-hook-form";
|
||||
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import { BrainConfig } from "@/lib/context/BrainConfigProvider/types";
|
||||
|
||||
interface BackendConfigProps {
|
||||
register: UseFormRegister<BrainConfig>;
|
||||
}
|
||||
|
||||
export const BackendConfig = ({
|
||||
register,
|
||||
}: BackendConfigProps): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { BackendConfig } from "../BackendConfig";
|
||||
|
||||
const registerMock = vi.fn(() => void 0);
|
||||
|
||||
describe("BackendConfig", () => {
|
||||
it("renders the component with fields and labels", () => {
|
||||
//@ts-expect-error we don't need registerMock to return all `register` keys
|
||||
const { getByText } = render(<BackendConfig register={registerMock} />);
|
||||
|
||||
expect(getByText("Backend config")).toBeDefined();
|
||||
expect(getByText("Backend URL")).toBeDefined();
|
||||
expect(getByText("Supabase URL")).toBeDefined();
|
||||
expect(getByText("Supabase key")).toBeDefined();
|
||||
expect(getByText("Keep in local")).toBeDefined();
|
||||
expect(getByText("Keep in local")).toBeDefined();
|
||||
expect(registerMock).toHaveBeenCalledWith("backendUrl");
|
||||
expect(registerMock).toHaveBeenCalledWith("supabaseUrl");
|
||||
expect(registerMock).toHaveBeenCalledWith("supabaseKey");
|
||||
expect(registerMock).toHaveBeenCalledWith("backendUrl");
|
||||
});
|
||||
});
|
@ -1,64 +0,0 @@
|
||||
/* eslint-disable */
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
|
||||
import { useConfig } from "../hooks/useConfig";
|
||||
import { BackendConfig } from "./BackendConfig/BackendConfig";
|
||||
import { ModelConfig } from "./ModelConfig";
|
||||
import { UserAccountSection } from "./UserAccountSection";
|
||||
|
||||
export const ConfigForm = (): JSX.Element => {
|
||||
const {
|
||||
handleSubmit,
|
||||
isDirty,
|
||||
maxTokens,
|
||||
openAiKey,
|
||||
saveConfig,
|
||||
register,
|
||||
temperature,
|
||||
model,
|
||||
resetBrainConfig,
|
||||
} = useConfig();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleDoneClick = () => {
|
||||
if (isDirty) {
|
||||
saveConfig();
|
||||
}
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-5 py-5 w-full max-w-xl"
|
||||
onSubmit={handleSubmit(handleDoneClick)}
|
||||
>
|
||||
<ModelConfig
|
||||
register={register}
|
||||
model={model}
|
||||
openAiKey={openAiKey}
|
||||
temperature={temperature}
|
||||
maxTokens={maxTokens}
|
||||
/>
|
||||
<BackendConfig register={register} />
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
variant="danger"
|
||||
className="self-end"
|
||||
type="button"
|
||||
onClick={resetBrainConfig}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button variant="secondary" className="self-end">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
<UserAccountSection />
|
||||
</form>
|
||||
);
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
export const ConfigTitle = (): JSX.Element => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl font-bold text-center">Configuration</h1>
|
||||
<h2 className="opacity-50 text-center">
|
||||
Here, you can choose your model, set your credentials...
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,102 +0,0 @@
|
||||
/* eslint-disable */
|
||||
"use client";
|
||||
|
||||
import { UseFormRegister } from "react-hook-form";
|
||||
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import {
|
||||
BrainConfig,
|
||||
Model,
|
||||
PaidModels,
|
||||
anthropicModels,
|
||||
models,
|
||||
paidModels,
|
||||
} from "@/lib/context/BrainConfigProvider/types";
|
||||
import { defineMaxTokens } from "@/lib/helpers/defineMexTokens";
|
||||
|
||||
interface ModelConfigProps {
|
||||
register: UseFormRegister<BrainConfig>;
|
||||
model: Model | PaidModels;
|
||||
openAiKey: string | undefined;
|
||||
temperature: number;
|
||||
maxTokens: number;
|
||||
}
|
||||
|
||||
export const ModelConfig = ({
|
||||
register,
|
||||
model,
|
||||
openAiKey,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}: ModelConfigProps): JSX.Element => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<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="password"
|
||||
placeholder="Open AI Key"
|
||||
className="w-full"
|
||||
label="Open AI Key"
|
||||
{...register("openAiKey")}
|
||||
/>
|
||||
<fieldset className="w-full 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"
|
||||
>
|
||||
{(openAiKey ? paidModels : 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={defineMaxTokens(model)}
|
||||
step="32"
|
||||
value={maxTokens}
|
||||
{...register("maxTokens")}
|
||||
/>
|
||||
</fieldset>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,36 +0,0 @@
|
||||
/* eslint-disable */
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
|
||||
export const UserAccountSection = (): JSX.Element => {
|
||||
const { session } = useSupabase();
|
||||
|
||||
if (session === null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-gray-300 mt-8 mb-8">
|
||||
<p className="text-center text-gray-600 uppercase tracking-wide font-semibold">
|
||||
Your Account
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<span>
|
||||
Signed In as: <b>{session.user.email}</b>
|
||||
</span>
|
||||
<Link className="mt-2" href={"/logout"}>
|
||||
<Button className="px-3 py-2" variant={"danger"}>
|
||||
Logout
|
||||
</Button>
|
||||
</Link>
|
||||
{/* TODO: add functionality to change password */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
export * from "./ApiKeyConfig/ApiKeyConfig";
|
||||
export * from "./ConfigForm";
|
||||
export * from "./ConfigTitle";
|
@ -1,84 +0,0 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useBrainConfig } from "@/lib/context/BrainConfigProvider/hooks/useBrainConfig";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
|
||||
export const useConfig = () => {
|
||||
const { config, updateConfig, resetConfig } = useBrainConfig();
|
||||
const { publish } = useToast();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
getValues,
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
setError,
|
||||
} = useForm({
|
||||
defaultValues: config,
|
||||
});
|
||||
|
||||
const model = watch("model");
|
||||
const temperature = watch("temperature");
|
||||
const maxTokens = watch("maxTokens");
|
||||
const openAiKey = watch("openAiKey");
|
||||
|
||||
useEffect(() => {
|
||||
reset(config);
|
||||
}, [config, reset]);
|
||||
|
||||
const saveConfig = () => {
|
||||
const values = getValues();
|
||||
|
||||
if (!validateConfig()) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateConfig(values);
|
||||
publish({
|
||||
text: "Config saved",
|
||||
variant: "success",
|
||||
});
|
||||
};
|
||||
|
||||
const resetBrainConfig = () => {
|
||||
resetConfig();
|
||||
publish({
|
||||
text: "Config reset",
|
||||
variant: "success",
|
||||
});
|
||||
};
|
||||
|
||||
const openAiKeyPattern = /^sk-[a-zA-Z0-9]{45,50}$/;
|
||||
|
||||
const validateConfig = (): boolean => {
|
||||
const { openAiKey } = getValues();
|
||||
|
||||
const isKeyEmpty = openAiKey === "" || openAiKey === undefined;
|
||||
if (isKeyEmpty || openAiKeyPattern.test(openAiKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
publish({
|
||||
text: "Invalid OpenAI Key",
|
||||
variant: "danger",
|
||||
});
|
||||
setError("openAiKey", { type: "pattern", message: "Invalid OpenAI Key" });
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
saveConfig,
|
||||
maxTokens,
|
||||
openAiKey,
|
||||
temperature,
|
||||
isDirty,
|
||||
register,
|
||||
model,
|
||||
resetBrainConfig,
|
||||
};
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
/* eslint-disable */
|
||||
"use client";
|
||||
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||
import { ApiKeyConfig, ConfigForm, ConfigTitle } from "./components";
|
||||
|
||||
// TODO: Use states instead of NEXTJS router to open and close modal
|
||||
const ConfigPage = (): JSX.Element => {
|
||||
const { session } = useSupabase();
|
||||
|
||||
if (session === null) {
|
||||
redirectToLogin();
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="w-full flex flex-col">
|
||||
<section className="w-full outline-none pt-10 flex flex-col gap-5 items-center justify-center p-6">
|
||||
<ConfigTitle />
|
||||
<ConfigForm />
|
||||
<ApiKeyConfig />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigPage;
|
20
frontend/lib/api/chat/chat.local.ts
Normal file
20
frontend/lib/api/chat/chat.local.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ChatConfig } from "@/lib/context/ChatProvider/types";
|
||||
|
||||
export const saveChatConfigInLocalStorage = (
|
||||
chatId: string,
|
||||
chatConfig: ChatConfig
|
||||
): void => {
|
||||
localStorage.setItem(`chat-config-${chatId}`, JSON.stringify(chatConfig));
|
||||
};
|
||||
|
||||
export const getChatConfigFromLocalStorage = (
|
||||
chatId: string
|
||||
): ChatConfig | undefined => {
|
||||
const config = localStorage.getItem(`chat-config-${chatId}`);
|
||||
|
||||
if (config === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return JSON.parse(config) as ChatConfig;
|
||||
};
|
@ -13,7 +13,7 @@ import { useToast } from "@/lib/hooks";
|
||||
export const useAddBrainModal = () => {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const { publish } = useToast();
|
||||
const { createBrain } = useBrainContext();
|
||||
const { createBrain, setActiveBrain } = useBrainContext();
|
||||
const { setAsDefaultBrain } = useBrainApi();
|
||||
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
||||
const { config } = useBrainConfig();
|
||||
@ -56,15 +56,21 @@ export const useAddBrainModal = () => {
|
||||
temperature,
|
||||
});
|
||||
|
||||
if (setDefault) {
|
||||
if (createdBrainId === undefined) {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: "Error occurred while creating a brain",
|
||||
});
|
||||
if (createdBrainId === undefined) {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: "Error occurred while creating a brain",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveBrain({
|
||||
id: createdBrainId,
|
||||
name,
|
||||
});
|
||||
|
||||
if (setDefault) {
|
||||
await setAsDefaultBrain(createdBrainId);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { FaBrain } from "react-icons/fa";
|
||||
import { MdSettings } from "react-icons/md";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
export const BrainManagementButton = (): JSX.Element => {
|
||||
@ -9,10 +9,13 @@ export const BrainManagementButton = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<Link href={`/brains-management/${currentBrainId ?? ""}`}>
|
||||
<button type="button" className="flex items-center focus:outline-none">
|
||||
<MdSettings className="w-6 h-6" color="gray" />
|
||||
<FaBrain className="w-3 h-3" color="gray" />
|
||||
</button>
|
||||
<Button
|
||||
variant={"tertiary"}
|
||||
className="focus:outline-none text-2xl"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<MdSettings />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, HTMLAttributes, SetStateAction } from "react";
|
||||
import { MdPerson, MdSettings } from "react-icons/md";
|
||||
import { MdPerson } from "react-icons/md";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@ -63,15 +62,6 @@ export const NavItems = ({
|
||||
<Link aria-label="account" className="" href={"/user"}>
|
||||
<MdPerson className="text-2xl" />
|
||||
</Link>
|
||||
<Link href={"/config"}>
|
||||
<Button
|
||||
variant={"tertiary"}
|
||||
className="focus:outline-none text-2xl"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<MdSettings />
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{!isUserLoggedIn && <AuthButtons />}
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { ChatHistory } from "@/app/chat/[chatId]/types";
|
||||
|
||||
import { Model } from "../BrainConfigProvider/types";
|
||||
|
||||
export type ChatConfig = {
|
||||
model?: Model;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
};
|
||||
|
||||
export type ChatContextProps = {
|
||||
history: ChatHistory[];
|
||||
setHistory: (history: ChatHistory[]) => void;
|
||||
|
Loading…
Reference in New Issue
Block a user