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:
Mamadou DICKO 2023-08-01 16:25:02 +02:00 committed by GitHub
parent 130a3e0938
commit edcbb30e97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 453 additions and 802 deletions

View File

@ -2,11 +2,12 @@ from typing import Any, List, Optional
from uuid import UUID from uuid import UUID
from logger import get_logger from logger import get_logger
from models.settings import BrainRateLimiting, CommonsDep, common_dependencies
from models.users import User
from pydantic import BaseModel from pydantic import BaseModel
from utils.vectors import get_unique_files_from_vector_ids 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__) logger = get_logger(__name__)
@ -111,7 +112,9 @@ class Brain(BaseModel):
.filter("brain_id", "eq", self.id) .filter("brain_id", "eq", self.id)
.execute() .execute()
) )
return response.data if response.data == []:
return None
return response.data[0]
def delete_brain(self, user_id): def delete_brain(self, user_id):
results = ( results = (

View File

@ -20,7 +20,9 @@ def resend_invitation_email(
brain_url = get_brain_url(origin, brain_subscription.brain_id) brain_url = get_brain_url(origin, brain_subscription.brain_id)
invitation_brain_client = Brain(id=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"] brain_name = invitation_brain["name"]
html_body = f""" html_body = f"""

View File

@ -73,15 +73,16 @@ async def get_brain_endpoint(
history, which includes the brain messages exchanged in the brain. history, which includes the brain messages exchanged in the brain.
""" """
brain = Brain(id=brain_id) brain = Brain(id=brain_id)
brains = brain.get_brain_details()
if len(brains) > 0: brain_details = brain.get_brain_details()
return brains[0] if brain_details is None:
else: raise HTTPException(
return HTTPException(
status_code=404, status_code=404,
detail="Brain not found", detail="Brain details not found",
) )
return brain_details
# create new brain # create new brain
@brain_router.post("/brains/", dependencies=[Depends(AuthBearer())], tags=["Brain"]) @brain_router.post("/brains/", dependencies=[Depends(AuthBearer())], tags=["Brain"])

View File

@ -9,7 +9,7 @@ from auth import AuthBearer, get_current_user
from fastapi import APIRouter, Depends, Query, Request from fastapi import APIRouter, Depends, Query, Request
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from llm.openai import OpenAIBrainPicking 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.chat import Chat, ChatHistory
from models.chats import ChatQuestion from models.chats import ChatQuestion
from models.settings import LLMSettings, common_dependencies 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_chat_history import get_chat_history
from repository.chat.get_user_chats import get_user_chats from repository.chat.get_user_chats import get_user_chats
from repository.chat.update_chat import ChatUpdatableProperties, update_chat from repository.chat.update_chat import ChatUpdatableProperties, update_chat
from repository.user_identity.get_user_identity import get_user_identity
chat_router = APIRouter() chat_router = APIRouter()
class NullableUUID(UUID): class NullableUUID(UUID):
@classmethod @classmethod
def __get_validators__(cls): def __get_validators__(cls):
yield cls.validate yield cls.validate
@ -180,10 +180,38 @@ async def create_question_handler(
| None = Query(..., description="The ID of the brain"), | None = Query(..., description="The ID of the brain"),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
) -> ChatHistory: ) -> 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") 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: try:
check_user_limit(current_user) check_user_limit(current_user)
llm_settings = LLMSettings() LLMSettings()
if not brain_id: if not brain_id:
brain_id = get_default_user_brain_or_create_new(current_user).id brain_id = get_default_user_brain_or_create_new(current_user).id
@ -227,14 +255,38 @@ async def create_stream_question_handler(
) -> StreamingResponse: ) -> StreamingResponse:
# TODO: check if the user has access to the brain # 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: try:
user_openai_api_key = request.headers.get("Openai-Api-Key")
logger.info(f"Streaming request for {chat_question.model}") logger.info(f"Streaming request for {chat_question.model}")
check_user_limit(current_user) check_user_limit(current_user)
if not brain_id: if not brain_id:
brain_id = get_default_user_brain_or_create_new(current_user).id brain_id = get_default_user_brain_or_create_new(current_user).id
gpt_answer_generator = OpenAIBrainPicking( gpt_answer_generator = OpenAIBrainPicking(
chat_id=str(chat_id), chat_id=str(chat_id),
model=chat_question.model, model=chat_question.model,

View File

@ -173,13 +173,13 @@ def get_user_invitation(brain_id: UUID, current_user: User = Depends(get_current
brain = Brain(id=brain_id) brain = Brain(id=brain_id)
brain_details = brain.get_brain_details() brain_details = brain.get_brain_details()
if len(brain_details) == 0: if brain_details is None:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="Brain not found while trying to get invitation", 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( @subscription_router.post(

View File

@ -62,7 +62,9 @@ async def upload_file(
else: else:
openai_api_key = request.headers.get("Openai-Api-Key", None) openai_api_key = request.headers.get("Openai-Api-Key", None)
if openai_api_key is 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: if openai_api_key is None:
openai_api_key = get_user_identity(current_user.id).openai_api_key openai_api_key = get_user_identity(current_user.id).openai_api_key

View File

@ -8,7 +8,6 @@ import { useForm } from "react-hook-form";
import { useBrainApi } from "@/lib/api/brain/useBrainApi"; import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { useBrainConfig } from "@/lib/context/BrainConfigProvider"; import { useBrainConfig } from "@/lib/context/BrainConfigProvider";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useBrainProvider } from "@/lib/context/BrainProvider/hooks/useBrainProvider";
import { Brain } from "@/lib/context/BrainProvider/types"; import { Brain } from "@/lib/context/BrainProvider/types";
import { defineMaxTokens } from "@/lib/helpers/defineMexTokens"; import { defineMaxTokens } from "@/lib/helpers/defineMexTokens";
import { useToast } from "@/lib/hooks"; import { useToast } from "@/lib/hooks";
@ -25,7 +24,8 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const { setAsDefaultBrain, getBrain, updateBrain } = useBrainApi(); const { setAsDefaultBrain, getBrain, updateBrain } = useBrainApi();
const { config } = useBrainConfig(); const { config } = useBrainConfig();
const { fetchAllBrains, fetchDefaultBrain } = useBrainContext(); const { fetchAllBrains, fetchDefaultBrain, defaultBrainId } =
useBrainContext();
const defaultValues = { const defaultValues = {
...config, ...config,
@ -58,15 +58,21 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
return; return;
} }
if (brainKey === "max_tokens") { if (brainKey === "max_tokens" && brain["max_tokens"] !== undefined) {
if (brain["max_tokens"] !== undefined) { setValue("maxTokens", brain["max_tokens"]);
setValue("maxTokens", brain["max_tokens"]); continue;
}
} else {
// @ts-expect-error bad type inference from typescript
// eslint-disable-next-line
setValue(key, brain[key]);
} }
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(); void fetchBrain();
@ -131,7 +137,17 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
try { try {
setIsUpdating(true); 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({ publish({
variant: "success", variant: "success",
@ -160,7 +176,6 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
setIsUpdating(false); setIsUpdating(false);
} }
}; };
const { defaultBrainId } = useBrainProvider();
const isDefaultBrain = defaultBrainId === brainId; const isDefaultBrain = defaultBrainId === brainId;
useEffect(() => { useEffect(() => {

View File

@ -1,10 +1,31 @@
/* eslint-disable max-lines */
import { fireEvent, render } from "@testing-library/react"; import { fireEvent, render } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { BrainProvider } from "@/lib/context";
import { BrainConfigProvider } from "@/lib/context/BrainConfigProvider";
import { ChatInput } from "../index"; import { ChatInput } from "../index";
const addQuestionMock = vi.fn((...params: unknown[]) => ({ params })); 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", () => ({ vi.mock("@/app/chat/[chatId]/hooks/useChat", () => ({
useChat: () => ({ useChat: () => ({
addQuestion: (...params: unknown[]) => addQuestionMock(...params), addQuestion: (...params: unknown[]) => addQuestionMock(...params),
@ -12,14 +33,28 @@ vi.mock("@/app/chat/[chatId]/hooks/useChat", () => ({
}), }),
})); }));
afterEach(() => { const mockUseSupabase = vi.fn(() => ({
addQuestionMock.mockClear(); session: {},
}); }));
vi.mock("@/lib/context/SupabaseProvider", () => ({
useSupabase: () => mockUseSupabase(),
}));
describe("ChatInput", () => { describe("ChatInput", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("should render correctly", () => { it("should render correctly", () => {
// Rendering the ChatInput component // Rendering the ChatInput component
const { getByTestId } = render(<ChatInput />); const { getByTestId } = render(
<BrainConfigProvider>
<BrainProvider>
<ChatInput />
</BrainProvider>
</BrainConfigProvider>
);
const chatInputForm = getByTestId("chat-input-form"); const chatInputForm = getByTestId("chat-input-form");
expect(chatInputForm).toBeDefined(); expect(chatInputForm).toBeDefined();
@ -30,15 +65,18 @@ describe("ChatInput", () => {
const submitButton = getByTestId("submit-button"); const submitButton = getByTestId("submit-button");
expect(submitButton).toBeDefined(); expect(submitButton).toBeDefined();
const configButton = getByTestId("config-button");
expect(configButton).toBeDefined();
const micButton = getByTestId("mic-button"); const micButton = getByTestId("mic-button");
expect(micButton).toBeDefined(); expect(micButton).toBeDefined();
}); });
it("should not call addQuestion on form submit when message is empty", () => { 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"); const chatInputForm = getByTestId("chat-input-form");
fireEvent.submit(chatInputForm); fireEvent.submit(chatInputForm);
@ -47,7 +85,13 @@ describe("ChatInput", () => {
}); });
it("should call addQuestion once on form submit when message is not empty", () => { 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"); const chatInput = getByTestId("chat-input");
fireEvent.change(chatInput, { target: { value: "Test question" } }); fireEvent.change(chatInput, { target: { value: "Test question" } });
const chatInputForm = getByTestId("chat-input-form"); const chatInputForm = getByTestId("chat-input-form");
@ -65,7 +109,13 @@ describe("ChatInput", () => {
// Mocking the addQuestion function // Mocking the addQuestion function
// Rendering the ChatInput component with the mock 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"); const chatInput = getByTestId("chat-input");
fireEvent.change(chatInput, { target: { value: "Another test question" } }); 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', () => { 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"); const inputElement = getByTestId("chat-input");

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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,
};
};

View File

@ -0,0 +1 @@
export * from "./ConfigModal";

View File

@ -4,12 +4,12 @@ import Button from "@/lib/components/ui/Button";
import { useChat } from "@/app/chat/[chatId]/hooks/useChat"; import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
import { useState } from "react"; import { useState } from "react";
import { ConfigButton } from "./components/ConfigButton"; import { ConfigModal } from "./components/ConfigModal";
import { MicButton } from "./components/MicButton/MicButton"; import { MicButton } from "./components/MicButton/MicButton";
export const ChatInput = (): JSX.Element => { export const ChatInput = (): JSX.Element => {
const [message, setMessage] = useState<string>(""); // for optimistic updates const [message, setMessage] = useState<string>("");
const { addQuestion, generatingAnswer } = useChat(); const { addQuestion, generatingAnswer, chatId } = useChat();
const submitQuestion = () => { const submitQuestion = () => {
if (message.length === 0) return; if (message.length === 0) return;
@ -52,7 +52,7 @@ export const ChatInput = (): JSX.Element => {
</Button> </Button>
<div className="flex items-center"> <div className="flex items-center">
<MicButton setMessage={setMessage} /> <MicButton setMessage={setMessage} />
<ConfigButton /> <ConfigModal chatId={chatId} />
</div> </div>
</form> </form>
); );

View File

@ -3,20 +3,15 @@ import { AxiosError } from "axios";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getChatConfigFromLocalStorage } from "@/lib/api/chat/chat.local";
import { useChatApi } from "@/lib/api/chat/useChatApi"; import { useChatApi } from "@/lib/api/chat/useChatApi";
import { useBrainConfig } from "@/lib/context/BrainConfigProvider/hooks/useBrainConfig";
import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext"; import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext";
import { useToast } from "@/lib/hooks"; import { useToast } from "@/lib/hooks";
import { useEventTracking } from "@/services/analytics/useEventTracking"; import { useEventTracking } from "@/services/analytics/useEventTracking";
import { useQuestion } from "./useQuestion"; import { useQuestion } from "./useQuestion";
import { ChatQuestion } from "../types"; import { ChatQuestion } from "../types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useChat = () => { export const useChat = () => {
const { track } = useEventTracking(); const { track } = useEventTracking();
@ -25,9 +20,7 @@ export const useChat = () => {
params?.chatId as string | undefined params?.chatId as string | undefined
); );
const [generatingAnswer, setGeneratingAnswer] = useState(false); const [generatingAnswer, setGeneratingAnswer] = useState(false);
const {
config: { maxTokens, model, temperature },
} = useBrainConfig();
const { history, setHistory } = useChatContext(); const { history, setHistory } = useChatContext();
const { publish } = useToast(); const { publish } = useToast();
const { createChat, getHistory } = useChatApi(); const { createChat, getHistory } = useChatApi();
@ -51,13 +44,6 @@ export const useChat = () => {
}, [chatId, setHistory]); }, [chatId, setHistory]);
const addQuestion = async (question: string, callback?: () => void) => { const addQuestion = async (question: string, callback?: () => void) => {
const chatQuestion: ChatQuestion = {
model,
question,
temperature,
max_tokens: maxTokens,
};
try { try {
setGeneratingAnswer(true); setGeneratingAnswer(true);
@ -72,10 +58,16 @@ export const useChat = () => {
} }
void track("QUESTION_ASKED"); 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); await addStreamQuestion(currentChatId, chatQuestion);
callback?.(); callback?.();
} catch (error) { } catch (error) {
@ -103,5 +95,6 @@ export const useChat = () => {
history, history,
addQuestion, addQuestion,
generatingAnswer, generatingAnswer,
chatId,
}; };
}; };

View File

@ -1,10 +1,10 @@
import { UUID } from "crypto"; import { UUID } from "crypto";
export type ChatQuestion = { export type ChatQuestion = {
model: string; model?: string;
question?: string; question?: string;
temperature: number; temperature?: number;
max_tokens: number; max_tokens?: number;
}; };
export type ChatHistory = { export type ChatHistory = {
chat_id: string; chat_id: string;

View File

@ -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);
});
});

View File

@ -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>
</>
);
};

View File

@ -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);
});
});

View File

@ -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"
);
});
});

View File

@ -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,
};
};

