mirror of
https://github.com/StanGirard/quivr.git
synced 2024-09-11 13:15:41 +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 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 = (
|
||||||
|
@ -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"""
|
||||||
|
@ -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"])
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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 { 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>
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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 = () => {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 />}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user