mirror of
https://github.com/StanGirard/quivr.git
synced 2024-09-11 13:15:41 +03:00
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:
parent
7f83fcdf42
commit
d0b8b797f6
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -17,5 +17,6 @@ class ChatInterface(ABC):
|
||||
streaming,
|
||||
prompt_id,
|
||||
user_id,
|
||||
chat_question,
|
||||
):
|
||||
pass
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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]
|
||||
|
@ -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",
|
||||
{
|
||||
|
@ -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]);
|
||||
};
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
@ -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 <></>;
|
||||
};
|
||||
|
@ -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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from "./ShortCutItem";
|
@ -1 +0,0 @@
|
||||
export * from './ShortCuts';
|
@ -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 });
|
||||
|
||||
|
@ -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"
|
||||
|
@ -16,6 +16,9 @@ export type ChatMessage = {
|
||||
message_time: string;
|
||||
prompt_title?: string;
|
||||
brain_name?: string;
|
||||
metadata?: {
|
||||
sources?: [string];
|
||||
};
|
||||
};
|
||||
|
||||
type NotificationStatus = "Pending" | "Done";
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
43
frontend/app/search/page.module.scss
Normal file
43
frontend/app/search/page.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
frontend/app/search/page.tsx
Normal file
35
frontend/app/search/page.tsx
Normal 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;
|
@ -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">
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
16
frontend/lib/components/Menu/Menu.module.scss
Normal file
16
frontend/lib/components/Menu/Menu.module.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
|
@ -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(
|
||||
|
@ -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 = () => {
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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());
|
||||
});
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
41
frontend/lib/components/ui/SearchBar/SearchBar.module.scss
Normal file
41
frontend/lib/components/ui/SearchBar/SearchBar.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
52
frontend/lib/components/ui/SearchBar/SearchBar.tsx
Normal file
52
frontend/lib/components/ui/SearchBar/SearchBar.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
};
|
13
frontend/lib/context/MenuProvider/hooks/useMenuContext.tsx
Normal file
13
frontend/lib/context/MenuProvider/hooks/useMenuContext.tsx
Normal 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;
|
||||
};
|
@ -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;
|
||||
};
|
@ -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);
|
@ -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 [
|
||||
{
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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..."
|
||||
}
|
@ -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"
|
||||
}
|
@ -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
@ -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": "在这里开始对话…"
|
||||
}
|
8
frontend/styles/_Colors.module.scss
Normal file
8
frontend/styles/_Colors.module.scss
Normal file
@ -0,0 +1,8 @@
|
||||
$white: #FFFFFF;
|
||||
$black: #11243E;
|
||||
$primary: #6142D4;
|
||||
$secondary: #F3ECFF;
|
||||
$tertiary: #F6F4FF;
|
||||
$accent: #13ABBA;
|
||||
$highlight: #FAFAFA;
|
||||
$ivory: #FCFAF6,
|
2
frontend/styles/_IconSizes.module.scss
Normal file
2
frontend/styles/_IconSizes.module.scss
Normal file
@ -0,0 +1,2 @@
|
||||
$big: 30px;
|
||||
$medium: 24px;
|
1
frontend/styles/_ScreenSizes.module.scss
Normal file
1
frontend/styles/_ScreenSizes.module.scss
Normal file
@ -0,0 +1 @@
|
||||
$small: 768px
|
12
frontend/styles/_Spacings.module.scss
Normal file
12
frontend/styles/_Spacings.module.scss
Normal 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;
|
4
frontend/styles/_Typography.module.scss
Normal file
4
frontend/styles/_Typography.module.scss
Normal file
@ -0,0 +1,4 @@
|
||||
@mixin H1 {
|
||||
font-weight: 500;
|
||||
font-size: 36px;
|
||||
}
|
7
frontend/styles/_ZIndex.module.scss
Normal file
7
frontend/styles/_ZIndex.module.scss
Normal file
@ -0,0 +1,7 @@
|
||||
$base: 1000;
|
||||
$overlay: 1010;
|
||||
$modal: 1020;
|
||||
$navbar: 1030;
|
||||
$tooltip: 1040;
|
||||
$refresh-banner: 1050;
|
||||
$sentry-modal: 1060;
|
@ -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;
|
||||
}
|
@ -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==
|
||||
|
27
supabase/migrations/20240119070124_search.sql
Normal file
27
supabase/migrations/20240119070124_search.sql
Normal 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$
|
||||
;
|
||||
|
||||
|
5
supabase/migrations/20240119222036_metadata.sql
Normal file
5
supabase/migrations/20240119222036_metadata.sql
Normal 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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user