View File

@ -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>
</>
);
};

View File

@ -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");
});
});

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -1,3 +0,0 @@
export * from "./ApiKeyConfig/ApiKeyConfig";
export * from "./ConfigForm";
export * from "./ConfigTitle";

View File

@ -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,
};
};

View File

@ -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;

View 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;
};

View File

@ -13,7 +13,7 @@ import { useToast } from "@/lib/hooks";
export const useAddBrainModal = () => { export const useAddBrainModal = () => {
const [isPending, setIsPending] = useState(false); const [isPending, setIsPending] = useState(false);
const { publish } = useToast(); const { publish } = useToast();
const { createBrain } = useBrainContext(); const { createBrain, setActiveBrain } = useBrainContext();
const { setAsDefaultBrain } = useBrainApi(); const { setAsDefaultBrain } = useBrainApi();
const [isShareModalOpen, setIsShareModalOpen] = useState(false); const [isShareModalOpen, setIsShareModalOpen] = useState(false);
const { config } = useBrainConfig(); const { config } = useBrainConfig();
@ -56,15 +56,21 @@ export const useAddBrainModal = () => {
temperature, temperature,
}); });
if (setDefault) { if (createdBrainId === undefined) {
if (createdBrainId === undefined) { publish({
publish({ variant: "danger",
variant: "danger", text: "Error occurred while creating a brain",
text: "Error occurred while creating a brain", });
});
return; return;
} }
setActiveBrain({
id: createdBrainId,
name,
});
if (setDefault) {
await setAsDefaultBrain(createdBrainId); await setAsDefaultBrain(createdBrainId);
} }

