feat(search): new way to interact with Quivr (#2026)

Co-authored-by: Zewed <dewez.antoine2@gmail.com>
Co-authored-by: Antoine Dewez <44063631+Zewed@users.noreply.github.com>
This commit is contained in:
Stan Girard 2024-01-19 20:34:30 -08:00 committed by GitHub
parent 7f83fcdf42
commit d0b8b797f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 1106 additions and 724 deletions

View File

@ -28,6 +28,15 @@ brain_service = BrainService()
chat_service = ChatService()
def is_valid_uuid(uuid_to_test, version=4):
try:
uuid_obj = UUID(uuid_to_test, version=version)
except ValueError:
return False
return str(uuid_obj) == uuid_to_test
class KnowledgeBrainQA(BaseModel, QAInterface):
"""
Main class for the Brain Picking functionality.
@ -50,7 +59,7 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
model: str = None # pyright: ignore reportPrivateUsage=none
temperature: float = 0.1
chat_id: str = None # pyright: ignore reportPrivateUsage=none
brain_id: str = None # pyright: ignore reportPrivateUsage=none
brain_id: str # pyright: ignore reportPrivateUsage=none
max_tokens: int = 256
streaming: bool = False
knowledge_qa: Optional[RAGInterface]
@ -88,13 +97,18 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
@property
def prompt_to_use(self):
# TODO: move to prompt service or instruction or something
return get_prompt_to_use(UUID(self.brain_id), self.prompt_id)
if self.brain_id and is_valid_uuid(self.brain_id):
return get_prompt_to_use(UUID(self.brain_id), self.prompt_id)
else:
return None
@property
def prompt_to_use_id(self) -> Optional[UUID]:
# TODO: move to prompt service or instruction or something
return get_prompt_to_use_id(UUID(self.brain_id), self.prompt_id)
if self.brain_id and is_valid_uuid(self.brain_id):
return get_prompt_to_use_id(UUID(self.brain_id), self.prompt_id)
else:
return None
def generate_answer(
self, chat_id: UUID, question: ChatQuestion, save_answer: bool = True
@ -129,10 +143,7 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
answer = model_response["answer"]
brain = None
if question.brain_id:
brain = brain_service.get_brain_by_id(question.brain_id)
brain = brain_service.get_brain_by_id(self.brain_id)
if save_answer:
# save the answer to the database or not -> add a variable
@ -142,7 +153,7 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
"chat_id": chat_id,
"user_message": question.question,
"assistant": answer,
"brain_id": question.brain_id,
"brain_id": brain.brain_id,
"prompt_id": self.prompt_to_use_id,
}
)
@ -223,10 +234,7 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
)
)
brain = None
if question.brain_id:
brain = brain_service.get_brain_by_id(question.brain_id)
brain = brain_service.get_brain_by_id(self.brain_id)
if save_answer:
streamed_chat_history = chat_service.update_chat_history(
@ -235,7 +243,7 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
"chat_id": chat_id,
"user_message": question.question,
"assistant": "",
"brain_id": question.brain_id,
"brain_id": brain.brain_id,
"prompt_id": self.prompt_to_use_id,
}
)
@ -252,6 +260,7 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
if self.prompt_to_use
else None,
"brain_name": brain.name if brain else None,
"sources": None,
}
)
else:
@ -277,7 +286,6 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
yield f"data: {json.dumps(streamed_chat_history.dict())}"
except Exception as e:
logger.error("Error during streaming tokens: %s", e)
sources_string = ""
try:
result = await run
source_documents = result.get("source_documents", [])
@ -288,11 +296,13 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
if source_documents:
# Formatting the source documents using Markdown without new lines for each source
sources_string = "\n\n**Sources:** " + ", ".join(
f"{doc.metadata.get('file_name', 'Unnamed Document')}"
for doc in source_documents
)
streamed_chat_history.assistant += sources_string
sources_list = [
f"[{doc.metadata['file_name']}])" for doc in source_documents
]
# Create metadata if it doesn't exist
if not streamed_chat_history.metadata:
streamed_chat_history.metadata = {}
streamed_chat_history.metadata["sources"] = sources_list
yield f"data: {json.dumps(streamed_chat_history.dict())}"
else:
logger.info(
@ -303,7 +313,8 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
# Combine all response tokens to form the final assistant message
assistant = "".join(response_tokens)
assistant += sources_string
logger.info("💋💋💋💋")
logger.info(streamed_chat_history)
try:
if save_answer:
@ -311,6 +322,7 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
message_id=str(streamed_chat_history.message_id),
user_message=question.question,
assistant=assistant,
metadata=streamed_chat_history.metadata,
)
except Exception as e:
logger.error("Error updating message by ID: %s", e)

View File

@ -28,6 +28,15 @@ logger = get_logger(__name__)
QUIVR_DEFAULT_PROMPT = "Your name is Quivr. You're a helpful assistant. If you don't know the answer, just say that you don't know, don't try to make up an answer."
def is_valid_uuid(uuid_to_test, version=4):
try:
uuid_obj = UUID(uuid_to_test, version=version)
except ValueError:
return False
return str(uuid_obj) == uuid_to_test
brain_service = BrainService()
chat_service = ChatService()
@ -65,7 +74,10 @@ class QuivrRAG(BaseModel, RAGInterface):
@property
def prompt_to_use(self):
return get_prompt_to_use(UUID(self.brain_id), self.prompt_id)
if self.brain_id and is_valid_uuid(self.brain_id):
return get_prompt_to_use(UUID(self.brain_id), self.prompt_id)
else:
return None
supabase_client: Optional[Client] = None
vector_store: Optional[CustomSupabaseVectorStore] = None
@ -179,4 +191,5 @@ class QuivrRAG(BaseModel, RAGInterface):
def get_retriever(self):
return self.vector_store.as_retriever()
# Some other methods can be added such as on_stream, on_end,... to abstract history management (each answer should be saved or not) # Some other methods can be added such as on_stream, on_end,... to abstract history management (each answer should be saved or not)
# Some other methods can be added such as on_stream, on_end,... to abstract history management (each answer should be saved or not)

View File

@ -139,9 +139,6 @@ class UserUsage(Repository):
matching_customers = None
try:
user_is_customer, user_customer_id = self.check_user_is_customer(user_id)
logger.info("🔥🔥🔥")
logger.info(user_is_customer)
logger.info(user_customer_id)
if user_is_customer:
self.db.table("user_settings").update({"is_premium": True}).match(

View File

@ -24,8 +24,8 @@ class CreateApiBrainDefinition(BaseModel, extra=Extra.forbid):
class CreateBrainProperties(BaseModel, extra=Extra.forbid):
name: Optional[str] = "Default brain"
description: Optional[str] = "This is a description"
status: Optional[str] = "private"
description: str = "This is a description"
status: Optional[str] = "public"
model: Optional[str]
temperature: Optional[float] = 0.0
max_tokens: Optional[int] = 256

View File

@ -1,10 +1,11 @@
from uuid import UUID
from logger import get_logger
from models.settings import get_supabase_client
from models.settings import get_embeddings, get_supabase_client
from modules.brain.dto.inputs import BrainUpdatableProperties
from modules.brain.entity.brain_entity import BrainEntity, PublicBrain
from modules.brain.repository.interfaces.brains_interface import BrainsInterface
from modules.brain.repository.interfaces.brains_interface import \
BrainsInterface
logger = get_logger(__name__)
@ -15,17 +16,18 @@ class Brains(BrainsInterface):
self.db = supabase_client
def create_brain(self, brain):
response = (
self.db.table("brains").insert(
brain.dict(
exclude={
"brain_definition",
"brain_secrets_values",
"connected_brains_ids",
}
)
)
).execute()
embeddings = get_embeddings()
string_to_embed = f"Name: {brain.name} Description: {brain.description}"
brain_meaning = embeddings.embed_query(string_to_embed)
brain_dict = brain.dict(
exclude={
"brain_definition",
"brain_secrets_values",
"connected_brains_ids",
}
)
brain_dict["meaning"] = brain_meaning
response = (self.db.table("brains").insert(brain_dict)).execute()
return BrainEntity(**response.data[0])
@ -80,9 +82,14 @@ class Brains(BrainsInterface):
def update_brain_by_id(
self, brain_id: UUID, brain: BrainUpdatableProperties
) -> BrainEntity | None:
embeddings = get_embeddings()
string_to_embed = f"Name: {brain.name} Description: {brain.description}"
brain_meaning = embeddings.embed_query(string_to_embed)
brain_dict = brain.dict(exclude_unset=True)
brain_dict["meaning"] = brain_meaning
update_brain_response = (
self.db.table("brains")
.update(brain.dict(exclude_unset=True))
.update(brain_dict)
.match({"brain_id": brain_id})
.execute()
).data

View File

@ -1,13 +1,22 @@
from fastapi import HTTPException
from langchain.embeddings.ollama import OllamaEmbeddings
from langchain.embeddings.openai import OpenAIEmbeddings
from llm.api_brain_qa import APIBrainQA
from llm.composite_brain_qa import CompositeBrainQA
from llm.knowledge_brain_qa import KnowledgeBrainQA
from logger import get_logger
from models.settings import BrainSettings, get_supabase_client
from modules.brain.entity.brain_entity import BrainType, RoleEnum
from modules.brain.service.brain_authorization_service import (
validate_brain_authorization,
)
from modules.brain.service.brain_service import BrainService
from modules.chat.controller.chat.interface import ChatInterface
from modules.chat.service.chat_service import ChatService
from vectorstore.supabase import CustomSupabaseVectorStore
chat_service = ChatService()
logger = get_logger(__name__)
models_supporting_function_calls = [
"gpt-4",
@ -40,14 +49,42 @@ class BrainfulChat(ChatInterface):
streaming,
prompt_id,
user_id,
chat_question,
):
brain = brain_service.get_brain_by_id(brain_id)
if not brain:
raise HTTPException(status_code=404, detail="Brain not found")
brain_id_to_use = brain_id
if not brain_id:
brain_settings = BrainSettings()
supabase_client = get_supabase_client()
embeddings = None
if brain_settings.ollama_api_base_url:
embeddings = OllamaEmbeddings(
base_url=brain_settings.ollama_api_base_url
) # pyright: ignore reportPrivateUsage=none
else:
embeddings = OpenAIEmbeddings()
vector_store = CustomSupabaseVectorStore(
supabase_client, embeddings, table_name="vectors"
)
# Get the first question from the chat_question
logger.info(f"Finding brain closest to {chat_question}")
logger.info("🔥🔥🔥🔥🔥")
question = chat_question.question
logger.info(f"Question is {question}")
history = chat_service.get_chat_history(chat_id)
if history:
question = history[0].user_message
logger.info(f"Question is {question}")
brain_id_to_use = vector_store.find_brain_closest_query(question)
logger.info(f"Found brain {brain_id_to_use}")
logger.info("🧠🧠🧠")
brain = brain_service.get_brain_by_id(brain_id_to_use)
logger.info(f"Brain type: {brain.brain_type}")
logger.info(f"Id is {brain.brain_id}")
logger.info(f"Type of brain_id is {type(brain.brain_id)}")
if (
brain.brain_type == BrainType.DOC
brain
and brain.brain_type == BrainType.DOC
or model not in models_supporting_function_calls
):
return KnowledgeBrainQA(
@ -55,7 +92,7 @@ class BrainfulChat(ChatInterface):
model=model,
max_tokens=max_tokens,
temperature=temperature,
brain_id=brain_id,
brain_id=str(brain.brain_id),
streaming=streaming,
prompt_id=prompt_id,
)
@ -65,19 +102,20 @@ class BrainfulChat(ChatInterface):
model=model,
max_tokens=max_tokens,
temperature=temperature,
brain_id=brain_id,
brain_id=str(brain.brain_id),
streaming=streaming,
prompt_id=prompt_id,
user_id=user_id,
)
return APIBrainQA(
chat_id=chat_id,
model=model,
max_tokens=max_tokens,
temperature=temperature,
brain_id=brain_id,
streaming=streaming,
prompt_id=prompt_id,
user_id=user_id,
)
if brain.brain_type == BrainType.API:
return APIBrainQA(
chat_id=chat_id,
model=model,
max_tokens=max_tokens,
temperature=temperature,
brain_id=str(brain.brain_id),
streaming=streaming,
prompt_id=prompt_id,
user_id=user_id,
)

View File

@ -17,5 +17,6 @@ class ChatInterface(ABC):
streaming,
prompt_id,
user_id,
chat_question,
):
pass

View File

@ -7,6 +7,7 @@ from fastapi.responses import StreamingResponse
from middlewares.auth import AuthBearer, get_current_user
from models.user_usage import UserUsage
from modules.brain.service.brain_service import BrainService
from modules.chat.controller.chat.brainful_chat import BrainfulChat
from modules.chat.controller.chat.factory import get_chat_strategy
from modules.chat.controller.chat.utils import NullableUUID, check_user_requests_limit
from modules.chat.dto.chats import ChatItem, ChatQuestion
@ -166,6 +167,7 @@ async def create_question_handler(
streaming=False,
prompt_id=chat_question.prompt_id,
user_id=current_user.id,
chat_question=chat_question,
)
chat_answer = gpt_answer_generator.generate_answer(
@ -196,7 +198,7 @@ async def create_stream_question_handler(
| None = Query(..., description="The ID of the brain"),
current_user: UserIdentity = Depends(get_current_user),
) -> StreamingResponse:
chat_instance = get_chat_strategy(brain_id)
chat_instance = BrainfulChat()
chat_instance.validate_authorization(user_id=current_user.id, brain_id=brain_id)
user_daily_usage = UserUsage(
@ -215,7 +217,6 @@ async def create_stream_question_handler(
fallback_model = "gpt-3.5-turbo-1106"
fallback_temperature = 0
fallback_max_tokens = 256
if brain_id:
brain = brain_service.get_brain_by_id(brain_id)
if brain:
@ -240,8 +241,9 @@ async def create_stream_question_handler(
temperature=chat_question.temperature, # type: ignore
streaming=True,
prompt_id=chat_question.prompt_id,
brain_id=str(brain_id),
brain_id=brain_id,
user_id=current_user.id,
chat_question=chat_question,
)
return StreamingResponse(

View File

@ -12,6 +12,8 @@ class GetChatHistoryOutput(BaseModel):
message_time: Optional[str]
prompt_title: Optional[str] | None
brain_name: Optional[str] | None
brain_id: Optional[UUID] | None
metadata: Optional[dict] | None
def dict(self, *args, **kwargs):
chat_history = super().dict(*args, **kwargs)

View File

@ -26,6 +26,7 @@ class ChatHistory:
message_time: str
prompt_id: Optional[UUID]
brain_id: Optional[UUID]
metadata: Optional[dict] = None
def __init__(self, chat_dict: dict):
self.chat_id = chat_dict.get("chat_id", "")
@ -36,6 +37,7 @@ class ChatHistory:
self.prompt_id = chat_dict.get("prompt_id")
self.brain_id = chat_dict.get("brain_id")
self.metadata = chat_dict.get("metadata")
def to_dict(self):
return asdict(self)

View File

@ -79,7 +79,9 @@ class ChatService:
assistant=message.assistant,
message_time=message.message_time,
brain_name=brain.name if brain else None,
brain_id=brain.id if brain else None,
prompt_title=prompt.title if prompt else None,
metadata=message.metadata,
)
)
return enriched_history
@ -132,6 +134,7 @@ class ChatService:
message_id: str,
user_message: str = None, # pyright: ignore reportPrivateUsage=none
assistant: str = None, # pyright: ignore reportPrivateUsage=none
metadata: dict = None, # pyright: ignore reportPrivateUsage=none
) -> ChatHistory:
if not message_id:
logger.error("No message_id provided")
@ -145,6 +148,9 @@ class ChatService:
if assistant is not None:
updates["assistant"] = assistant
if metadata is not None:
updates["metadata"] = metadata
updated_message = None
if updates:

View File

@ -1,10 +1,14 @@
from typing import Any, List
from uuid import UUID
from langchain.docstore.document import Document
from langchain.embeddings.base import Embeddings
from langchain.vectorstores import SupabaseVectorStore
from logger import get_logger
from supabase.client import Client
logger = get_logger(__name__)
class CustomSupabaseVectorStore(SupabaseVectorStore):
"""A custom vector store that uses the match_vectors table instead of the vectors table."""
@ -21,13 +25,39 @@ class CustomSupabaseVectorStore(SupabaseVectorStore):
super().__init__(client, embedding, table_name)
self.brain_id = brain_id
def find_brain_closest_query(
self,
query: str,
k: int = 6,
table: str = "match_brain",
threshold: float = 0.5,
) -> UUID | None:
vectors = self._embedding.embed_documents([query])
query_embedding = vectors[0]
res = self._client.rpc(
table,
{
"query_embedding": query_embedding,
"match_count": k,
},
).execute()
# Get the brain_id of the brain that is most similar to the query
logger.info(f"Found {len(res.data)} brains")
logger.info(res.data)
logger.info("🔥🔥🔥🔥🔥")
brain_id = res.data[0].get("id", None)
if not brain_id:
return None
return str(brain_id)
def similarity_search(
self,
query: str,
k: int = 6,
table: str = "match_vectors",
threshold: float = 0.5,
**kwargs: Any
**kwargs: Any,
) -> List[Document]:
vectors = self._embedding.embed_documents([query])
query_embedding = vectors[0]

View File

@ -130,14 +130,6 @@ module.exports = {
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/strict-boolean-expressions": [
"error",
{
allowString: false,
allowNumber: false,
allowNullableObject: true,
},
],
"@typescript-eslint/ban-ts-comment": [
"error",
{

View File

@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToPreviousPageOrChatPage } from "@/lib/helpers/redirectToPreviousPageOrChatPage";
import { redirectToPreviousPageOrSearchPage } from "@/lib/helpers/redirectToPreviousPageOrSearchPage";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -13,7 +13,7 @@ export const useLogin = () => {
useEffect(() => {
if (session?.user !== undefined) {
void track("SIGNED_IN");
redirectToPreviousPageOrChatPage();
redirectToPreviousPageOrSearchPage();
}
}, [session?.user]);
};

View File

@ -2,7 +2,7 @@
import { useEffect } from "react";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToPreviousPageOrChatPage } from "@/lib/helpers/redirectToPreviousPageOrChatPage";
import { redirectToPreviousPageOrSearchPage } from "@/lib/helpers/redirectToPreviousPageOrSearchPage";
import {
DemoSection,
@ -21,7 +21,7 @@ const HomePage = (): JSX.Element => {
useEffect(() => {
if (session?.user !== undefined) {
redirectToPreviousPageOrChatPage();
redirectToPreviousPageOrSearchPage();
}
}, [session?.user]);

View File

@ -8,13 +8,13 @@ import { PropsWithChildren, useEffect } from "react";
import { Menu } from "@/lib/components/Menu/Menu";
import { useOutsideClickListener } from "@/lib/components/Menu/hooks/useOutsideClickListener";
import { NotificationBanner } from "@/lib/components/NotificationBanner";
import { BrainProvider } from "@/lib/context";
import { BrainProvider, ChatProvider } from "@/lib/context";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { SideBarProvider } from "@/lib/context/SidebarProvider/sidebar-provider";
import { ChatsProvider } from "@/lib/context/ChatsProvider";
import { MenuProvider } from "@/lib/context/MenuProvider/Menu-provider";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { UpdateMetadata } from "@/lib/helpers/updateMetadata";
import { usePageTracking } from "@/services/analytics/june/usePageTracking";
import "../lib/config/LocaleConfig/i18n";
if (
@ -30,8 +30,7 @@ if (
// This wrapper is used to make effect calls at a high level in app rendering.
const App = ({ children }: PropsWithChildren): JSX.Element => {
const { fetchAllBrains, fetchDefaultBrain, fetchPublicPrompts } =
useBrainContext();
const { fetchAllBrains, fetchDefaultBrain, fetchPublicPrompts } = useBrainContext();
const { onClickOutside } = useOutsideClickListener();
const { session } = useSupabase();
@ -39,11 +38,11 @@ const App = ({ children }: PropsWithChildren): JSX.Element => {
useEffect(() => {
if (session?.user) {
void fetchAllBrains();
void fetchDefaultBrain();
void fetchPublicPrompts();
posthog.identify(session.user.id, { email: session.user.email });
posthog.startSessionRecording();
void fetchAllBrains();
void fetchDefaultBrain();
void fetchPublicPrompts();
posthog.identify(session.user.id, { email: session.user.email });
posthog.startSessionRecording();
}
}, [session]);
@ -52,11 +51,11 @@ const App = ({ children }: PropsWithChildren): JSX.Element => {
<div className="flex flex-1 flex-col overflow-auto">
<NotificationBanner />
<div className="relative h-full w-full flex justify-stretch items-stretch overflow-auto">
<Menu />
<div onClick={onClickOutside} className="flex-1">
{children}
</div>
<UpdateMetadata />
<Menu />
<div onClick={onClickOutside} className="flex-1">
{children}
</div>
<UpdateMetadata />
</div>
</div>
</PostHogProvider>
@ -69,9 +68,13 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => {
return (
<QueryClientProvider client={queryClient}>
<BrainProvider>
<SideBarProvider>
<App>{children}</App>
</SideBarProvider>
<MenuProvider>
<ChatsProvider>
<ChatProvider>
<App>{children}</App>
</ChatProvider>
</ChatsProvider>
</MenuProvider>
</BrainProvider>
</QueryClientProvider>
);

View File

@ -11,7 +11,7 @@ import {
ChatProviderMock,
} from "@/lib/context/ChatProvider/mocks/ChatProviderMock";
import { KnowledgeToFeedProvider } from "@/lib/context/KnowledgeToFeedProvider";
import { SideBarProvider } from "@/lib/context/SidebarProvider/sidebar-provider";
import { MenuProvider } from "@/lib/context/MenuProvider/Menu-provider";
import {
SupabaseContextMock,
SupabaseProviderMock,
@ -104,9 +104,9 @@ describe("Chat page", () => {
<ChatProviderMock>
<SupabaseProviderMock>
<BrainProviderMock>
<SideBarProvider>
<MenuProvider>
<SelectedChatPage />,
</SideBarProvider>
</MenuProvider>
</BrainProviderMock>
</SupabaseProviderMock>
</ChatProviderMock>

View File

@ -13,7 +13,6 @@ import { ChangeBrainButton } from "./components/ChangeBrainButton";
import { ChatHistoryButton } from "./components/ChatHistoryButton/ChatHistoryButton";
import { ConfigModal } from "./components/ConfigModal";
import { FeedCardTrigger } from "./components/FeedCardTrigger";
import { NewDiscussionButton } from "./components/NewDiscussionButton";
import { SelectedBrainTag } from "./components/SelectedBrainTag";
export const ActionsModal = (): JSX.Element => {
@ -40,7 +39,6 @@ export const ActionsModal = (): JSX.Element => {
className="min-h-[200px] w-[250px]"
>
<SelectedBrainTag />
<NewDiscussionButton />
<FeedCardTrigger />
<ChatHistoryButton />
<ConfigModal />

View File

@ -12,7 +12,7 @@ import {
ChatProviderMock,
} from "@/lib/context/ChatProvider/mocks/ChatProviderMock";
import { KnowledgeToFeedProvider } from "@/lib/context/KnowledgeToFeedProvider";
import { SideBarProvider } from "@/lib/context/SidebarProvider/sidebar-provider";
import { MenuProvider } from "@/lib/context/MenuProvider/Menu-provider";
import { SupabaseContextMock } from "@/lib/context/SupabaseProvider/mocks/SupabaseProviderMock";
vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({
@ -91,9 +91,9 @@ describe("ChatsList", () => {
<KnowledgeToFeedProvider>
<ChatProviderMock>
<BrainProviderMock>
<SideBarProvider>
<MenuProvider>
<ChatsList />
</SideBarProvider>
</MenuProvider>
</BrainProviderMock>
</ChatProviderMock>
</KnowledgeToFeedProvider>
@ -109,9 +109,9 @@ describe("ChatsList", () => {
<KnowledgeToFeedProvider>
<ChatProviderMock>
<BrainProviderMock>
<SideBarProvider>
<MenuProvider>
<ChatsList />
</SideBarProvider>
</MenuProvider>
</BrainProviderMock>
</ChatProviderMock>
</KnowledgeToFeedProvider>

View File

@ -1,20 +0,0 @@
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { LuChevronRight, LuMessageSquarePlus } from "react-icons/lu";
import { Button } from "./Button";
export const NewDiscussionButton = (): JSX.Element => {
const { t } = useTranslation(["chat"]);
return (
<Link href="/chat">
<Button
label={t("new_discussion")}
startIcon={<LuMessageSquarePlus size={18} />}
endIcon={<LuChevronRight size={18} />}
className="w-full"
/>
</Link>
);
};

View File

@ -1,25 +0,0 @@
import { useTranslation } from "react-i18next";
import { LuPanelLeftClose, LuPanelRightClose } from "react-icons/lu";
import Button from "@/lib/components/ui/Button";
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
export const MenuControlButton = (): JSX.Element => {
const { isOpened, setIsOpened } = useSideBarContext();
const Icon = isOpened ? LuPanelLeftClose : LuPanelRightClose;
const { t } = useTranslation("chat");
return (
<Button
variant="tertiary"
className="px-2 py-0"
type="button"
onClick={() => setIsOpened(!isOpened)}
>
<div className="flex flex-col items-center justify-center gap-1">
<Icon className="text-2xl md:text-3xl self-center text-accent" />
<span className="text-xs">{t("menu")}</span>
</div>
</Button>
);
};

View File

@ -0,0 +1,13 @@
@use '@/styles/Colors.module.scss';
@use '@/styles/IconSizes.module.scss';
@use '@/styles/Spacings.module.scss';
.menu_icon {
width: IconSizes.$medium;
height: IconSizes.$medium;
cursor: pointer;
&:hover {
color: Colors.$accent;
}
}

View File

@ -0,0 +1,18 @@
import { GiHamburgerMenu } from "react-icons/gi";
import { LuArrowLeftFromLine } from "react-icons/lu";
import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext";
import styles from './MenuControlButton.module.scss'
export const MenuControlButton = (): JSX.Element => {
const { isOpened, setIsOpened } = useMenuContext();
const Icon = isOpened ? LuArrowLeftFromLine : GiHamburgerMenu;
return (
<Icon
className={styles.menu_icon}
onClick={() => setIsOpened(!isOpened)}
/>
);
};

View File

@ -6,7 +6,6 @@ import Button from "@/lib/components/ui/Button";
import { OnboardingQuestions } from "./components";
import { ActionsModal } from "./components/ActionsModal/ActionsModal";
import { ChatEditor } from "./components/ChatEditor/ChatEditor";
import { MenuControlButton } from "./components/MenuControlButton";
import { useChatInput } from "./hooks/useChatInput";
export const ChatInput = (): JSX.Element => {
@ -27,7 +26,6 @@ export const ChatInput = (): JSX.Element => {
}}
className="sticky bottom-0 bg-white dark:bg-black w-full flex items-center gap-2 z-20 p-2"
>
<MenuControlButton />
<div className="flex flex-1">
<ChatEditor

View File

@ -2,7 +2,6 @@ import { useChatContext } from "@/lib/context";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { ChatDialogue } from "./components/ChatDialogue";
import { ShortCuts } from "./components/ShortCuts";
import { getMergedChatMessagesWithDoneStatusNotificationsReduced } from "./utils/getMergedChatMessagesWithDoneStatusNotificationsReduced";
export const ChatDialogueArea = (): JSX.Element => {
@ -20,5 +19,5 @@ export const ChatDialogueArea = (): JSX.Element => {
return <ChatDialogue chatItems={chatItems} />;
}
return <ShortCuts />;
return <></>;
};

View File

@ -7,8 +7,14 @@ type QADisplayProps = {
content: ChatMessage;
};
export const QADisplay = ({ content }: QADisplayProps): JSX.Element => {
const { assistant, message_id, user_message, brain_name, prompt_title } =
content;
const {
assistant,
message_id,
user_message,
brain_name,
prompt_title,
metadata,
} = content;
return (
<>
@ -18,6 +24,7 @@ export const QADisplay = ({ content }: QADisplayProps): JSX.Element => {
text={user_message}
promptName={prompt_title}
brainName={brain_name}
metadata={metadata} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
/>
<MessageRow
key={`assistant-${message_id}`}
@ -25,6 +32,7 @@ export const QADisplay = ({ content }: QADisplayProps): JSX.Element => {
text={assistant}
brainName={brain_name}
promptName={prompt_title}
metadata={metadata} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
/>
</>
);

View File

@ -13,11 +13,21 @@ type MessageRowProps = {
brainName?: string | null;
promptName?: string | null;
children?: React.ReactNode;
metadata?: {
sources?: [string] | [];
};
};
export const MessageRow = React.forwardRef(
(
{ speaker, text, brainName, promptName, children }: MessageRowProps,
{
speaker,
text,
brainName,
promptName,
children,
metadata,
}: MessageRowProps,
ref: React.Ref<HTMLDivElement>
) => {
const {
@ -32,18 +42,10 @@ export const MessageRow = React.forwardRef(
text,
});
let messageContent = text ?? "";
let sourcesContent = "";
const messageContent = text ?? "";
const sourcesContent = metadata?.sources ?? [];
const sourcesIndex = messageContent.lastIndexOf("**Sources:**");
const hasSources = sourcesIndex !== -1;
if (hasSources) {
sourcesContent = messageContent
.substring(sourcesIndex + "**Sources:**".length)
.trim();
messageContent = messageContent.substring(0, sourcesIndex).trim();
}
const hasSources = Boolean(sourcesContent);
return (
<div className={containerWrapperClasses}>

View File

@ -5,7 +5,7 @@ import { FaQuestionCircle } from "react-icons/fa";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
type SourcesButtonProps = {
sources: string;
sources: [string] | [];
};
export const SourcesButton = ({ sources }: SourcesButtonProps): JSX.Element => {
@ -37,7 +37,7 @@ export const SourcesButton = ({ sources }: SourcesButtonProps): JSX.Element => {
const sourcesList = (
<ul className="list-disc list-inside">
{sources.split(", ").map((source, index) => (
{sources.map((source, index) => (
<li key={index} className="truncate">
{source.trim()}
</li>

View File

@ -1,45 +0,0 @@
import { useTranslation } from "react-i18next";
import { MdKeyboardCommandKey } from "react-icons/md";
import { ShortcutItem } from "./components";
export const ShortCuts = (): JSX.Element => {
const { t } = useTranslation(["chat"]);
const shortcuts = [
{
content: [t("shortcut_select_brain"), t("shortcut_choose_prompt")],
},
// {
// content: [
// t("shortcut_select_file"),
// t("shortcut_create_brain"),
// t("shortcut_feed_brain"),
// t("shortcut_create_prompt"),
// ],
// },
// {
// content: [
// t("shortcut_manage_brains"),
// t("shortcut_go_to_user_page"),
// t("shortcut_go_to_shortcuts"),
// ],
// },
];
return (
<>
<div className="flex items-center justify-center">
<MdKeyboardCommandKey className="text-4xl mr-2" />
<span className="font-bold text-2xl">{t("keyboard_shortcuts")}</span>
</div>
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-row space-x-4">
{shortcuts.map((shortcut, index) => (
<ShortcutItem key={index} content={shortcut.content} />
))}
</div>
</div>
</>
);
};

View File

@ -1,15 +0,0 @@
type ShortcutItemProps = {
content: string[];
};
export const ShortcutItem = ({ content }: ShortcutItemProps): JSX.Element => {
return (
<div className="bg-gray-100 rounded-lg p-4 flex-grow">
{content.map((text, index) => (
<p className="text-gray-500" key={index}>
{text}
</p>
))}
</div>
);
};

View File

@ -1 +0,0 @@
export * from './ShortCuts';

View File

@ -58,14 +58,12 @@ export const useChat = () => {
let currentChatId = chatId;
let shouldUpdateUrl = false;
//if chatId is not set, create a new chat. Chat name is from the first question
if (currentChatId === undefined) {
const chat = await createChat(getChatNameFromQuestion(question));
currentChatId = chat.chat_id;
setChatId(currentChatId);
shouldUpdateUrl = true;
router.push(`/chat/${currentChatId}`);
void queryClient.invalidateQueries({
queryKey: [CHATS_DATA_KEY],
});
@ -95,9 +93,6 @@ export const useChat = () => {
callback?.();
await addStreamQuestion(currentChatId, chatQuestion);
if (shouldUpdateUrl) {
router.replace(`/chat/${currentChatId}`);
}
} catch (error) {
console.error({ error });

View File

@ -21,7 +21,7 @@ const SelectedChatPage = (): JSX.Element => {
<div
className={cn(
"flex flex-col flex-1 items-center justify-stretch w-full h-full overflow-hidden",
shouldDisplayFeedCard ? "bg-chat-bg-gray" : "bg-tertiary",
shouldDisplayFeedCard ? "bg-chat-bg-gray" : "bg-ivory",
"dark:bg-black transition-colors ease-out duration-500"
)}
data-testid="chat-page"

View File

@ -16,6 +16,9 @@ export type ChatMessage = {
message_time: string;
prompt_title?: string;
brain_name?: string;
metadata?: {
sources?: [string];
};
};
type NotificationStatus = "Pending" | "Done";

View File

@ -1,8 +1,8 @@
"use client";
import { ReactNode } from "react";
import { usePathname, useRouter } from "next/navigation";
import { ReactNode, useEffect, useState } from "react";
import { ChatProvider, KnowledgeToFeedProvider } from "@/lib/context";
import { ChatsProvider } from "@/lib/context/ChatsProvider/chats-provider";
import { KnowledgeToFeedProvider } from "@/lib/context";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
@ -12,20 +12,29 @@ interface LayoutProps {
const Layout = ({ children }: LayoutProps): JSX.Element => {
const { session } = useSupabase();
const pathname = usePathname();
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
if (session === null) {
redirectToLogin();
useEffect(() => {
if (session === null) {
redirectToLogin();
} else if (pathname === '/chat') {
router.push('/search');
} else {
setIsLoading(false);
}
}, [session, pathname, router]);
if (isLoading) {
return <></>
}
return (
<KnowledgeToFeedProvider>
<ChatsProvider>
<ChatProvider>
<div className="relative h-full w-full flex justify-stretch items-stretch overflow-auto">
{children}
</div>
</ChatProvider>
</ChatsProvider>
<div className="relative h-full w-full flex justify-stretch items-stretch overflow-auto">
{children}
</div>
</KnowledgeToFeedProvider>
);
};

View File

@ -0,0 +1,43 @@
@use '@/styles/Colors.module.scss';
@use '@/styles/IconSizes.module.scss';
@use '@/styles/ScreenSizes.module.scss';
@use '@/styles/Spacings.module.scss';
@use '@/styles/Typography.module.scss';
.search_page_container {
background-color: Colors.$ivory;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.main_container {
display: flex;
flex-direction: column;
row-gap: Spacings.$spacing05;
position: relative;
width: 50vw;
margin-inline: auto;
@media (max-width: ScreenSizes.$small) {
width: 100%;
margin-inline: Spacings.$spacing07;
}
.quivr_logo_wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.quivr_text {
@include Typography.H1;
.quivr_text_primary {
color: Colors.$primary;
}
}
}
}
}

View File

@ -0,0 +1,35 @@
"use client";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar";
import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext";
import styles from "./page.module.scss";
const Search = (): JSX.Element => {
const {setIsOpened} = useMenuContext();
const pathname = usePathname()
useEffect(() => {
setIsOpened(false);
}, [pathname]);
return (
<div className={styles.search_page_container}>
<div className={styles.main_container}>
<div className={styles.quivr_logo_wrapper}>
<QuivrLogo size={80} color="black" />
<div className={styles.quivr_text}>
<span>Talk to </span>
<span className={styles.quivr_text_primary}>Quivr</span>
</div>
</div>
<SearchBar />
</div>
</div >
);
};
export default Search;

View File

@ -26,9 +26,9 @@ const UserPage = (): JSX.Element => {
return (
<>
<main className="container lg:w-2/3 mx-auto py-10 px-5">
<Link href="/chat">
<Link href="/search">
<Button className="mb-5" variant="primary">
{t("chat:back_to_chat")}
{t("chat:back_to_search")}
</Button>
</Link>
<Card className="mb-5 shadow-sm hover:shadow-none">

View File

@ -1,6 +1,6 @@
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next';
import { ChatEntity } from "@/app/chat/[chatId]/types";
import { useChatApi } from "@/lib/api/chat/useChatApi";
@ -27,7 +27,7 @@ export const useChatsListItem = (chat: ChatEntity) => {
chats.filter((currentChat) => currentChat.chat_id !== chatId)
);
// TODO: Change route only when the current chat is being deleted
void router.push("/chat");
void router.push("/search");
publish({
text: t('chatDeleted',{ id: chatId,ns:'chat'}) ,
variant: "success",

View File

@ -5,7 +5,7 @@ export const Logo = (): JSX.Element => {
return (
<Link
data-testid="app-logo"
href={"/chat"}
href={"/search"}
className="flex items-center gap-4"
>
<Image

View File

@ -0,0 +1,16 @@
@use '@/styles/Colors.module.scss';
@use '@/styles/Spacings.module.scss';
@use '@/styles/ZIndex.module.scss';
.menu_control_button_wrapper {
background-color: transparent;
position: absolute;
top: Spacings.$spacing05;
left: Spacings.$spacing05;
transition: margin-left 0.2s ease-in-out;
z-index: ZIndex.$overlay;
&.shifted {
margin-left: 260px;
}
}

View File

@ -1,10 +1,11 @@
import { MotionConfig } from "framer-motion";
import { usePathname } from "next/navigation";
import { LuPanelLeftOpen } from "react-icons/lu";
import { MenuControlButton } from "@/app/chat/[chatId]/components/ActionsBar/components/ChatInput/components/MenuControlButton/MenuControlButton";
import { nonProtectedPaths } from "@/lib/config/routesConfig";
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext";
import styles from './Menu.module.scss'
import { AnimatedDiv } from "./components/AnimationDiv";
import { BrainsManagementButton } from "./components/BrainsManagementButton";
import { DiscussionButton } from "./components/DiscussionButton";
@ -13,57 +14,53 @@ import { MenuHeader } from "./components/MenuHeader";
import { ParametersButton } from "./components/ParametersButton";
import { ProfileButton } from "./components/ProfileButton";
import { UpgradeToPlus } from "./components/UpgradeToPlus";
import Button from "../ui/Button";
export const Menu = (): JSX.Element => {
const pathname = usePathname() ?? "";
const { isOpened } = useMenuContext();
const pathname = usePathname() ?? "";
const { setIsOpened } = useSideBarContext();
if (nonProtectedPaths.includes(pathname)) {
return <></>;
}
if (nonProtectedPaths.includes(pathname)) {
return <></>;
}
const displayedOnPages = ["/chat", "/library", "/brains-management", "/search"];
const displayedOnPages = ["/chat", "/library", "/brains-management"];
const isMenuDisplayed = displayedOnPages.some((page) =>
pathname.includes(page)
);
const isMenuDisplayed = displayedOnPages.some((page) =>
pathname.includes(page)
);
if (!isMenuDisplayed) {
return <></>;
}
if (!isMenuDisplayed) {
return <></>;
}
/* eslint-disable @typescript-eslint/restrict-template-expressions */
return (
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.2 }}>
<div
className="flex flex-col fixed sm:sticky top-0 left-0 h-full overflow-visible z-[1000] border-r border-black/10 dark:border-white/25 bg-highlight"
>
<AnimatedDiv>
<div className="flex flex-col flex-1 p-4 gap-4 h-full">
<MenuHeader />
<div className="flex flex-1 w-full">
<div className="w-full gap-2 flex flex-col">
<DiscussionButton />
<ExplorerButton />
<BrainsManagementButton />
<ParametersButton />
</div>
return (
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.2 }}>
<div
className="flex flex-col fixed sm:sticky top-0 left-0 h-full overflow-visible z-[1000] border-r border-black/10 dark:border-white/25 bg-highlight"
>
<AnimatedDiv>
<div className="flex flex-col flex-1 p-4 gap-4 h-full">
<MenuHeader />
<div className="flex flex-1 w-full">
<div className="w-full gap-2 flex flex-col">
<DiscussionButton />
<ExplorerButton />
<BrainsManagementButton />
<ParametersButton />
</div>
</div>
<div>
<UpgradeToPlus />
<ProfileButton />
</div>
</div>
</AnimatedDiv>
</div>
<div>
<UpgradeToPlus />
<ProfileButton />
<div className={`${styles.menu_control_button_wrapper} ${isOpened ? styles.shifted : ''}`}>
<MenuControlButton />
</div>
</div>
</AnimatedDiv>
</div>
<Button
variant="tertiary"
onClick={() => setIsOpened((prev) => !prev)}
className="absolute top-2 left-2 sm:hidden z-50"
>
<LuPanelLeftOpen className="text-primary" size={30} />
</Button>
</MotionConfig>
);
</MotionConfig>
);
};

View File

@ -1,12 +1,12 @@
import { motion } from "framer-motion";
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext";
type AnimatedDivProps = {
children: React.ReactNode;
};
export const AnimatedDiv = ({ children }: AnimatedDivProps): JSX.Element => {
const { isOpened } = useSideBarContext();
const { isOpened } = useMenuContext();
const OPENED_MENU_WIDTH = 260;
return (

View File

@ -8,13 +8,13 @@ import { cn } from "@/lib/utils";
export const DiscussionButton = (): JSX.Element => {
const pathname = usePathname() ?? "";
const isSelected = pathname.includes("/chat");
const isSelected = pathname.includes("/search");
const { t } = useTranslation("chat");
return (
<Link href="/chat">
<Link href="/search">
<Button
label={t("chat")}
label={t("search")}
startIcon={<LuMessageSquare />}
endIcon={<LuChevronRight size={18} />}
className={cn(

View File

@ -1,9 +1,9 @@
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext";
import { useDevice } from "@/lib/hooks/useDevice";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useOutsideClickListener = () => {
const { isOpened, setIsOpened } = useSideBarContext();
const { isOpened, setIsOpened } = useMenuContext();
const { isMobile } = useDevice();
const onClickOutside = () => {

View File

@ -1,71 +0,0 @@
import { motion, MotionConfig } from "framer-motion";
import { LuPanelLeftOpen } from "react-icons/lu";
import { SidebarHeader } from "@/lib/components/Sidebar/components/SidebarHeader";
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
import { cn } from "@/lib/utils";
import {
SidebarFooter,
SidebarFooterButtons,
} from "./components/SidebarFooter/SidebarFooter";
type SidebarProps = {
children: React.ReactNode;
showButtons?: SidebarFooterButtons[];
};
export const Sidebar = ({
children,
showButtons,
}: SidebarProps): JSX.Element => {
const { isOpened, setIsOpened } = useSideBarContext();
return (
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.2 }}>
<motion.div
drag="x"
dragConstraints={{ right: 0, left: 0 }}
dragElastic={0.15}
onDragEnd={(event, info) => {
if (info.offset.x > 100 && !isOpened) {
setIsOpened(true);
} else if (info.offset.x < -100 && isOpened) {
setIsOpened(false);
}
}}
className="flex flex-col fixed sm:sticky top-0 left-0 h-full overflow-visible z-30 border-r border-black/10 dark:border-white/25 bg-white dark:bg-black"
>
{!isOpened && (
<button
title="Open Sidebar"
type="button"
className="absolute p-3 text-2xl bg-red top-5 -right-20 hover:text-primary dark:hover:text-gray-200 transition-colors"
data-testid="open-sidebar-button"
onClick={() => setIsOpened(true)}
>
<LuPanelLeftOpen />
</button>
)}
<motion.div
initial={{
width: isOpened ? "18rem" : "0px",
}}
animate={{
width: isOpened ? "18rem" : "0px",
opacity: isOpened ? 1 : 0.5,
boxShadow: isOpened
? "10px 10px 16px rgba(0, 0, 0, 0)"
: "10px 10px 16px rgba(0, 0, 0, 0.5)",
}}
className={cn("overflow-hidden flex flex-col flex-1 max-w-xs")}
data-testid="sidebar"
>
<SidebarHeader />
<div className="overflow-auto flex flex-col flex-1">{children}</div>
{showButtons && <SidebarFooter showButtons={showButtons} />}
</motion.div>
</motion.div>
</MotionConfig>
);
};

View File

@ -1,81 +0,0 @@
/* eslint-disable max-lines */
import {
act,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { Sidebar } from "@/lib/components/Sidebar/Sidebar";
import { SideBarProvider } from "@/lib/context/SidebarProvider/sidebar-provider";
import { useDevice } from "@/lib/hooks/useDevice";
vi.mock("@/lib/hooks/useDevice");
const renderSidebar = async () => {
await act(() =>
render(
<SideBarProvider>
<Sidebar>
<div data-testid="sidebar-test-content">📦</div>
</Sidebar>
</SideBarProvider>
)
);
};
describe("Sidebar", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("is rendered by default on desktop", async () => {
vi.mocked(useDevice).mockReturnValue({ isMobile: false });
await renderSidebar();
const closeSidebarButton = screen.queryByTestId("close-sidebar-button");
expect(closeSidebarButton).toBeVisible();
const sidebarContent = screen.getByTestId("sidebar-test-content");
expect(sidebarContent).toBeVisible();
});
it("is hidden by default on mobile", async () => {
vi.mocked(useDevice).mockReturnValue({ isMobile: true });
await renderSidebar();
const closeSidebarButton = screen.queryByTestId("close-sidebar-button");
expect(closeSidebarButton).not.toBeVisible();
const openSidebarButton = screen.queryByTestId("open-sidebar-button");
expect(openSidebarButton).toBeVisible();
const sidebarContent = screen.getByTestId("sidebar-test-content");
expect(sidebarContent).not.toBeVisible();
});
it("shows and hide content when the open and close buttons are clicked", async () => {
vi.mocked(useDevice).mockReturnValue({ isMobile: true });
await renderSidebar();
const openSidebarButton = screen.getByTestId("open-sidebar-button");
expect(openSidebarButton).toBeVisible();
const sidebarContent = screen.queryByTestId("sidebar-test-content");
expect(sidebarContent).not.toBeVisible();
fireEvent.click(openSidebarButton);
await waitFor(() => expect(sidebarContent).toBeVisible());
const closeSidebarButton = screen.getByTestId("close-sidebar-button");
expect(closeSidebarButton);
fireEvent.click(closeSidebarButton);
await waitFor(() => expect(sidebarContent).not.toBeVisible());
});
});

View File

@ -1,34 +0,0 @@
import { Fragment } from "react";
import { BrainManagementButton } from "@/lib/components/Sidebar/components/SidebarFooter/components/BrainManagementButton";
import { MarketPlaceButton } from "./components/MarketplaceButton";
import { UpgradeToPlus } from "./components/UpgradeToPlus";
import { UserButton } from "./components/UserButton";
export type SidebarFooterButtons = "myBrains" | "user" | "upgradeToPlus" | "marketplace";
type SidebarFooterProps = {
showButtons: SidebarFooterButtons[];
};
export const SidebarFooter = ({
showButtons,
}: SidebarFooterProps): JSX.Element => {
const buttons = {
myBrains: <BrainManagementButton />,
marketplace: <MarketPlaceButton />,
upgradeToPlus: <UpgradeToPlus />,
user: <UserButton />,
};
return (
<div className="bg-gray-50 dark:bg-gray-900 border-t dark:border-white/10 mt-auto p-2">
<div className="max-w-screen-xl flex justify-center items-center flex-col">
{showButtons.map((button) => (
<Fragment key={button}> {buttons[button]}</Fragment>
))}
</div>
</div>
);
};

View File

@ -1,20 +0,0 @@
import { useTranslation } from "react-i18next";
import { FaBrain } from "react-icons/fa";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { SidebarFooterButton } from "./SidebarFooterButton";
export const BrainManagementButton = (): JSX.Element => {
const { currentBrainId } = useBrainContext();
const { t } = useTranslation("brain");
return (
<SidebarFooterButton
href={`/brains-management/${currentBrainId ?? ""}`}
icon={<FaBrain className="w-8 h-8" />}
label={t("myBrains")}
data-testid="brain-management-button"
/>
);
};

View File

@ -1,18 +0,0 @@
import { useTranslation } from "react-i18next";
import { PiDotsNineBold } from "react-icons/pi";
import { SidebarFooterButton } from "./SidebarFooterButton";
export const MarketPlaceButton = (): JSX.Element => {
const { t } = useTranslation("brain");
return (
<SidebarFooterButton
href={`/brains-management/library`}
icon={<PiDotsNineBold className="w-8 h-8" />}
label={t("brain_library_button_label")}
data-testid="brain_library_button_label"
/>
);
};

View File

@ -1,36 +0,0 @@
import { useRouter } from "next/navigation";
type SidebarFooterButtonProps = {
icon: JSX.Element;
label: string | JSX.Element;
href?: string;
onClick?: () => void;
};
export const SidebarFooterButton = ({
icon,
label,
href,
onClick,
}: SidebarFooterButtonProps): JSX.Element => {
const router = useRouter();
if (href !== undefined) {
onClick = () => {
void router.push(href);
};
}
return (
<button
type="button"
className="w-full rounded-lg px-5 py-2 text-base flex justify-start items-center gap-4 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-primary focus:outline-none"
onClick={onClick}
>
<span className="w-8 shrink-0">{icon}</span>
<span className="w-full text-ellipsis overflow-hidden text-start">
{label}
</span>
</button>
);
};

View File

@ -1,35 +0,0 @@
import { useTranslation } from "react-i18next";
import { FiUser } from "react-icons/fi";
import { StripePricingModal } from "@/lib/components/Stripe";
import { useUserData } from "@/lib/hooks/useUserData";
import { SidebarFooterButton } from "./SidebarFooterButton";
export const UpgradeToPlus = (): JSX.Element => {
const { userData } = useUserData();
const is_premium = userData?.is_premium;
const { t } = useTranslation("monetization");
if (is_premium === true) {
return <></>;
}
return (
<StripePricingModal
Trigger={
<SidebarFooterButton
icon={<FiUser className="w-8 h-8" />}
label={
<div className="flex justify-between items-center w-full">
{t("upgrade")}
<span className="rounded bg-primary/30 py-1 px-3 text-xs">
{t("new")}
</span>
</div>
}
/>
}
/>
);
};

View File

@ -1,30 +0,0 @@
import { FaCrown } from "react-icons/fa";
import { Avatar } from "@/lib/components/ui/Avatar";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useUserData } from "@/lib/hooks/useUserData";
import { SidebarFooterButton } from "./SidebarFooterButton";
import { useGravatar } from "../../../../../hooks/useGravatar";
export const UserButton = (): JSX.Element => {
const { session } = useSupabase();
const { gravatarUrl } = useGravatar();
const { userData } = useUserData();
const is_premium = userData?.is_premium ?? false;
const email = session?.user.email ?? "";
const label = (
<span className="flex justify-between items-center flex-nowrap gap-1 w-full">
<span className="text-ellipsis overflow-hidden">{email}</span>
{is_premium && <FaCrown className="w-5 h-5 shrink-0" />}
</span>
);
return (
<SidebarFooterButton
href={"/user"}
icon={<Avatar url={gravatarUrl} />}
label={label}
/>
);
};

View File

@ -1,25 +0,0 @@
import { LuPanelLeftClose } from "react-icons/lu";
import { Logo } from "@/lib/components/Logo/Logo";
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
export const SidebarHeader = (): JSX.Element => {
const { setIsOpened } = useSideBarContext();
return (
<div className="p-2 border-b relative">
<div className="max-w-screen-xl flex justify-between items-center pt-3 pl-3">
<Logo />
<button
title="Close Sidebar"
className="p-3 text-2xl bg:white dark:bg-black text-black dark:text-white hover:text-primary dark:hover:text-gray-200 transition-colors"
type="button"
data-testid="close-sidebar-button"
onClick={() => setIsOpened(false)}
>
<LuPanelLeftClose />
</button>
</div>
</div>
);
};

View File

@ -0,0 +1,41 @@
@use '@/styles/Colors.module.scss';
@use '@/styles/IconSizes.module.scss';
@use '@/styles/Spacings.module.scss';
.search_bar_wrapper {
display: flex;
justify-content: space-between;
align-items: center;
gap: Spacings.$spacing03;
background-color: Colors.$white;
padding: Spacings.$spacing05;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
&:hover {
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
}
.search_input {
border: none;
flex: 1;
caret-color: Colors.$accent;
&:focus {
box-shadow: none;
}
}
.search_icon {
width: IconSizes.$big;
height: IconSizes.$big;
color: Colors.$accent;
cursor: pointer;
&.disabled {
color: Colors.$black;
pointer-events: none;
opacity: 0.2;
}
}
}

View File

@ -0,0 +1,52 @@
import { ChangeEvent } from 'react';
import { LuSearch } from "react-icons/lu";
import { useChatInput } from '@/app/chat/[chatId]/components/ActionsBar/components/ChatInput/hooks/useChatInput';
import { useChat } from '@/app/chat/[chatId]/hooks/useChat';
import { useChatContext } from '@/lib/context';
import styles from './SearchBar.module.scss';
export const SearchBar = (): JSX.Element => {
const { message, setMessage } = useChatInput()
const { setMessages } = useChatContext()
const { addQuestion } = useChat()
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
setMessage(event.target.value);
};
const handleEnter = async (event: React.KeyboardEvent<HTMLInputElement>): Promise<void> => {
if (event.key === 'Enter') {
await submit()
}
};
const submit = async (): Promise<void> => {
setMessages([]);
try {
await addQuestion(message);
} catch (error) {
console.error(error);
}
}
/* eslint-disable @typescript-eslint/restrict-template-expressions */
return (
<div className={styles.search_bar_wrapper}>
<input
className={styles.search_input}
type="text"
placeholder="Search"
value={message}
onChange={handleChange}
onKeyDown={(event) => void handleEnter(event)}
/>
<LuSearch
className={`${styles.search_icon} ${!message ? styles.disabled : ''}`}
onClick={() => void submit()}
/>
</div>
)
}

View File

@ -1,36 +1,38 @@
import { usePathname } from "next/navigation";
import { createContext, useEffect, useState } from "react";
import { useDevice } from "@/lib/hooks/useDevice";
type SideBarContextType = {
type MenuContextType = {
isOpened: boolean;
setIsOpened: React.Dispatch<React.SetStateAction<boolean>>;
};
export const SideBarContext = createContext<SideBarContextType | undefined>(
export const MenuContext = createContext<MenuContextType | undefined>(
undefined
);
export const SideBarProvider = ({
export const MenuProvider = ({
children,
}: {
children: React.ReactNode;
}): JSX.Element => {
const { isMobile } = useDevice();
const [isOpened, setIsOpened] = useState(!isMobile);
const [isOpened, setIsOpened] = useState(false);
const pathname = usePathname()
useEffect(() => {
setIsOpened(!isMobile);
setIsOpened(!isMobile && !["/search", "/chat", "/"].includes(pathname!));
}, [isMobile]);
return (
<SideBarContext.Provider
<MenuContext.Provider
value={{
isOpened,
setIsOpened,
}}
>
{children}
</SideBarContext.Provider>
</MenuContext.Provider>
);
};
};

View File

@ -0,0 +1,13 @@
import { useContext } from "react";
import { MenuContext } from "../Menu-provider";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useMenuContext = () => {
const context = useContext(MenuContext);
if (context === undefined) {
throw new Error("useMenuContext must be used within a MenuProvider");
}
return context;
};

View File

@ -1,13 +0,0 @@
import { useContext } from "react";
import { SideBarContext } from "../sidebar-provider";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useSideBarContext = () => {
const context = useContext(SideBarContext);
if (context === undefined) {
throw new Error("useSideBarContext must be used within a SideBarProvider");
}
return context;
};

View File

@ -1,9 +1,9 @@
import { redirect } from "next/navigation";
export const redirectToPreviousPageOrChatPage = (): void => {
export const redirectToPreviousPageOrSearchPage = (): void => {
const previousPage = sessionStorage.getItem("previous-page");
if (previousPage === null) {
redirect("/chat");
redirect("/search");
} else {
sessionStorage.removeItem("previous-page");
redirect(previousPage);

View File

@ -1,6 +1,18 @@
/* eslint-disable max-lines */
const path = require("path");
const nextConfig = {
output: "standalone",
webpack: (config) => {
// Resolve the @ alias for Sass
config.resolve.alias["@"] = path.join(__dirname, ".");
// Important: return the modified config
return config;
},
sassOptions: {
includePaths: [path.join(__dirname, "styles")],
},
redirects: async () => {
return [
{

View File

@ -94,6 +94,7 @@
"react-use": "17.4.0",
"rehype-highlight": "6.0.0",
"rehype-raw": "7.0.0",
"sass": "^1.70.0",
"sharp": "0.32.4",
"tailwind-merge": "1.14.0",
"tailwindcss": "3.4.0",
@ -114,4 +115,4 @@
"react-icons": "4.11.0",
"vitest": "0.32.4"
}
}
}

View File

@ -2,7 +2,7 @@
"actions_bar_placeholder": "Ask a question to a @brain and choose your #prompt",
"add_document": "Add document",
"ask": "Ask a question, or describe a task.",
"back_to_chat": "Back to chat",
"back_to_search": "Back to search",
"brain": "brain",
"brains": "brains",
"change_brain": "Change brain",
@ -44,6 +44,7 @@
"parameters": "Parameters",
"receivedResponse": "Received response. Starting to handle stream...",
"resposeBodyNull": "Response body is null",
"search": "Search",
"shortcut_choose_prompt": "#: Choose a specific prompt",
"shortcut_create_brain": "@+: Create a new brain",
"shortcut_create_prompt": "#+: Create a new custom prompt",
@ -60,4 +61,4 @@
"tooManyRequests": "You have exceeded the number of requests per day. To continue chatting, please upgrade your account or come back tomorrow.",
"welcome": "Welcome",
"yesterday": "Yesterday"
}
}

View File

@ -1 +1,74 @@
{"api_brain":{"addRow":"Agregar fila","description":"Descripción","name":"Nombre","required":"Requerido","type":"Tipo","value":"Valor"},"brain_library_button_label":"Biblioteca de cerebros","brain_management_button_label":"Gestionar cerebro","brain_params":"Parámetros del cerebro","brain_status_label":"Estado","brain_type":"Tipo de cerebro","brainCreated":"Cerebro creado correctamente","brainDescription":"Descripción","brainDescriptionPlaceholder":"Mi nuevo cerebro es acerca de...","brainName":"Nombre del cerebro","brainNamePlaceholder":"Ejemplo: Anotaciones de historia","brainUndefined":"Cerebro no definido","cancel_set_brain_status_to_private":"No, mantenerlo público","cancel_set_brain_status_to_public":"No, mantenerlo privado","composite_brain_composition_invitation":"Conecta tu nuevo cerebro a otros cerebros existentes de tu biblioteca seleccionándolos.","confirm_set_brain_status_to_private":"Sí, establecer como privado","confirm_set_brain_status_to_public":"Sí, establecer como público","copiedToClipboard":"Copiado al portapeles","defaultBrain":"Cerebro por defecto","empty_brain_description":"Sin descripción","errorCreatingBrain":"Error al crear cerebro","errorFetchingBrainUsers":"Error al obtener usuarios del cerebro","errorSendingInvitation":"Un error ocurrió al enviar invitaciones","explore_brains":"Explorar cerebros de la comunidad Quivr","inviteUsers":"Agrega nuevos usuarios","knowledge_source_api":"API","knowledge_source_composite_brain":"Agente","knowledge_source_doc":"Documentos","knowledge_source_label":"Fuente de conocimiento","manage_brain":"Gestionar cerebro","myBrains":"Mis cerebros","newBrain":"Agregar nuevo cerebro","newBrainSubtitle":"Crea un nuevo espacio para tus datos","newBrainTitle":"Agregar nuevo cerebro","noBrainUsers":"Sin usuarios del cerebro","private_brain_description":"Accesible para ti y las personas con las que lo compartas","private_brain_label":"Privado","public_brain_already_subscribed_button_label":"Ya suscrito","public_brain_description":"El cerebro se compartirá con la comunidad de Quivr","public_brain_label":"Público","public_brain_last_update_label":"Última actualización","public_brain_subscribe_button_label":"Suscribirse","public_brain_subscription_success_message":"Te has suscrito con éxito al cerebro","public_brains_search_bar_placeholder":"Buscar cerebros públicos","resources":"Recursos","searchBrain":"Buscar un cerebro","secrets_update_error":"Error al actualizar secretos","secrets_updated":"Secretos actualizados","set_brain_status_to_private_modal_description":"Los usuarios de Quivr ya no podrán utilizar este cerebro y no lo verán en la biblioteca de cerebros.","set_brain_status_to_private_modal_title":"¿Estás seguro de que quieres establecer esto como <span class='text-primary'>Privado</span>?<br/><br/>","set_brain_status_to_public_modal_description":"Cada usuario de Quivr podrá:<br/>- Suscribirse a tu cerebro en la 'biblioteca de cerebros'.<br/>- Usar este cerebro y comprobar las configuraciones de las indicaciones y el modelo.<br/><br/>No tendrán acceso a tus archivos cargados ni a la sección de personas.","set_brain_status_to_public_modal_title":"¿Estás seguro de querer establecer esto como <span class='text-primary'>Público</span>?<br/><br/>","setDefaultBrain":"Asignar cerebro por defecto","shareBrain":"Compartir cerebro {{name}}","shareBrainLink":"Click para copiar y compartir tu cerebro","shareBrainUsers":"Usuarios con acceso","update_secrets_button":"Actualizar secretos","update_secrets_message":"Ingrese su contraseña. Esta información es necesaria para identificarlo al llamar a la API","userRemoved":"Eliminado {{email}} del cerebro","userRemoveFailed":"Error al eliminar {{email}} del cerebro","userRoleUpdated":"Actualizado {{email}} a {{newRole}}","userRoleUpdateFailed":"Error actualizado {{email}} a {{newRole}} ","usersInvited":"Usuarios invitados correctamente","usersWithAccess":"Usuarios con acceso"}
{
"api_brain": {
"addRow": "Agregar fila",
"description": "Descripción",
"name": "Nombre",
"required": "Requerido",
"type": "Tipo",
"value": "Valor"
},
"brain_library_button_label": "Biblioteca de cerebros",
"brain_management_button_label": "Gestionar cerebro",
"brain_params": "Parámetros del cerebro",
"brain_status_label": "Estado",
"brain_type": "Tipo de cerebro",
"brainCreated": "Cerebro creado correctamente",
"brainDescription": "Descripción",
"brainDescriptionPlaceholder": "Mi nuevo cerebro es acerca de...",
"brainName": "Nombre del cerebro",
"brainNamePlaceholder": "Ejemplo: Anotaciones de historia",
"brainUndefined": "Cerebro no definido",
"cancel_set_brain_status_to_private": "No, mantenerlo público",
"cancel_set_brain_status_to_public": "No, mantenerlo privado",
"composite_brain_composition_invitation": "Conecta tu nuevo cerebro a otros cerebros existentes de tu biblioteca seleccionándolos.",
"confirm_set_brain_status_to_private": "Sí, establecer como privado",
"confirm_set_brain_status_to_public": "Sí, establecer como público",
"copiedToClipboard": "Copiado al portapeles",
"defaultBrain": "Cerebro por defecto",
"empty_brain_description": "Sin descripción",
"errorCreatingBrain": "Error al crear cerebro",
"errorFetchingBrainUsers": "Error al obtener usuarios del cerebro",
"errorSendingInvitation": "Un error ocurrió al enviar invitaciones",
"explore_brains": "Explorar cerebros de la comunidad Quivr",
"inviteUsers": "Agrega nuevos usuarios",
"knowledge_source_api": "API",
"knowledge_source_composite_brain": "Agente",
"knowledge_source_doc": "Documentos",
"knowledge_source_label": "Fuente de conocimiento",
"manage_brain": "Gestionar cerebro",
"myBrains": "Mis cerebros",
"newBrain": "Agregar nuevo cerebro",
"newBrainSubtitle": "Crea un nuevo espacio para tus datos",
"newBrainTitle": "Agregar nuevo cerebro",
"noBrainUsers": "Sin usuarios del cerebro",
"private_brain_description": "Accesible para ti y las personas con las que lo compartas",
"private_brain_label": "Privado",
"public_brain_already_subscribed_button_label": "Ya suscrito",
"public_brain_description": "El cerebro se compartirá con la comunidad de Quivr",
"public_brain_label": "Público",
"public_brain_last_update_label": "Última actualización",
"public_brain_subscribe_button_label": "Suscribirse",
"public_brain_subscription_success_message": "Te has suscrito con éxito al cerebro",
"public_brains_search_bar_placeholder": "Buscar cerebros públicos",
"resources": "Recursos",
"search": "Buscar",
"searchBrain": "Buscar un cerebro",
"secrets_update_error": "Error al actualizar secretos",
"secrets_updated": "Secretos actualizados",
"set_brain_status_to_private_modal_description": "Los usuarios de Quivr ya no podrán utilizar este cerebro y no lo verán en la biblioteca de cerebros.",
"set_brain_status_to_private_modal_title": "¿Estás seguro de que quieres establecer esto como <span class='text-primary'>Privado</span>?<br/><br/>",
"set_brain_status_to_public_modal_description": "Cada usuario de Quivr podrá:<br/>- Suscribirse a tu cerebro en la 'biblioteca de cerebros'.<br/>- Usar este cerebro y comprobar las configuraciones de las indicaciones y el modelo.<br/><br/>No tendrán acceso a tus archivos cargados ni a la sección de personas.",
"set_brain_status_to_public_modal_title": "¿Estás seguro de querer establecer esto como <span class='text-primary'>Público</span>?<br/><br/>",
"setDefaultBrain": "Asignar cerebro por defecto",
"shareBrain": "Compartir cerebro {{name}}",
"shareBrainLink": "Click para copiar y compartir tu cerebro",
"shareBrainUsers": "Usuarios con acceso",
"update_secrets_button": "Actualizar secretos",
"update_secrets_message": "Ingrese su contraseña. Esta información es necesaria para identificarlo al llamar a la API",
"userRemoved": "Eliminado {{email}} del cerebro",
"userRemoveFailed": "Error al eliminar {{email}} del cerebro",
"userRoleUpdated": "Actualizado {{email}} a {{newRole}}",
"userRoleUpdateFailed": "Error actualizado {{email}} a {{newRole}} ",
"usersInvited": "Usuarios invitados correctamente",
"usersWithAccess": "Usuarios con acceso"
}

View File

@ -1 +1,64 @@
{"actions_bar_placeholder":"Haz una pregunta a un @cerebro y elige tu #prompt","add_document":"Agregar documento","ask":"Has una pregunta o describe un tarea.","back_to_chat":"Volver al chat","brain":"cerebro","brains":"cerebros","change_brain":"Cambiar cerebro","chat":"Conversar","chatDeleted":"Chat borrado correstamente. Id: {{id}}","chatNameUpdated":"Nombre de chat actualizado","error_occurred":"Error al obtener respuesta","errorCallingAPI":"Error al llamar a la API","errorDeleting":"Error al borrar chat: {{error}}","errorFetching":"Error al obtener tus chats","errorParsingData":"Error al transformar datos","feed_brain_placeholder":"Elige cuál @cerebro quieres alimentar con estos archivos","feedingBrain":"Su conocimiento recién agregado se está procesando, ¡puede seguir chateando mientras tanto!","history":"Historia","keyboard_shortcuts":"Atajos de teclado","last30Days":"Últimos 30 días","last7Days":"Últimos 7 días","limit_reached":"Has alcanzado el límite de peticiones, intente de nuevo más tarde","menu":"Menú","missing_brain":"No hay cerebro seleccionado","new_discussion":"Nueva discusión","new_prompt":"Crear nueva instrucción","noCurrentBrain":"Sin cerebro seleccionado","onboarding":{"answer":{"how_to_use_quivr":"Consulta la documentación en https://brain.quivr.app/docs/intro.html","what_is_brain":"Un cerebro es una carpeta virtual para organizar información sobre un tema específico. Puede almacenar documentos y conectarse a aplicaciones externas o APIs. Por ejemplo, un cerebro de 'Ciencias Médicas' podría contener datos relacionados con la salud, y un cerebro de 'Legal' podría tener información legal. Los cerebros pueden hacerse públicos para que otros los utilicen sin revelar el contenido, fomentando el intercambio de conocimientos.","what_is_quivr":"Quivr es un asistente útil. Puedes arrastrar y soltar archivos en el chat o en la sección de conocimientos para interactuar con ellos. No es solo una herramienta de chat; también puedes comunicarte con aplicaciones utilizando APIs.\nPara mantener tu trabajo organizado, puedes crear cerebros, que son básicamente carpetas virtuales, y suscribirte a los cerebros de otros en la sección de exploración para una colaboración y compartición de información fluida."},"how_to_use_quivr":"¿Cómo usar Quivr?","step_1_1":"1. Arrastra y suelta el archivo en el chat o en el 📎.","step_1_2":"¿No tienes un archivo? Descarga 'Documentación de Quivr'","step_2":"2. Comienza a chatear con tu archivo","step_3":"3. ¡Disfruta!","title":"¡Hola 👋🏻 ¿Quieres descubrir Quivr? 😇","what_is_brain":"¿Qué es un cerebro?","what_is_quivr":"¿Qué es Quivr?"},"parameters":"Parámetros","receivedResponse":"Respuesta recibida. Iniciando gestión de stream...","resposeBodyNull":"Cuerpo de respuesta vacío","shortcut_choose_prompt":"#: Elegir una instrucción específica","shortcut_create_brain":"@+: Crear un nuevo cerebro","shortcut_create_prompt":"#+: Crear una nueva instrucción personalizada","shortcut_feed_brain":"/+: Alimentar un cerebro con conocimiento","shortcut_go_to_shortcuts":"CMDK: Ir a los atajos","shortcut_go_to_user_page":"CMDU: Ir a la página de usuario","shortcut_manage_brains":"CMDB: Administrar tus cerebros","shortcut_select_brain":"@: Seleccionar un cerebro","shortcut_select_file":"/: Seleccionar un archivo para hablar","subtitle":"Habla con un modelo de lenguaje acerca de tus datos subidos","thinking":"Pensando...","title":"Conversa con {{brain}}","today":"Hoy","tooManyRequests":"Has excedido el número de solicitudes por día. Para continuar chateando, por favor ingresa una clave de API de OpenAI en tu perfil o en el cerebro utilizado.","welcome":"Bienvenido","yesterday":"Ayer"}
{
"actions_bar_placeholder": "Haz una pregunta a un @cerebro y elige tu #prompt",
"add_document": "Agregar documento",
"ask": "Has una pregunta o describe un tarea.",
"back_to_search": "Volver a la búsqueda",
"brain": "cerebro",
"brains": "cerebros",
"change_brain": "Cambiar cerebro",
"chat": "Conversar",
"chatDeleted": "Chat borrado correstamente. Id: {{id}}",
"chatNameUpdated": "Nombre de chat actualizado",
"error_occurred": "Error al obtener respuesta",
"errorCallingAPI": "Error al llamar a la API",
"errorDeleting": "Error al borrar chat: {{error}}",
"errorFetching": "Error al obtener tus chats",
"errorParsingData": "Error al transformar datos",
"feed_brain_placeholder": "Elige cuál @cerebro quieres alimentar con estos archivos",
"feedingBrain": "Su conocimiento recién agregado se está procesando, ¡puede seguir chateando mientras tanto!",
"history": "Historia",
"keyboard_shortcuts": "Atajos de teclado",
"last30Days": "Últimos 30 días",
"last7Days": "Últimos 7 días",
"limit_reached": "Has alcanzado el límite de peticiones, intente de nuevo más tarde",
"menu": "Menú",
"missing_brain": "No hay cerebro seleccionado",
"new_discussion": "Nueva discusión",
"new_prompt": "Crear nueva instrucción",
"noCurrentBrain": "Sin cerebro seleccionado",
"onboarding": {
"answer": {
"how_to_use_quivr": "Consulta la documentación en https://brain.quivr.app/docs/intro.html",
"what_is_brain": "Un cerebro es una carpeta virtual para organizar información sobre un tema específico. Puede almacenar documentos y conectarse a aplicaciones externas o APIs. Por ejemplo, un cerebro de 'Ciencias Médicas' podría contener datos relacionados con la salud, y un cerebro de 'Legal' podría tener información legal. Los cerebros pueden hacerse públicos para que otros los utilicen sin revelar el contenido, fomentando el intercambio de conocimientos.",
"what_is_quivr": "Quivr es un asistente útil. Puedes arrastrar y soltar archivos en el chat o en la sección de conocimientos para interactuar con ellos. No es solo una herramienta de chat; también puedes comunicarte con aplicaciones utilizando APIs.\nPara mantener tu trabajo organizado, puedes crear cerebros, que son básicamente carpetas virtuales, y suscribirte a los cerebros de otros en la sección de exploración para una colaboración y compartición de información fluida."
},
"how_to_use_quivr": "¿Cómo usar Quivr?",
"step_1_1": "1. Arrastra y suelta el archivo en el chat o en el 📎.",
"step_1_2": "¿No tienes un archivo? Descarga 'Documentación de Quivr'",
"step_2": "2. Comienza a chatear con tu archivo",
"step_3": "3. ¡Disfruta!",
"title": "¡Hola 👋🏻 ¿Quieres descubrir Quivr? 😇",
"what_is_brain": "¿Qué es un cerebro?",
"what_is_quivr": "¿Qué es Quivr?"
},
"parameters": "Parámetros",
"receivedResponse": "Respuesta recibida. Iniciando gestión de stream...",
"resposeBodyNull": "Cuerpo de respuesta vacío",
"search": "Buscar",
"shortcut_choose_prompt": "#: Elegir una instrucción específica",
"shortcut_create_brain": "@+: Crear un nuevo cerebro",
"shortcut_create_prompt": "#+: Crear una nueva instrucción personalizada",
"shortcut_feed_brain": "/+: Alimentar un cerebro con conocimiento",
"shortcut_go_to_shortcuts": "CMDK: Ir a los atajos",
"shortcut_go_to_user_page": "CMDU: Ir a la página de usuario",
"shortcut_manage_brains": "CMDB: Administrar tus cerebros",
"shortcut_select_brain": "@: Seleccionar un cerebro",
"shortcut_select_file": "/: Seleccionar un archivo para hablar",
"subtitle": "Habla con un modelo de lenguaje acerca de tus datos subidos",
"thinking": "Pensando...",
"title": "Conversa con {{brain}}",
"today": "Hoy",
"tooManyRequests": "Has excedido el número de solicitudes por día. Para continuar chateando, por favor ingresa una clave de API de OpenAI en tu perfil o en el cerebro utilizado.",
"welcome": "Bienvenido",
"yesterday": "Ayer"
}

View File

@ -1 +1,56 @@
{"accountSection":"Tu Cuenta","anthropicKeyLabel":"Clave de la API de Anthropic","anthropicKeyPlaceholder":"Clave de la API de Anthropic","apiKey":"Clave de API","backendSection":"Configuración de Backend","backendUrlLabel":"URL del Backend","backendUrlPlaceHolder":"URL del Backend","brainUpdated":"Cerebro actualizado correctamente","configReset":"Configuración restaurada","configSaved":"Configuración guardada","customPromptSection":"Indicadores personalizados","defaultBrainSet":"Cerebro asignado como predeterminado","descriptionRequired":"La descripción es necesaria","error":{"copy":"No se pudo copiar","createApiKey":"No se pudo crear la clave API"},"errorRemovingPrompt":"Error eliminando indicador","incorrectApiKey":"Clave de API incorrecta","invalidApiKeyError":"Clave de API inválida","invalidOpenAiKey":"Clave de OpenAI inválida","keepInLocal":"Mantener localmente","knowledge":"Conocimiento","maxTokens":"Tokens máximo","modelLabel":"Modelo","modelSection":"Configuración de Modelo","nameRequired":"El nombre es necesario","newAPIKey":"Crea una nueva clave","noUser":"Sin usuarios","ohno":"¡Oh no!","openAiKeyLabel":"Clave de Open AI","openAiKeyPlaceholder":"sk-xxx","people":"Personas","promptContent":"Contenido del indicador","promptContentPlaceholder":"Como una IA, tu...","promptFieldsRequired":"Título y contenido de indicador son necesarios","promptName":"Título del indicador","promptNamePlaceholder":"El nombre de mi súper indicador","promptRemoved":"Indicador eliminado correctamente","publicPrompts":"Selecciona un indicador público","removePrompt":"Quitar indicador","requireAccess":"Por favor, solicita acceso al dueño","roleRequired":"No tienen el rol necesario para acceder a esta pestaña 🧠💡🥲.","selectQuivrPersonalityBtn":"Selecciona una Personalidad Quivr","settings":"Configuración","signedInAs":"Sesión iniciada como","subtitle":"Gestiona tu cerebro","supabaseKeyLabel":"Clave de Supabase","supabaseKeyPlaceHolder":"Clave de Supabase","supabaseURLLabel":"URL de Supabase","supabaseURLPlaceHolder":"URL de Supabase","temperature":"Temperatura","title":"Configuración","updatingBrainSettings":"Actualizando configuración del cerebro..."}
{
"accountSection": "Tu Cuenta",
"anthropicKeyLabel": "Clave de la API de Anthropic",
"anthropicKeyPlaceholder": "Clave de la API de Anthropic",
"apiKey": "Clave de API",
"backendSection": "Configuración de Backend",
"backendUrlLabel": "URL del Backend",
"backendUrlPlaceHolder": "URL del Backend",
"brainUpdated": "Cerebro actualizado correctamente",
"configReset": "Configuración restaurada",
"configSaved": "Configuración guardada",
"customPromptSection": "Indicadores personalizados",
"defaultBrainSet": "Cerebro asignado como predeterminado",
"descriptionRequired": "La descripción es necesaria",
"error": {
"copy": "No se pudo copiar",
"createApiKey": "No se pudo crear la clave API"
},
"errorRemovingPrompt": "Error eliminando indicador",
"incorrectApiKey": "Clave de API incorrecta",
"invalidApiKeyError": "Clave de API inválida",
"invalidOpenAiKey": "Clave de OpenAI inválida",
"keepInLocal": "Mantener localmente",
"knowledge": "Conocimiento",
"maxTokens": "Tokens máximo",
"modelLabel": "Modelo",
"modelSection": "Configuración de Modelo",
"nameRequired": "El nombre es necesario",
"newAPIKey": "Crea una nueva clave",
"noUser": "Sin usuarios",
"ohno": "¡Oh no!",
"openAiKeyLabel": "Clave de Open AI",
"openAiKeyPlaceholder": "sk-xxx",
"people": "Personas",
"promptContent": "Contenido del indicador",
"promptContentPlaceholder": "Como una IA, tu...",
"promptFieldsRequired": "Título y contenido de indicador son necesarios",
"promptName": "Título del indicador",
"promptNamePlaceholder": "El nombre de mi súper indicador",
"promptRemoved": "Indicador eliminado correctamente",
"publicPrompts": "Selecciona un indicador público",
"removePrompt": "Quitar indicador",
"requireAccess": "Por favor, solicita acceso al dueño",
"roleRequired": "No tienen el rol necesario para acceder a esta pestaña 🧠💡🥲.",
"selectQuivrPersonalityBtn": "Selecciona una Personalidad Quivr",
"settings": "Configuración",
"signedInAs": "Sesión iniciada como",
"subtitle": "Gestiona tu cerebro",
"supabaseKeyLabel": "Clave de Supabase",
"supabaseKeyPlaceHolder": "Clave de Supabase",
"supabaseURLLabel": "URL de Supabase",
"supabaseURLPlaceHolder": "URL de Supabase",
"temperature": "Temperatura",
"title": "Configuración",
"updatingBrainSettings": "Actualizando configuración del cerebro..."
}

View File

@ -1 +1,64 @@
{"actions_bar_placeholder":"Posez une question à un @cerveau et sélectionnez un #prompt ","add_document":"Ajouter un document","ask":"Posez une question ou décrivez une tâche.","back_to_chat":"Retour au chat","brain":"cerveau","brains":"cerveaux","change_brain":"Changer de cerveau","chat":"Chat","chatDeleted":"Chat supprimé avec succès. Id : {{id}}","chatNameUpdated":"Nom du chat mis à jour","error_occurred":"Une erreur s'est produite lors de l'obtention de la réponse","errorCallingAPI":"Erreur lors de l'appel à l'API","errorDeleting":"Erreur lors de la suppression du chat : {{error}}","errorFetching":"Erreur lors de la récupération de vos chats","errorParsingData":"Erreur lors de l'analyse des données","feed_brain_placeholder":"Choisissez le @cerveau que vous souhaitez nourrir avec ces fichiers","feedingBrain":"Vos nouvelles connaissances sont en cours de traitement. Vous pouvez continuer à discuter en attendant !","history":"Histoire","keyboard_shortcuts":"Raccourcis clavier","last30Days":"30 derniers jours","last7Days":"7 derniers jours","limit_reached":"Vous avez atteint la limite de requêtes, veuillez réessayer plus tard","menu":"Menu","missing_brain":"Veuillez selectionner un cerveau pour discuter","new_discussion":"Nouvelle discussion","new_prompt":"Créer un nouveau prompt","noCurrentBrain":"Pas de cerveau actuel","onboarding":{"answer":{"how_to_use_quivr":"Consultez la documentation sur https://brain.quivr.app/docs/intro.html","what_is_brain":"Un cerveau est un dossier virtuel permettant d'organiser des informations sur un sujet spécifique. Il peut stocker des documents et se connecter à des applications externes ou des APIs. Par exemple, un cerveau 'Sciences Médicales' pourrait contenir des données liées à la santé, et un cerveau 'Juridique' pourrait contenir des informations juridiques. Les cerveaux peuvent être rendus publics pour que d'autres puissent les utiliser sans révéler le contenu, favorisant ainsi le partage des connaissances.","what_is_quivr":"Quivr est un assistant utile. Vous pouvez facilement glisser-déposer des fichiers dans le chat ou la section des connaissances pour interagir avec eux. Ce n'est pas seulement un outil de chat ; vous pouvez également communiquer avec des applications en utilisant des APIs.\nPour organiser votre travail, vous pouvez créer des cerveaux, essentiellement des dossiers virtuels, et vous abonner aux cerveaux des autres dans la section Explorer pour une collaboration et un partage d'informations transparents."},"how_to_use_quivr":"Comment utiliser Quivr ?","step_1_1":"1. Glissez-déposez un fichier dans le chat ou dans la 📎.","step_1_2":"Pas de fichier ? Téléchargez 'Documentation Quivr'","step_2":"2. Commencez à discuter avec votre fichier","step_3":"3. Profitez !","title":"Salut 👋🏻 Envie de découvrir Quivr ? 😇","what_is_brain":"Qu'est-ce qu'un cerveau ?","what_is_quivr":"Qu'est-ce que Quivr ?"},"parameters":"Paramètres","receivedResponse":"Réponse reçue. Commence à gérer le flux...","resposeBodyNull":"Le corps de la réponse est nul","shortcut_choose_prompt":"#: Choisir une directive spécifique","shortcut_create_brain":"@+: Créer un nouveau cerveau","shortcut_create_prompt":"#+: Créer une nouvelle directive personnalisée","shortcut_feed_brain":"/+: Alimenter un cerveau avec des connaissances","shortcut_go_to_shortcuts":"CMDK: Accéder aux raccourcis","shortcut_go_to_user_page":"CMDU: Accéder à la page utilisateur","shortcut_manage_brains":"CMDB: Gérer vos cerveaux","shortcut_select_brain":"@: Sélectionnez un cerveau","shortcut_select_file":"/: Sélectionner un fichier pour discuter","subtitle":"Parlez à un modèle linguistique de vos données téléchargées","thinking":"Réflexion...","title":"Discuter avec {{brain}}","today":"Aujourd'hui","tooManyRequests":"Vous avez dépassé le nombre de requêtes par jour. Pour continuer à discuter, veuillez entrer une clé d'API OpenAI dans votre profil ou dans le cerveau utilisé.","welcome":"Bienvenue","yesterday":"Hier"}
{
"actions_bar_placeholder": "Posez une question à un @cerveau et sélectionnez un #prompt ",
"add_document": "Ajouter un document",
"ask": "Posez une question ou décrivez une tâche.",
"back_to_search": "Retour à la recherche",
"brain": "cerveau",
"brains": "cerveaux",
"change_brain": "Changer de cerveau",
"chat": "Chat",
"chatDeleted": "Chat supprimé avec succès. Id : {{id}}",
"chatNameUpdated": "Nom du chat mis à jour",
"error_occurred": "Une erreur s'est produite lors de l'obtention de la réponse",
"errorCallingAPI": "Erreur lors de l'appel à l'API",
"errorDeleting": "Erreur lors de la suppression du chat : {{error}}",
"errorFetching": "Erreur lors de la récupération de vos chats",
"errorParsingData": "Erreur lors de l'analyse des données",
"feed_brain_placeholder": "Choisissez le @cerveau que vous souhaitez nourrir avec ces fichiers",
"feedingBrain": "Vos nouvelles connaissances sont en cours de traitement. Vous pouvez continuer à discuter en attendant !",
"history": "Histoire",
"keyboard_shortcuts": "Raccourcis clavier",
"last30Days": "30 derniers jours",
"last7Days": "7 derniers jours",
"limit_reached": "Vous avez atteint la limite de requêtes, veuillez réessayer plus tard",
"menu": "Menu",
"missing_brain": "Veuillez selectionner un cerveau pour discuter",
"new_discussion": "Nouvelle discussion",
"new_prompt": "Créer un nouveau prompt",
"noCurrentBrain": "Pas de cerveau actuel",
"onboarding": {
"answer": {
"how_to_use_quivr": "Consultez la documentation sur https://brain.quivr.app/docs/intro.html",
"what_is_brain": "Un cerveau est un dossier virtuel permettant d'organiser des informations sur un sujet spécifique. Il peut stocker des documents et se connecter à des applications externes ou des APIs. Par exemple, un cerveau 'Sciences Médicales' pourrait contenir des données liées à la santé, et un cerveau 'Juridique' pourrait contenir des informations juridiques. Les cerveaux peuvent être rendus publics pour que d'autres puissent les utiliser sans révéler le contenu, favorisant ainsi le partage des connaissances.",
"what_is_quivr": "Quivr est un assistant utile. Vous pouvez facilement glisser-déposer des fichiers dans le chat ou la section des connaissances pour interagir avec eux. Ce n'est pas seulement un outil de chat ; vous pouvez également communiquer avec des applications en utilisant des APIs.\nPour organiser votre travail, vous pouvez créer des cerveaux, essentiellement des dossiers virtuels, et vous abonner aux cerveaux des autres dans la section Explorer pour une collaboration et un partage d'informations transparents."
},
"how_to_use_quivr": "Comment utiliser Quivr ?",
"step_1_1": "1. Glissez-déposez un fichier dans le chat ou dans la 📎.",
"step_1_2": "Pas de fichier ? Téléchargez 'Documentation Quivr'",
"step_2": "2. Commencez à discuter avec votre fichier",
"step_3": "3. Profitez !",
"title": "Salut 👋🏻 Envie de découvrir Quivr ? 😇",
"what_is_brain": "Qu'est-ce qu'un cerveau ?",
"what_is_quivr": "Qu'est-ce que Quivr ?"
},
"parameters": "Paramètres",
"receivedResponse": "Réponse reçue. Commence à gérer le flux...",
"resposeBodyNull": "Le corps de la réponse est nul",
"search": "Rechercher",
"shortcut_choose_prompt": "#: Choisir une directive spécifique",
"shortcut_create_brain": "@+: Créer un nouveau cerveau",
"shortcut_create_prompt": "#+: Créer une nouvelle directive personnalisée",
"shortcut_feed_brain": "/+: Alimenter un cerveau avec des connaissances",
"shortcut_go_to_shortcuts": "CMDK: Accéder aux raccourcis",
"shortcut_go_to_user_page": "CMDU: Accéder à la page utilisateur",
"shortcut_manage_brains": "CMDB: Gérer vos cerveaux",
"shortcut_select_brain": "@: Sélectionnez un cerveau",
"shortcut_select_file": "/: Sélectionner un fichier pour discuter",
"subtitle": "Parlez à un modèle linguistique de vos données téléchargées",
"thinking": "Réflexion...",
"title": "Discuter avec {{brain}}",
"today": "Aujourd'hui",
"tooManyRequests": "Vous avez dépassé le nombre de requêtes par jour. Pour continuer à discuter, veuillez entrer une clé d'API OpenAI dans votre profil ou dans le cerveau utilisé.",
"welcome": "Bienvenue",
"yesterday": "Hier"
}

View File

@ -1 +1,64 @@
{"actions_bar_placeholder":"Faça uma pergunta a um @cérebro e escolha sua #prompt","add_document":"Adicionar documento","ask":"Faça uma pergunta ou descreva uma tarefa.","back_to_chat":"Voltar para o chat","brain":"cérebro","brains":"cérebros","change_brain":"Mudar cérebro","chat":"Conversa","chatDeleted":"Conversa excluída com sucesso. Id: {{id}}","chatNameUpdated":"Nome da conversa atualizado","error_occurred":"Ocorreu um erro ao obter a resposta","errorCallingAPI":"Erro ao chamar a API","errorDeleting":"Erro ao excluir a conversa: {{error}}","errorFetching":"Ocorreu um erro ao buscar suas conversas","errorParsingData":"Erro ao analisar os dados","feed_brain_placeholder":"Escolha qual @cérebro você deseja alimentar com esses arquivos","feedingBrain":"Seu conhecimento recém-adicionado está sendo processado, você pode continuar conversando enquanto isso!","history":"História","keyboard_shortcuts":"Atalhos do teclado","last30Days":"Últimos 30 dias","last7Days":"Últimos 7 dias","limit_reached":"Você atingiu o limite de solicitações, por favor, tente novamente mais tarde","menu":"Menu","missing_brain":"Cérebro não encontrado","new_discussion":"Nova discussão","new_prompt":"Criar novo prompt","noCurrentBrain":"Nenhum cérebro selecionado","onboarding":{"answer":{"how_to_use_quivr":"Verifique a documentação em https://brain.quivr.app/docs/intro.html","what_is_brain":"Um cérebro é uma pasta virtual para organizar informações sobre um tópico específico. Ele pode armazenar documentos e se conectar a aplicativos ou APIs externas. Por exemplo, um cérebro 'Ciência Médica' poderia conter dados relacionados à saúde, e um cérebro 'Jurídico' poderia ter informações legais. Os cérebros podem ser tornados públicos para que outros os usem sem revelar o conteúdo, promovendo o compartilhamento de conhecimento.","what_is_quivr":"Quivr é um assistente útil. Você pode facilmente arrastar e soltar arquivos no chat ou na seção de conhecimento para interagir com eles. Não é apenas uma ferramenta de chat; você também pode se comunicar com aplicativos usando APIs.\nPara manter seu trabalho organizado, você pode criar cérebros, essencialmente pastas virtuais, e se inscrever nos cérebros de outras pessoas na seção de exploração para colaboração e compartilhamento de informações perfeitos."},"how_to_use_quivr":"Como usar o Quivr?","step_1_1":"1. Arraste e solte o arquivo no chat ou no 📎.","step_1_2":"Não tem um arquivo? Baixe 'Documentação do Quivr'","step_2":"2. Comece a conversar com seu arquivo","step_3":"3. Divirta-se!","title":"Oi 👋🏻 Quer descobrir o Quivr ? 😇","what_is_brain":"O que é um cérebro?","what_is_quivr":"O que é o Quivr?"},"parameters":"Parâmetros","receivedResponse":"Resposta recebida. Iniciando o processamento do fluxo...","resposeBodyNull":"O corpo da resposta está vazio","shortcut_choose_prompt":"#: Escolha um prompt específico","shortcut_create_brain":"@+: Crie um novo cérebro","shortcut_create_prompt":"#+: Crie um novo prompt personalizado","shortcut_feed_brain":"/+: Alimente um cérebro com conhecimento","shortcut_go_to_shortcuts":"CMDA: Acesse os atalhos","shortcut_go_to_user_page":"CMDU: Acesse a página do usuário","shortcut_manage_brains":"CMGC: Gerencie seus cérebros","shortcut_select_brain":"@: Selecione um cérebro","shortcut_select_file":"/: Selecione um arquivo para conversar","subtitle":"Converse com um modelo de linguagem sobre seus dados enviados","thinking":"Pensando...","title":"Converse com {{brain}}","today":"Hoje","tooManyRequests":"Você excedeu o número de solicitações por dia. Para continuar conversando, insira uma chave de API da OpenAI em seu perfil ou no cérebro utilizado.","welcome":"Bem-vindo","yesterday":"Ontem"}
{
"actions_bar_placeholder": "Faça uma pergunta a um @cérebro e escolha sua #prompt",
"add_document": "Adicionar documento",
"ask": "Faça uma pergunta ou descreva uma tarefa.",
"back_to_search": "Voltar para a pesquisa",
"brain": "cérebro",
"brains": "cérebros",
"change_brain": "Mudar cérebro",
"chat": "Conversa",
"chatDeleted": "Conversa excluída com sucesso. Id: {{id}}",
"chatNameUpdated": "Nome da conversa atualizado",
"error_occurred": "Ocorreu um erro ao obter a resposta",
"errorCallingAPI": "Erro ao chamar a API",
"errorDeleting": "Erro ao excluir a conversa: {{error}}",
"errorFetching": "Ocorreu um erro ao buscar suas conversas",
"errorParsingData": "Erro ao analisar os dados",
"feed_brain_placeholder": "Escolha qual @cérebro você deseja alimentar com esses arquivos",
"feedingBrain": "Seu conhecimento recém-adicionado está sendo processado, você pode continuar conversando enquanto isso!",
"history": "História",
"keyboard_shortcuts": "Atalhos do teclado",
"last30Days": "Últimos 30 dias",
"last7Days": "Últimos 7 dias",
"limit_reached": "Você atingiu o limite de solicitações, por favor, tente novamente mais tarde",
"menu": "Menu",
"missing_brain": "Cérebro não encontrado",
"new_discussion": "Nova discussão",
"new_prompt": "Criar novo prompt",
"noCurrentBrain": "Nenhum cérebro selecionado",
"onboarding": {
"answer": {
"how_to_use_quivr": "Verifique a documentação em https://brain.quivr.app/docs/intro.html",
"what_is_brain": "Um cérebro é uma pasta virtual para organizar informações sobre um tópico específico. Ele pode armazenar documentos e se conectar a aplicativos ou APIs externas. Por exemplo, um cérebro 'Ciência Médica' poderia conter dados relacionados à saúde, e um cérebro 'Jurídico' poderia ter informações legais. Os cérebros podem ser tornados públicos para que outros os usem sem revelar o conteúdo, promovendo o compartilhamento de conhecimento.",
"what_is_quivr": "Quivr é um assistente útil. Você pode facilmente arrastar e soltar arquivos no chat ou na seção de conhecimento para interagir com eles. Não é apenas uma ferramenta de chat; você também pode se comunicar com aplicativos usando APIs.\nPara manter seu trabalho organizado, você pode criar cérebros, essencialmente pastas virtuais, e se inscrever nos cérebros de outras pessoas na seção de exploração para colaboração e compartilhamento de informações perfeitos."
},
"how_to_use_quivr": "Como usar o Quivr?",
"step_1_1": "1. Arraste e solte o arquivo no chat ou no 📎.",
"step_1_2": "Não tem um arquivo? Baixe 'Documentação do Quivr'",
"step_2": "2. Comece a conversar com seu arquivo",
"step_3": "3. Divirta-se!",
"title": "Oi 👋🏻 Quer descobrir o Quivr ? 😇",
"what_is_brain": "O que é um cérebro?",
"what_is_quivr": "O que é o Quivr?"
},
"parameters": "Parâmetros",
"receivedResponse": "Resposta recebida. Iniciando o processamento do fluxo...",
"resposeBodyNull": "O corpo da resposta está vazio",
"search": "Pesquisar",
"shortcut_choose_prompt": "#: Escolha um prompt específico",
"shortcut_create_brain": "@+: Crie um novo cérebro",
"shortcut_create_prompt": "#+: Crie um novo prompt personalizado",
"shortcut_feed_brain": "/+: Alimente um cérebro com conhecimento",
"shortcut_go_to_shortcuts": "CMDA: Acesse os atalhos",
"shortcut_go_to_user_page": "CMDU: Acesse a página do usuário",
"shortcut_manage_brains": "CMGC: Gerencie seus cérebros",
"shortcut_select_brain": "@: Selecione um cérebro",
"shortcut_select_file": "/: Selecione um arquivo para conversar",
"subtitle": "Converse com um modelo de linguagem sobre seus dados enviados",
"thinking": "Pensando...",
"title": "Converse com {{brain}}",
"today": "Hoje",
"tooManyRequests": "Você excedeu o número de solicitações por dia. Para continuar conversando, insira uma chave de API da OpenAI em seu perfil ou no cérebro utilizado.",
"welcome": "Bem-vindo",
"yesterday": "Ontem"
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1,65 @@
{"actions_bar_placeholder":"向 @大脑 提问,选择您的 #提示","add_document":"添加文件","ask":"提一个问题,或描述一个任务。","back_to_chat":"返回聊天","brain":"大脑","brains":"大脑","change_brain":"更改大脑","chat":"聊天","chatDeleted":"聊天删除成功. Id: {{id}}","chatNameUpdated":"聊天名称已更新","error_occurred":"获取答案时发生错误","errorCallingAPI":"调用 API 时出错","errorDeleting":"删除聊天时出错: {{error}}","errorFetching":"获取您的聊天记录时发生错误","errorParsingData":"解析数据时发生错误","feed_brain_placeholder":"选择要用这些文件充实的 @大脑","feedingBrain":"您新添加的知识正在处理中,不影响您继续聊天!","history":"历史","keyboard_shortcuts":"键盘快捷键","last30Days":"过去30天","last7Days":"过去7天","limit_reached":"您已达到请求限制,请稍后再试","menu":"菜单","missing_brain":"请选择一个大脑进行聊天","new_discussion":"新讨论","new_prompt":"新提示","noCurrentBrain":"没有当前的大脑","onboarding":{"answer":{"how_to_use_quivr":"查看文档 https://brain.quivr.app/docs/intro.html","what_is_brain":"大脑是用于组织特定主题信息的虚拟文件夹。它可以存储文档并连接到外部应用程序或 API。例如'医学科学' 大脑可以包含与健康相关的数据,而 '法律' 大脑可以包含法律信息。大脑可以公开使用,而不会透露内容,促进知识共享。","what_is_quivr":"Quivr 是一个有用的助手。您可以轻松将文件拖放到聊天或知识部分中与其进行交互。它不仅是一个聊天工具,还可以使用 API 与应用程序进行通信。\n为了使您的工作有条理您可以创建大脑即虚拟文件夹并在探索部分订阅其他人的大脑以实现无缝协作和信息共享。"},"how_to_use_quivr":"如何使用 Quivr","step_1_1":"1. 在聊天框或 📎 上拖放文件。","step_1_2":"没有文件?下载 “Quivr 文档”","step_2":"2. 开始与您的文件聊天。","step_3":"3. 尽情享受!","title":"嗨 👋🏻 想要探索 Quivr😇","what_is_brain":"什么是大脑?","what_is_quivr":"什么是 Quivr"},"parameters":"参数","receivedResponse":"收到响应。开始处理流…","resposeBodyNull":"响应内容为空","shortcut_choose_prompt":"#: 选择一个特定的提示","shortcut_create_brain":"@+: 创建一个新的大脑","shortcut_create_prompt":"#+: 创建一个新的自定义提示","shortcut_feed_brain":"/+: 用知识充实大脑","shortcut_go_to_shortcuts":"CMDK: 前往快捷方式","shortcut_go_to_user_page":"CMDU: 进入用户页面","shortcut_manage_brains":"CMDB: 管理大脑","shortcut_select_brain":"@: 选择一个大脑","shortcut_select_file":"/: 选择一个文件进行对话","subtitle":"与语言模型讨论您上传的数据","thinking":"思考中…","title":"与 {{brain}} 聊天","today":"今天","tooManyRequests":"您已超过每天的请求次数。想要继续聊天,请在您的个人资料中或为当前大脑配置 OpenAI API 密钥。","welcome":"欢迎","yesterday":"昨天","begin_conversation_placeholder":"在这里开始对话…"}
{
"actions_bar_placeholder": "向 @大脑 提问,选择您的 #提示",
"add_document": "添加文件",
"ask": "提一个问题,或描述一个任务。",
"back_to_search": "返回搜索",
"brain": "大脑",
"brains": "大脑",
"change_brain": "更改大脑",
"chat": "聊天",
"chatDeleted": "聊天删除成功. Id: {{id}}",
"chatNameUpdated": "聊天名称已更新",
"error_occurred": "获取答案时发生错误",
"errorCallingAPI": "调用 API 时出错",
"errorDeleting": "删除聊天时出错: {{error}}",
"errorFetching": "获取您的聊天记录时发生错误",
"errorParsingData": "解析数据时发生错误",
"feed_brain_placeholder": "选择要用这些文件充实的 @大脑",
"feedingBrain": "您新添加的知识正在处理中,不影响您继续聊天!",
"history": "历史",
"keyboard_shortcuts": "键盘快捷键",
"last30Days": "过去30天",
"last7Days": "过去7天",
"limit_reached": "您已达到请求限制,请稍后再试",
"menu": "菜单",
"missing_brain": "请选择一个大脑进行聊天",
"new_discussion": "新讨论",
"new_prompt": "新提示",
"noCurrentBrain": "没有当前的大脑",
"onboarding": {
"answer": {
"how_to_use_quivr": "查看文档 https://brain.quivr.app/docs/intro.html",
"what_is_brain": "大脑是用于组织特定主题信息的虚拟文件夹。它可以存储文档并连接到外部应用程序或 API。例如'医学科学' 大脑可以包含与健康相关的数据,而 '法律' 大脑可以包含法律信息。大脑可以公开使用,而不会透露内容,促进知识共享。",
"what_is_quivr": "Quivr 是一个有用的助手。您可以轻松将文件拖放到聊天或知识部分中与其进行交互。它不仅是一个聊天工具,还可以使用 API 与应用程序进行通信。\n为了使您的工作有条理您可以创建大脑即虚拟文件夹并在探索部分订阅其他人的大脑以实现无缝协作和信息共享。"
},
"how_to_use_quivr": "如何使用 Quivr",
"step_1_1": "1. 在聊天框或 📎 上拖放文件。",
"step_1_2": "没有文件?下载 “Quivr 文档”",
"step_2": "2. 开始与您的文件聊天。",
"step_3": "3. 尽情享受!",
"title": "嗨 👋🏻 想要探索 Quivr😇",
"what_is_brain": "什么是大脑?",
"what_is_quivr": "什么是 Quivr"
},
"parameters": "参数",
"receivedResponse": "收到响应。开始处理流…",
"resposeBodyNull": "响应内容为空",
"search": "搜索",
"shortcut_choose_prompt": "#: 选择一个特定的提示",
"shortcut_create_brain": "@+: 创建一个新的大脑",
"shortcut_create_prompt": "#+: 创建一个新的自定义提示",
"shortcut_feed_brain": "/+: 用知识充实大脑",
"shortcut_go_to_shortcuts": "CMDK: 前往快捷方式",
"shortcut_go_to_user_page": "CMDU: 进入用户页面",
"shortcut_manage_brains": "CMDB: 管理大脑",
"shortcut_select_brain": "@: 选择一个大脑",
"shortcut_select_file": "/: 选择一个文件进行对话",
"subtitle": "与语言模型讨论您上传的数据",
"thinking": "思考中…",
"title": "与 {{brain}} 聊天",
"today": "今天",
"tooManyRequests": "您已超过每天的请求次数。想要继续聊天,请在您的个人资料中或为当前大脑配置 OpenAI API 密钥。",
"welcome": "欢迎",
"yesterday": "昨天",
"begin_conversation_placeholder": "在这里开始对话…"
}

View File

@ -0,0 +1,8 @@
$white: #FFFFFF;
$black: #11243E;
$primary: #6142D4;
$secondary: #F3ECFF;
$tertiary: #F6F4FF;
$accent: #13ABBA;
$highlight: #FAFAFA;
$ivory: #FCFAF6,

View File

@ -0,0 +1,2 @@
$big: 30px;
$medium: 24px;

View File

@ -0,0 +1 @@
$small: 768px

View File

@ -0,0 +1,12 @@
$spacing01: 0.125rem;
$spacing02: 0.25rem;
$spacing03: 0.5rem;
$spacing04: 0.75rem;
$spacing05: 1rem;
$spacing06: 1.5rem;
$spacing07: 2rem;
$spacing08: 2.5rem;
$spacing09: 3rem;
$spacing10: 4rem;
$spacing11: 5rem;
$spacing12: 6rem;

View File

@ -0,0 +1,4 @@
@mixin H1 {
font-weight: 500;
font-size: 36px;
}

View File

@ -0,0 +1,7 @@
$base: 1000;
$overlay: 1010;
$modal: 1020;
$navbar: 1030;
$tooltip: 1040;
$refresh-banner: 1050;
$sentry-modal: 1060;

View File

@ -1,33 +0,0 @@
.container,
.post {
width: 100%;
min-height: 100vh;
margin: 0 auto;
padding: 40px 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container h1,
.post h1 {
font-size: 2em;
}
.card {
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
padding: 40px 0;
}
.flexing {
width: 30%;
box-sizing: content-box;
background: #f8f6f8;
margin: 1%;
padding: 30px;
border-radius: 8px;
}

View File

@ -3627,7 +3627,7 @@ check-error@^1.0.2:
resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz"
integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
chokidar@^3.5.3:
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@ -5488,6 +5488,11 @@ immediate@~3.0.5:
resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immutable@^4.0.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"
@ -7993,6 +7998,15 @@ safe-regex-test@^1.0.0:
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass@^1.70.0:
version "1.70.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.70.0.tgz#761197419d97b5358cb25f9dd38c176a8a270a75"
integrity sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
saxes@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz"
@ -8151,7 +8165,7 @@ socks@^2.7.1:
ip "^2.0.0"
smart-buffer "^4.2.0"
source-map-js@^1.0.2:
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==

View File

@ -0,0 +1,27 @@
alter table "public"."brains" add column "meaning" vector;
alter table "public"."brains" alter column "description" set default 'This needs to be changed'::text;
alter table "public"."brains" alter column "description" set not null;
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION public.match_brain(query_embedding vector, match_count integer)
RETURNS TABLE(id uuid, name text, similarity double precision)
LANGUAGE plpgsql
AS $function$
#variable_conflict use_column
begin
return query
select
brain_id,
name,
1 - (brains.meaning <=> query_embedding) as similarity
from brains
order by brains.meaning <=> query_embedding
limit match_count;
end;
$function$
;

View File

@ -0,0 +1,5 @@
drop function if exists "public"."match_documents"(query_embedding vector, match_count integer);
alter table "public"."chat_history" add column "metadata" jsonb;