View File

@ -1,7 +1,7 @@
import Link from "next/link"; import Link from "next/link";
import { FaBrain } from "react-icons/fa";
import { MdSettings } from "react-icons/md"; import { MdSettings } from "react-icons/md";
import Button from "@/lib/components/ui/Button";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
export const BrainManagementButton = (): JSX.Element => { export const BrainManagementButton = (): JSX.Element => {
@ -9,10 +9,13 @@ export const BrainManagementButton = (): JSX.Element => {
return ( return (
<Link href={`/brains-management/${currentBrainId ?? ""}`}> <Link href={`/brains-management/${currentBrainId ?? ""}`}>
<button type="button" className="flex items-center focus:outline-none"> <Button
<MdSettings className="w-6 h-6" color="gray" /> variant={"tertiary"}
<FaBrain className="w-3 h-3" color="gray" /> className="focus:outline-none text-2xl"
</button> aria-label="Settings"
>
<MdSettings />
</Button>
</Link> </Link>
); );
}; };

View File

@ -1,9 +1,8 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { Dispatch, HTMLAttributes, SetStateAction } from "react"; 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 { useSupabase } from "@/lib/context/SupabaseProvider";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -63,15 +62,6 @@ export const NavItems = ({
<Link aria-label="account" className="" href={"/user"}> <Link aria-label="account" className="" href={"/user"}>
<MdPerson className="text-2xl" /> <MdPerson className="text-2xl" />
</Link> </Link>
<Link href={"/config"}>
<Button
variant={"tertiary"}
className="focus:outline-none text-2xl"
aria-label="Settings"
>
<MdSettings />
</Button>
</Link>
</> </>
)} )}
{!isUserLoggedIn && <AuthButtons />} {!isUserLoggedIn && <AuthButtons />}

View File

@ -1,5 +1,13 @@
import { ChatHistory } from "@/app/chat/[chatId]/types"; import { ChatHistory } from "@/app/chat/[chatId]/types";
import { Model } from "../BrainConfigProvider/types";
export type ChatConfig = {
model?: Model;
temperature?: number;
maxTokens?: number;
};
export type ChatContextProps = { export type ChatContextProps = {
history: ChatHistory[]; history: ChatHistory[];
setHistory: (history: ChatHistory[]) => void; setHistory: (history: ChatHistory[]) => void;