mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-15 01:21:48 +03:00
feat(frontend): talk with models and handle code markdown (#2980)
# Description complete ENT-35 --------- Co-authored-by: Stan Girard <girard.stanislas@gmail.com>
This commit is contained in:
parent
111200184b
commit
ef6037e665
@ -201,6 +201,14 @@ class ChatLLMService:
|
||||
"brain_name": None,
|
||||
"brain_id": None,
|
||||
}
|
||||
metadata_model = ChatLLMMetadata(
|
||||
name=self.model_to_use.name,
|
||||
description=model_metadata.description,
|
||||
image_url=model_metadata.image_url,
|
||||
display_name=model_metadata.display_name,
|
||||
brain_id=str(generate_uuid_from_string(self.model_to_use.name)),
|
||||
brain_name=self.model_to_use.name,
|
||||
)
|
||||
|
||||
async for response in chat_llm.answer_astream(question, chat_history):
|
||||
# Format output to be correct servicedf;j
|
||||
@ -210,6 +218,7 @@ class ChatLLMService:
|
||||
metadata=response.metadata.model_dump(),
|
||||
**message_metadata,
|
||||
)
|
||||
streamed_chat_history.metadata["metadata_model"] = metadata_model # type: ignore
|
||||
full_answer += response.answer
|
||||
yield f"data: {streamed_chat_history.model_dump_json()}"
|
||||
if response.last_chunk and full_answer == "":
|
||||
@ -217,7 +226,7 @@ class ChatLLMService:
|
||||
|
||||
# For last chunk parse the sources, and the full answer
|
||||
streamed_chat_history = GetChatHistoryOutput(
|
||||
assistant=full_answer,
|
||||
assistant="",
|
||||
metadata=response.metadata.model_dump(),
|
||||
**message_metadata,
|
||||
)
|
||||
|
@ -1,31 +1,38 @@
|
||||
import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from quivr_api.logger import get_logger
|
||||
from quivr_api.models.settings import (get_embedding_client,
|
||||
get_supabase_client, settings)
|
||||
from quivr_api.modules.brain.entity.brain_entity import BrainEntity
|
||||
from quivr_api.modules.brain.service.brain_service import BrainService
|
||||
from quivr_api.modules.brain.service.utils.format_chat_history import \
|
||||
format_chat_history
|
||||
from quivr_api.modules.chat.controller.chat.utils import (
|
||||
compute_cost, find_model_and_generate_metadata, update_user_usage)
|
||||
from quivr_api.modules.chat.dto.inputs import CreateChatHistory
|
||||
from quivr_api.modules.chat.dto.outputs import GetChatHistoryOutput
|
||||
from quivr_api.modules.chat.service.chat_service import ChatService
|
||||
from quivr_api.modules.knowledge.repository.knowledges import \
|
||||
KnowledgeRepository
|
||||
from quivr_api.modules.prompt.entity.prompt import Prompt
|
||||
from quivr_api.modules.prompt.service.prompt_service import PromptService
|
||||
from quivr_api.modules.user.entity.user_identity import UserIdentity
|
||||
from quivr_api.modules.user.service.user_usage import UserUsage
|
||||
from quivr_api.vectorstore.supabase import CustomSupabaseVectorStore
|
||||
from quivr_core.chat import ChatHistory as ChatHistoryCore
|
||||
from quivr_core.config import LLMEndpointConfig, RAGConfig
|
||||
from quivr_core.llm.llm_endpoint import LLMEndpoint
|
||||
from quivr_core.models import ParsedRAGResponse, RAGResponseMetadata
|
||||
from quivr_core.quivr_rag import QuivrQARAG
|
||||
|
||||
from quivr_api.logger import get_logger
|
||||
from quivr_api.models.settings import (
|
||||
get_embedding_client,
|
||||
get_supabase_client,
|
||||
settings,
|
||||
)
|
||||
from quivr_api.modules.brain.entity.brain_entity import BrainEntity
|
||||
from quivr_api.modules.brain.service.brain_service import BrainService
|
||||
from quivr_api.modules.brain.service.utils.format_chat_history import (
|
||||
format_chat_history,
|
||||
)
|
||||
from quivr_api.modules.chat.controller.chat.utils import (
|
||||
compute_cost,
|
||||
find_model_and_generate_metadata,
|
||||
update_user_usage,
|
||||
)
|
||||
from quivr_api.modules.chat.dto.inputs import CreateChatHistory
|
||||
from quivr_api.modules.chat.dto.outputs import GetChatHistoryOutput
|
||||
from quivr_api.modules.chat.service.chat_service import ChatService
|
||||
from quivr_api.modules.knowledge.repository.knowledges import KnowledgeRepository
|
||||
from quivr_api.modules.prompt.entity.prompt import Prompt
|
||||
from quivr_api.modules.prompt.service.prompt_service import PromptService
|
||||
from quivr_api.modules.user.entity.user_identity import UserIdentity
|
||||
from quivr_api.modules.user.service.user_usage import UserUsage
|
||||
from quivr_api.vectorstore.supabase import CustomSupabaseVectorStore
|
||||
|
||||
from .utils import generate_source
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
@ -15,7 +15,7 @@ from quivr_core.models import (
|
||||
ParsedRAGResponse,
|
||||
RAGResponseMetadata,
|
||||
)
|
||||
from quivr_core.utils import get_chunk_metadata, parse_chunk_response, parse_response
|
||||
from quivr_core.utils import get_chunk_metadata, parse_response
|
||||
|
||||
logger = logging.getLogger("quivr_core")
|
||||
|
||||
@ -39,12 +39,16 @@ class ChatLLM:
|
||||
filtered_chat_history: list[AIMessage | HumanMessage] = []
|
||||
if chat_history is None:
|
||||
return filtered_chat_history
|
||||
for human_message, ai_message in chat_history.iter_pairs():
|
||||
|
||||
# Convert generator to list to allow reversing
|
||||
pairs = list(chat_history.iter_pairs())
|
||||
# Iterate in reverse to prioritize the last messages
|
||||
for human_message, ai_message in reversed(pairs):
|
||||
# TODO: replace with tiktoken
|
||||
message_tokens = (len(human_message.content) + len(ai_message.content)) // 4
|
||||
if (
|
||||
total_tokens + message_tokens > self.llm_endpoint._config.max_input
|
||||
or total_pairs >= 10
|
||||
or total_pairs >= 20
|
||||
):
|
||||
break
|
||||
filtered_chat_history.append(human_message)
|
||||
@ -52,7 +56,7 @@ class ChatLLM:
|
||||
total_tokens += message_tokens
|
||||
total_pairs += 1
|
||||
|
||||
return filtered_chat_history[::-1]
|
||||
return filtered_chat_history[::-1] # Reverse back to original order
|
||||
|
||||
def build_chain(self):
|
||||
loaded_memory = RunnablePassthrough.assign(
|
||||
@ -104,34 +108,19 @@ class ChatLLM:
|
||||
{"question": question, "chat_history": history}
|
||||
):
|
||||
if "answer" in chunk:
|
||||
rolling_message, answer_str = parse_chunk_response(
|
||||
rolling_message,
|
||||
chunk,
|
||||
self.llm_endpoint.supports_func_calling(),
|
||||
)
|
||||
answer_str = chunk["answer"].content
|
||||
rolling_message += chunk["answer"]
|
||||
if len(answer_str) > 0:
|
||||
if self.llm_endpoint.supports_func_calling():
|
||||
diff_answer = answer_str[len(prev_answer) :]
|
||||
if len(diff_answer) > 0:
|
||||
parsed_chunk = ParsedRAGChunkResponse(
|
||||
answer=diff_answer,
|
||||
metadata=RAGResponseMetadata(),
|
||||
)
|
||||
prev_answer += diff_answer
|
||||
parsed_chunk = ParsedRAGChunkResponse(
|
||||
answer=answer_str,
|
||||
metadata=RAGResponseMetadata(),
|
||||
)
|
||||
prev_answer += answer_str
|
||||
|
||||
logger.debug(
|
||||
f"answer_astream func_calling=True question={question} rolling_msg={rolling_message} chunk_id={chunk_id}, chunk={parsed_chunk}"
|
||||
)
|
||||
yield parsed_chunk
|
||||
else:
|
||||
parsed_chunk = ParsedRAGChunkResponse(
|
||||
answer=answer_str,
|
||||
metadata=RAGResponseMetadata(),
|
||||
)
|
||||
logger.debug(
|
||||
f"answer_astream func_calling=False question={question} rolling_msg={rolling_message} chunk_id={chunk_id}, chunk={parsed_chunk}"
|
||||
)
|
||||
yield parsed_chunk
|
||||
logger.debug(
|
||||
f"answer_astream func_calling=True question={question} rolling_msg={rolling_message} chunk_id={chunk_id}, chunk={parsed_chunk}"
|
||||
)
|
||||
yield parsed_chunk
|
||||
|
||||
chunk_id += 1
|
||||
# Last chunk provides metadata
|
||||
|
@ -22,5 +22,6 @@
|
||||
|
||||
.brain_name {
|
||||
@include Typography.EllipsisOverflow;
|
||||
font-size: Typography.$small;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { Icon } from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import styles from "./MentionItem.module.scss";
|
||||
@ -23,7 +25,15 @@ export const MentionItem = ({
|
||||
key={item.id}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name="brain" size="normal" color={selected ? "primary" : "black"} />
|
||||
{item.iconUrl ? (
|
||||
<Image src={item.iconUrl} alt="Brain or Model" width={14} height={14} />
|
||||
) : (
|
||||
<Icon
|
||||
name="brain"
|
||||
size="small"
|
||||
color={selected ? "primary" : "black"}
|
||||
/>
|
||||
)}
|
||||
<span className={styles.brain_name}>{item.label}</span>
|
||||
</span>
|
||||
);
|
||||
|
@ -7,16 +7,18 @@
|
||||
background-color: var(--background-0);
|
||||
flex-direction: column;
|
||||
padding: Spacings.$spacing03;
|
||||
box-shadow: BoxShadow.$small;
|
||||
box-shadow: BoxShadow.$medium;
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: Radius.$normal;
|
||||
gap: Spacings.$spacing03;
|
||||
gap: Spacings.$spacing02;
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
overflow: scroll;
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
|
||||
.mentions_list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing02;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ export const useBrainMention = () => {
|
||||
|
||||
const items: SuggestionItem[] = allBrains.map((brain) => ({
|
||||
id: brain.id,
|
||||
label: brain.name,
|
||||
label: brain.display_name ?? brain.name,
|
||||
type: "brain",
|
||||
iconUrl: brain.integration_logo_url,
|
||||
iconUrl: brain.image_url,
|
||||
}));
|
||||
|
||||
const { Mention: BrainMention } = useMentionConfig({
|
||||
|
@ -22,15 +22,15 @@ export const QADisplay = ({
|
||||
key={`user-${message_id}`}
|
||||
speaker={"user"}
|
||||
text={user_message}
|
||||
metadata={metadata} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||
metadata={metadata}
|
||||
/>
|
||||
<MessageRow
|
||||
key={`assistant-${message_id}`}
|
||||
speaker={"assistant"}
|
||||
text={assistant}
|
||||
brainName={brain_name}
|
||||
brainName={brain_name ?? metadata?.metadata_model?.display_name}
|
||||
index={index}
|
||||
metadata={metadata} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||
metadata={metadata}
|
||||
messageId={message_id}
|
||||
thumbs={thumbs}
|
||||
lastMessage={lastMessage}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { UUID } from "crypto";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useChatInput } from "@/app/chat/[chatId]/components/ActionsBar/components/ChatInput/hooks/useChatInput";
|
||||
@ -24,6 +25,11 @@ type MessageRowProps = {
|
||||
sources?: Source[];
|
||||
thoughts?: string;
|
||||
followup_questions?: string[];
|
||||
metadata_model?: {
|
||||
display_name: string;
|
||||
image_url: string;
|
||||
brain_id: UUID;
|
||||
};
|
||||
};
|
||||
index?: number;
|
||||
messageId?: string;
|
||||
@ -102,7 +108,10 @@ export const MessageRow = ({
|
||||
return (
|
||||
<div className={styles.message_header_wrapper}>
|
||||
<div className={styles.message_header}>
|
||||
<QuestionBrain brainName={brainName} />
|
||||
<QuestionBrain
|
||||
brainName={brainName}
|
||||
imageUrl={metadata?.metadata_model?.image_url ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
@use "styles/Radius.module.scss";
|
||||
@use "styles/Spacings.module.scss";
|
||||
@use "styles/Typography.module.scss";
|
||||
|
||||
@ -9,6 +10,7 @@
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
@ -70,6 +72,47 @@
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
background: var(--background-5);
|
||||
color: var(--white-0);
|
||||
padding: Spacings.$spacing05;
|
||||
border-radius: Radius.$normal;
|
||||
overflow: auto;
|
||||
margin: 0 0 Spacings.$spacing05 0;
|
||||
white-space: pre-wrap;
|
||||
font-size: Typography.$small;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
code[class*="language-"] {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border-radius: Radius.$normal;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: Typography.$small;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--background-5);
|
||||
color: var(--white-0);
|
||||
padding: Spacings.$spacing01;
|
||||
border-radius: Radius.$normal;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: Typography.$medium;
|
||||
}
|
||||
|
||||
.code_block {
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: Spacings.$spacing05;
|
||||
}
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thinking {
|
||||
@ -93,4 +136,4 @@
|
||||
100% {
|
||||
font-size: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,19 @@
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/components/prism-c";
|
||||
import "prismjs/components/prism-cpp";
|
||||
import "prismjs/components/prism-csharp";
|
||||
import "prismjs/components/prism-go";
|
||||
import "prismjs/components/prism-java";
|
||||
import "prismjs/components/prism-markup";
|
||||
import "prismjs/components/prism-python";
|
||||
import "prismjs/components/prism-rust";
|
||||
import "prismjs/components/prism-typescript";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import gfm from "remark-gfm";
|
||||
|
||||
import { Icon } from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import styles from "./MessageContent.module.scss";
|
||||
|
||||
export const MessageContent = ({
|
||||
@ -27,17 +39,13 @@ export const MessageContent = ({
|
||||
}
|
||||
|
||||
return {
|
||||
logs: logs.join(""), // Join with empty string, each log already has newline
|
||||
logs: logs.join(""),
|
||||
cleanedText: log.replace(logRegex, ""),
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (text.includes("🧠<")) {
|
||||
setIsLog(true);
|
||||
} else {
|
||||
setIsLog(false);
|
||||
}
|
||||
setIsLog(text.includes("🧠<"));
|
||||
}, [text]);
|
||||
|
||||
const { logs, cleanedText } = extractLog(text);
|
||||
@ -48,17 +56,64 @@ export const MessageContent = ({
|
||||
data-testid="chat-message-text"
|
||||
>
|
||||
{isLog && showLog && logs.length > 0 && (
|
||||
<div className="text-xs text-white p-2 rounded">
|
||||
<div
|
||||
className={`${styles.logContainer} text-xs text-white p-2 rounded`}
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={[gfm]}>{logs}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
<ReactMarkdown
|
||||
className={`
|
||||
${styles.markdown}
|
||||
${isUser ? styles.user : styles.brain}
|
||||
${cleanedText === "🧠" ? styles.thinking : ""}
|
||||
${styles.markdown}
|
||||
${isUser ? styles.user : styles.brain}
|
||||
${cleanedText === "🧠" ? styles.thinking : ""}
|
||||
`}
|
||||
remarkPlugins={[gfm]}
|
||||
components={{
|
||||
code: ({ className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className ?? "");
|
||||
if (match) {
|
||||
const language = match[1];
|
||||
const code = String(children).trim();
|
||||
const html = Prism.highlight(
|
||||
code,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
Prism.languages[language],
|
||||
language
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.code_block}>
|
||||
<div
|
||||
className={styles.icon}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(code);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="copy"
|
||||
size="small"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
/>
|
||||
</div>
|
||||
<pre className={className}>
|
||||
<code
|
||||
{...props}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<code {...props} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{cleanedText}
|
||||
</ReactMarkdown>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import Image from "next/image";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { Icon } from "@/lib/components/ui/Icon/Icon";
|
||||
@ -6,9 +7,11 @@ import styles from "./QuestionBrain.module.scss";
|
||||
|
||||
type QuestionBrainProps = {
|
||||
brainName?: string | null;
|
||||
imageUrl?: string;
|
||||
};
|
||||
export const QuestionBrain = ({
|
||||
brainName,
|
||||
imageUrl,
|
||||
}: QuestionBrainProps): JSX.Element => {
|
||||
if (brainName === undefined || brainName === null) {
|
||||
return <Fragment />;
|
||||
@ -16,7 +19,11 @@ export const QuestionBrain = ({
|
||||
|
||||
return (
|
||||
<div data-testid="brain-tags" className={styles.brain_name_wrapper}>
|
||||
<Icon name="brain" size="normal" color="black" />
|
||||
{imageUrl ? (
|
||||
<Image src={imageUrl} alt="" width={18} height={18} />
|
||||
) : (
|
||||
<Icon name="brain" size="normal" color="black" />
|
||||
)}
|
||||
<span className={styles.brain_name}>{brainName}</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { UUID } from "crypto";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { AddBrainModal } from "@/lib/components/AddBrainModal";
|
||||
@ -61,7 +60,12 @@ const SelectedChatPage = (): JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentBrain && messages.length > 0) {
|
||||
setCurrentBrainId(messages[messages.length - 1].brain_id as UUID);
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
setCurrentBrainId(
|
||||
lastMessage.brain_id
|
||||
? lastMessage.brain_id
|
||||
: lastMessage.metadata?.metadata_model?.brain_id ?? null
|
||||
);
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
|
@ -23,6 +23,11 @@ export type ChatMessage = {
|
||||
sources?: Source[];
|
||||
thoughts?: string;
|
||||
followup_questions?: string[];
|
||||
metadata_model?: {
|
||||
display_name: string;
|
||||
image_url: string;
|
||||
brain_id: UUID;
|
||||
};
|
||||
};
|
||||
thumbs?: boolean;
|
||||
};
|
||||
|
@ -121,4 +121,77 @@ body.dark_mode {
|
||||
|
||||
/* Box Shadow */
|
||||
--box-shadow: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #75715e;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #f92672;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number {
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #a6e22e;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string,
|
||||
.token.variable {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #e6db74;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #66d9ef;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: #fd971f;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
||||
|
||||
import styles from "./BrainButton.module.scss";
|
||||
|
||||
interface BrainButtonProps {
|
||||
brain: MinimalBrainForUser;
|
||||
newBrain: () => void;
|
||||
}
|
||||
|
||||
const BrainButton = ({ brain, newBrain }: BrainButtonProps): JSX.Element => {
|
||||
const { setCurrentBrainId } = useBrainContext();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.brain_button_container}
|
||||
onClick={() => {
|
||||
setCurrentBrainId(brain.id);
|
||||
newBrain();
|
||||
}}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<Icon
|
||||
name="brain"
|
||||
size="normal"
|
||||
color={hovered ? "primary" : "black"}
|
||||
/>
|
||||
<span className={styles.name}>{brain.name}</span>
|
||||
</div>
|
||||
<span className={styles.description}>{brain.description}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrainButton;
|
@ -17,10 +17,6 @@
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-0);
|
||||
|
||||
.header {
|
||||
color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
60
frontend/app/search/BrainsList/BrainButton/BrainButton.tsx
Normal file
60
frontend/app/search/BrainsList/BrainButton/BrainButton.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { UUID } from "crypto";
|
||||
import Image from "next/image";
|
||||
|
||||
import { Icon } from "@/lib/components/ui/Icon/Icon";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { BrainType } from "@/lib/types/BrainConfig";
|
||||
|
||||
import styles from "./BrainButton.module.scss";
|
||||
|
||||
export interface BrainOrModel {
|
||||
name: string;
|
||||
description: string;
|
||||
id: UUID;
|
||||
brain_type: BrainType;
|
||||
image_url?: string;
|
||||
price?: number;
|
||||
display_name?: string;
|
||||
}
|
||||
interface BrainButtonProps {
|
||||
brainOrModel: BrainOrModel;
|
||||
newBrain: () => void;
|
||||
}
|
||||
|
||||
const BrainButton = ({
|
||||
brainOrModel,
|
||||
newBrain,
|
||||
}: BrainButtonProps): JSX.Element => {
|
||||
const { setCurrentBrainId } = useBrainContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.brain_button_container}
|
||||
onClick={() => {
|
||||
setCurrentBrainId(brainOrModel.id);
|
||||
newBrain();
|
||||
}}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
{brainOrModel.image_url ? (
|
||||
<Image
|
||||
src={brainOrModel.image_url}
|
||||
alt="Brain or Model"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
) : (
|
||||
<Icon name="brain" size="normal" color="black" />
|
||||
)}
|
||||
<span className={styles.name}>
|
||||
{brainOrModel.display_name ?? brainOrModel.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className={styles.description}>{brainOrModel.description}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrainButton;
|
39
frontend/app/search/BrainsList/BrainsList.module.scss
Normal file
39
frontend/app/search/BrainsList/BrainsList.module.scss
Normal file
@ -0,0 +1,39 @@
|
||||
@use "styles/Radius.module.scss";
|
||||
@use "styles/ScreenSizes.module.scss";
|
||||
@use "styles/Spacings.module.scss";
|
||||
@use "styles/Variables.module.scss";
|
||||
|
||||
.brains_list_container {
|
||||
display: flex;
|
||||
margin-inline: -(Spacings.$spacing10);
|
||||
gap: calc(Spacings.$spacing05 + Spacings.$spacing01);
|
||||
|
||||
.brains_list_wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: Spacings.$spacing03;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
|
||||
@media screen and (min-width: ScreenSizes.$large) {
|
||||
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
visibility: visible;
|
||||
height: min-content;
|
||||
padding: Spacings.$spacing03;
|
||||
margin-top: calc(Variables.$brainButtonHeight / 2 - 20px);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-3);
|
||||
border-radius: Radius.$circle;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
111
frontend/app/search/BrainsList/BrainsList.tsx
Normal file
111
frontend/app/search/BrainsList/BrainsList.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Icon } from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import BrainButton, { BrainOrModel } from "./BrainButton/BrainButton";
|
||||
import styles from "./BrainsList.module.scss";
|
||||
|
||||
interface BrainsListProps {
|
||||
brains: BrainOrModel[];
|
||||
selectedTab: string;
|
||||
brainsPerPage: number;
|
||||
newBrain: () => void;
|
||||
}
|
||||
|
||||
const BrainsList = ({
|
||||
brains,
|
||||
selectedTab,
|
||||
brainsPerPage,
|
||||
newBrain,
|
||||
}: BrainsListProps): JSX.Element => {
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [transitionDirection, setTransitionDirection] = useState("");
|
||||
const [filteredBrains, setFilteredBrains] = useState<BrainOrModel[]>([]);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (currentPage < totalPages - 1) {
|
||||
setTransitionDirection("next");
|
||||
setCurrentPage((prevPage) => prevPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
if (currentPage > 0) {
|
||||
setTransitionDirection("prev");
|
||||
setCurrentPage((prevPage) => prevPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(0);
|
||||
let filtered = brains;
|
||||
|
||||
if (selectedTab === "Brains") {
|
||||
filtered = brains.filter((brain) => brain.brain_type === "doc");
|
||||
} else if (selectedTab === "Models") {
|
||||
filtered = brains.filter((brain) => brain.brain_type === "model");
|
||||
}
|
||||
|
||||
setFilteredBrains(filtered);
|
||||
setTotalPages(Math.ceil(filtered.length / brainsPerPage));
|
||||
}, [brains, selectedTab, brainsPerPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case "ArrowRight":
|
||||
handleNextPage();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
handlePreviousPage();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handleNextPage, handlePreviousPage]);
|
||||
|
||||
const displayedItems = filteredBrains.slice(
|
||||
currentPage * brainsPerPage,
|
||||
(currentPage + 1) * brainsPerPage
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.brains_list_container}>
|
||||
<div
|
||||
className={`${styles.chevron} ${
|
||||
currentPage === 0 ? styles.disabled : ""
|
||||
}`}
|
||||
onClick={handlePreviousPage}
|
||||
>
|
||||
<Icon name="chevronLeft" size="big" color="black" handleHover={true} />
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.brains_list_wrapper} ${
|
||||
transitionDirection === "next" ? styles.slide_next : styles.slide_prev
|
||||
}`}
|
||||
>
|
||||
{displayedItems.map((item, index) => (
|
||||
<BrainButton key={index} brainOrModel={item} newBrain={newBrain} />
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.chevron} ${
|
||||
currentPage >= totalPages - 1 ? styles.disabled : ""
|
||||
}`}
|
||||
onClick={handleNextPage}
|
||||
>
|
||||
<Icon name="chevronRight" size="big" color="black" handleHover={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrainsList;
|
@ -54,38 +54,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.brains_list_container {
|
||||
.assistants_container {
|
||||
display: flex;
|
||||
margin-inline: -(Spacings.$spacing10);
|
||||
gap: calc(Spacings.$spacing05 + Spacings.$spacing01);
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
|
||||
.brains_list_wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: Spacings.$spacing03;
|
||||
overflow: hidden;
|
||||
.tabs {
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
|
||||
@media screen and (min-width: ScreenSizes.$large) {
|
||||
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
visibility: visible;
|
||||
height: min-content;
|
||||
padding: Spacings.$spacing03;
|
||||
margin-top: calc(Variables.$brainButtonHeight / 2 - 20px);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-3);
|
||||
border-radius: Radius.$circle;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,10 @@ import { useBrainCreationContext } from "@/lib/components/AddBrainModal/brainCre
|
||||
import { OnboardingModal } from "@/lib/components/OnboardingModal/OnboardingModal";
|
||||
import { PageHeader } from "@/lib/components/PageHeader/PageHeader";
|
||||
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import { QuivrButton } from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar";
|
||||
import { SmallTabs } from "@/lib/components/ui/SmallTabs/SmallTabs";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useOnboardingContext } from "@/lib/context/OnboardingProvider/hooks/useOnboardingContext";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
@ -19,15 +19,15 @@ import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||
import { ButtonType } from "@/lib/types/QuivrButton";
|
||||
import { Tab } from "@/lib/types/Tab";
|
||||
|
||||
import BrainButton from "./BrainButton/BrainButton";
|
||||
import BrainsList from "./BrainsList/BrainsList";
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
const Search = (): JSX.Element => {
|
||||
const [selectedTab, setSelectedTab] = useState("Models");
|
||||
const [isUserDataFetched, setIsUserDataFetched] = useState(false);
|
||||
const [isNewBrain, setIsNewBrain] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [transitionDirection, setTransitionDirection] = useState("");
|
||||
const brainsPerPage = 6;
|
||||
|
||||
const pathname = usePathname();
|
||||
@ -52,6 +52,27 @@ const Search = (): JSX.Element => {
|
||||
},
|
||||
]);
|
||||
|
||||
const assistantsTabs: Tab[] = [
|
||||
{
|
||||
label: "Models",
|
||||
isSelected: selectedTab === "Models",
|
||||
onClick: () => setSelectedTab("Models"),
|
||||
iconName: "file",
|
||||
},
|
||||
{
|
||||
label: "Brains",
|
||||
isSelected: selectedTab === "Brains",
|
||||
onClick: () => setSelectedTab("Brains"),
|
||||
iconName: "settings",
|
||||
},
|
||||
{
|
||||
label: "All",
|
||||
isSelected: selectedTab === "All",
|
||||
onClick: () => setSelectedTab("All"),
|
||||
iconName: "settings",
|
||||
},
|
||||
];
|
||||
|
||||
const newBrain = () => {
|
||||
setIsNewBrain(true);
|
||||
setTimeout(() => {
|
||||
@ -59,22 +80,6 @@ const Search = (): JSX.Element => {
|
||||
}, 750);
|
||||
};
|
||||
|
||||
const totalPages = Math.ceil(allBrains.length / brainsPerPage);
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (currentPage < totalPages - 1) {
|
||||
setTransitionDirection("next");
|
||||
setCurrentPage((prevPage) => prevPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
if (currentPage > 0) {
|
||||
setTransitionDirection("prev");
|
||||
setCurrentPage((prevPage) => prevPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userIdentityData) {
|
||||
setIsUserDataFetched(true);
|
||||
@ -88,7 +93,9 @@ const Search = (): JSX.Element => {
|
||||
if (button.label === "Create brain") {
|
||||
return {
|
||||
...button,
|
||||
disabled: userData.max_brains <= allBrains.length,
|
||||
disabled:
|
||||
userData.max_brains <=
|
||||
allBrains.filter((brain) => brain.brain_type === "doc").length,
|
||||
};
|
||||
}
|
||||
|
||||
@ -104,39 +111,6 @@ const Search = (): JSX.Element => {
|
||||
}
|
||||
}, [pathname, session]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (document.activeElement) {
|
||||
const tagName = document.activeElement.tagName.toLowerCase();
|
||||
if (tagName !== "body") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
handlePreviousPage();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
handleNextPage();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handlePreviousPage, handleNextPage]);
|
||||
|
||||
const displayedBrains = allBrains.slice(
|
||||
currentPage * brainsPerPage,
|
||||
(currentPage + 1) * brainsPerPage
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.main_container}>
|
||||
@ -155,44 +129,16 @@ const Search = (): JSX.Element => {
|
||||
<div className={styles.search_bar_wrapper}>
|
||||
<SearchBar newBrain={isNewBrain} />
|
||||
</div>
|
||||
<div className={styles.brains_list_container}>
|
||||
<div
|
||||
className={`${styles.chevron} ${
|
||||
currentPage === 0 ? styles.disabled : ""
|
||||
}`}
|
||||
onClick={handlePreviousPage}
|
||||
>
|
||||
<Icon
|
||||
name="chevronLeft"
|
||||
size="big"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.brains_list_wrapper} ${
|
||||
transitionDirection === "next"
|
||||
? styles.slide_next
|
||||
: styles.slide_prev
|
||||
}`}
|
||||
>
|
||||
{displayedBrains.map((brain, index) => (
|
||||
<BrainButton key={index} brain={brain} newBrain={newBrain} />
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.chevron} ${
|
||||
currentPage >= totalPages - 1 ? styles.disabled : ""
|
||||
}`}
|
||||
onClick={handleNextPage}
|
||||
>
|
||||
<Icon
|
||||
name="chevronRight"
|
||||
size="big"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
/>
|
||||
<div className={styles.assistants_container}>
|
||||
<div className={styles.tabs}>
|
||||
<SmallTabs tabList={assistantsTabs} />
|
||||
</div>
|
||||
<BrainsList
|
||||
brains={allBrains}
|
||||
selectedTab={selectedTab}
|
||||
brainsPerPage={brainsPerPage}
|
||||
newBrain={newBrain}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,8 +55,9 @@ export const Analytics = (): JSX.Element => {
|
||||
{ label: "Last 90 days", value: Range.QUARTER },
|
||||
];
|
||||
|
||||
const brainsWithUploadRights =
|
||||
formatMinimalBrainsToSelectComponentInput(allBrains);
|
||||
const brainsWithUploadRights = formatMinimalBrainsToSelectComponentInput(
|
||||
allBrains.filter((brain) => brain.brain_type === "doc")
|
||||
);
|
||||
|
||||
const selectedGraphRangeOption = graphRangeOptions.find(
|
||||
(option) => option.value === currentChartRange
|
||||
|
@ -27,7 +27,9 @@ export const ManageBrains = (): JSX.Element => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BrainsList brains={brains} />
|
||||
<BrainsList
|
||||
brains={brains.filter((brain) => brain.brain_type === "doc")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -67,7 +67,9 @@ const Studio = (): JSX.Element => {
|
||||
if (button.label === "Create brain") {
|
||||
return {
|
||||
...button,
|
||||
disabled: userData.max_brains <= allBrains.length,
|
||||
disabled:
|
||||
userData.max_brains <=
|
||||
allBrains.filter((brain) => brain.brain_type === "doc").length,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,17 @@ export const getBrains = async (
|
||||
)
|
||||
).data;
|
||||
|
||||
return brains.map(mapBackendMinimalBrainToMinimalBrain);
|
||||
const sortedBrains = brains.sort((a, b) => {
|
||||
if (a.brain_type === "model" && b.brain_type !== "model") {
|
||||
return -1;
|
||||
} else if (a.brain_type !== "model" && b.brain_type === "model") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sortedBrains.map(mapBackendMinimalBrainToMinimalBrain);
|
||||
};
|
||||
|
||||
export type Subscription = { email: string; role: BrainRoleType };
|
||||
|
@ -15,4 +15,6 @@ export const mapBackendMinimalBrainToMinimalBrain = (
|
||||
integration_logo_url: backendMinimalBrain.integration_logo_url,
|
||||
max_files: backendMinimalBrain.max_files,
|
||||
allow_model_change: backendMinimalBrain.allow_model_change,
|
||||
image_url: backendMinimalBrain.image_url,
|
||||
display_name: backendMinimalBrain.display_name,
|
||||
});
|
||||
|
@ -1,4 +1,7 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
||||
import { useNotificationsContext } from "@/lib/context/NotificationsProvider/hooks/useNotificationsContext";
|
||||
|
||||
import styles from "./CurrentBrain.module.scss";
|
||||
@ -12,16 +15,66 @@ interface CurrentBrainProps {
|
||||
isNewBrain?: boolean;
|
||||
}
|
||||
|
||||
const BrainNameAndImage = ({
|
||||
currentBrain,
|
||||
isNewBrain,
|
||||
}: {
|
||||
currentBrain: MinimalBrainForUser;
|
||||
isNewBrain: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{currentBrain.image_url ? (
|
||||
<Image src={currentBrain.image_url} alt="" width={18} height={18} />
|
||||
) : (
|
||||
<Icon name="brain" size="normal" color="black" />
|
||||
)}
|
||||
<span className={`${styles.brain_name} ${isNewBrain ? styles.new : ""}`}>
|
||||
{currentBrain.display_name ?? currentBrain.name}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ProcessingNotification = ({
|
||||
currentBrain,
|
||||
bulkNotifications,
|
||||
}: {
|
||||
currentBrain?: MinimalBrainForUser;
|
||||
bulkNotifications: Array<{
|
||||
brain_id: string;
|
||||
notifications: Array<{ status: string }>;
|
||||
}>;
|
||||
}) => {
|
||||
const isProcessing =
|
||||
currentBrain &&
|
||||
bulkNotifications.some(
|
||||
(bulkNotif) =>
|
||||
bulkNotif.brain_id === currentBrain.id &&
|
||||
bulkNotif.notifications.some((notif) => notif.status === "info")
|
||||
);
|
||||
|
||||
return (
|
||||
isProcessing && (
|
||||
<div className={styles.warning}>
|
||||
<LoaderIcon size="small" color="warning" />
|
||||
<span>Processing knowledges</span>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const CurrentBrain = ({
|
||||
allowingRemoveBrain,
|
||||
remainingCredits,
|
||||
isNewBrain,
|
||||
}: CurrentBrainProps): JSX.Element => {
|
||||
const { currentBrain, setCurrentBrainId } = useBrainContext();
|
||||
const { bulkNotifications } = useNotificationsContext();
|
||||
|
||||
const removeCurrentBrain = (): void => {
|
||||
setCurrentBrainId(null);
|
||||
};
|
||||
const { bulkNotifications } = useNotificationsContext();
|
||||
|
||||
if (remainingCredits === 0) {
|
||||
return (
|
||||
@ -43,26 +96,14 @@ export const CurrentBrain = ({
|
||||
<div className={styles.left}>
|
||||
<span className={styles.title}>Talking to</span>
|
||||
<div className={styles.brain_name_wrapper}>
|
||||
<Icon
|
||||
name="brain"
|
||||
size="small"
|
||||
color={isNewBrain ? "primary" : "black"}
|
||||
<BrainNameAndImage
|
||||
currentBrain={currentBrain}
|
||||
isNewBrain={!!isNewBrain}
|
||||
/>
|
||||
<ProcessingNotification
|
||||
currentBrain={currentBrain}
|
||||
bulkNotifications={bulkNotifications}
|
||||
/>
|
||||
<span
|
||||
className={`${styles.brain_name} ${isNewBrain ? styles.new : ""}`}
|
||||
>
|
||||
{currentBrain.name}
|
||||
</span>
|
||||
{bulkNotifications.some(
|
||||
(bulkNotif) =>
|
||||
bulkNotif.brain_id === currentBrain.id &&
|
||||
bulkNotif.notifications.some((notif) => notif.status === "info")
|
||||
) && (
|
||||
<div className={styles.warning}>
|
||||
<LoaderIcon size="small" color="warning" />
|
||||
<span>Processing knowledges</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{allowingRemoveBrain && (
|
||||
@ -72,7 +113,7 @@ export const CurrentBrain = ({
|
||||
removeCurrentBrain();
|
||||
}}
|
||||
>
|
||||
<Icon size="normal" name="close" color="black" handleHover={true} />
|
||||
<Icon size="normal" name="close" color="black" handleHover />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
68
frontend/lib/components/ui/SmallTabs/SmallTabs.module.scss
Normal file
68
frontend/lib/components/ui/SmallTabs/SmallTabs.module.scss
Normal file
@ -0,0 +1,68 @@
|
||||
@use "styles/Radius.module.scss";
|
||||
@use "styles/ScreenSizes.module.scss";
|
||||
@use "styles/Spacings.module.scss";
|
||||
@use "styles/Typography.module.scss";
|
||||
|
||||
.tabs_container {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border-2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-block: Spacings.$spacing02;
|
||||
border-bottom: 1px solid transparent;
|
||||
padding-inline: Spacings.$spacing05;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
gap: Spacings.$spacing03;
|
||||
margin-bottom: -1px;
|
||||
z-index: 1;
|
||||
|
||||
&.selected {
|
||||
border-bottom-color: var(--primary-0);
|
||||
color: var(--primary-0);
|
||||
background-color: var(--background-2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-0);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--text-1);
|
||||
}
|
||||
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.label_wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: Spacings.$spacing02;
|
||||
|
||||
.label {
|
||||
font-size: Typography.$tiny;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.label_badge {
|
||||
border-radius: Radius.$circle;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--white-0);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--primary-0);
|
||||
font-size: Typography.$very-tiny;
|
||||
}
|
||||
}
|
||||
}
|
32
frontend/lib/components/ui/SmallTabs/SmallTabs.tsx
Normal file
32
frontend/lib/components/ui/SmallTabs/SmallTabs.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { Tab } from "@/lib/types/Tab";
|
||||
|
||||
import styles from "./SmallTabs.module.scss";
|
||||
|
||||
type TabsProps = {
|
||||
tabList: Tab[];
|
||||
};
|
||||
|
||||
export const SmallTabs = ({ tabList }: TabsProps): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.tabs_container}>
|
||||
{tabList.map((tab, index) => (
|
||||
<div
|
||||
className={`
|
||||
${styles.tab_wrapper}
|
||||
${tab.isSelected ? styles.selected : ""}
|
||||
${tab.disabled ? styles.disabled : ""}
|
||||
`}
|
||||
key={index}
|
||||
onClick={tab.onClick}
|
||||
>
|
||||
<div className={styles.label_wrapper}>
|
||||
<span className={styles.label}>{tab.label}</span>
|
||||
{!!tab.badge && tab.badge > 0 && (
|
||||
<div className={styles.label_badge}>{tab.badge}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -3,7 +3,6 @@ import { UUID } from "crypto";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useBrainFetcher } from "@/app/studio/[brainId]/BrainManagementTabs/hooks/useBrainFetcher";
|
||||
import { CreateBrainInput } from "@/lib/api/brain/types";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
import { usePromptApi } from "@/lib/api/prompt/usePromptApi";
|
||||
@ -31,9 +30,6 @@ export const useBrainProvider = () => {
|
||||
(prompt) => prompt.id === currentPromptId
|
||||
);
|
||||
const currentBrain = allBrains.find((brain) => brain.id === currentBrainId);
|
||||
const { brain: currentBrainDetails } = useBrainFetcher({
|
||||
brainId: currentBrainId ?? undefined,
|
||||
});
|
||||
|
||||
const fetchAllBrains = useCallback(async () => {
|
||||
setIsFetchingBrains(true);
|
||||
@ -95,7 +91,7 @@ export const useBrainProvider = () => {
|
||||
isFetchingBrains,
|
||||
|
||||
currentBrain,
|
||||
currentBrainDetails,
|
||||
|
||||
currentBrainId,
|
||||
setCurrentBrainId,
|
||||
|
||||
|
@ -36,6 +36,8 @@ export type Brain = {
|
||||
brain_definition?: ApiBrainDefinition;
|
||||
integration_description?: IntegrationDescription;
|
||||
max_files?: number;
|
||||
image_url?: string;
|
||||
display_name?: string;
|
||||
};
|
||||
|
||||
export type MinimalBrainForUser = {
|
||||
@ -48,6 +50,8 @@ export type MinimalBrainForUser = {
|
||||
integration_logo_url?: string;
|
||||
max_files: number;
|
||||
allow_model_change: boolean;
|
||||
image_url?: string;
|
||||
display_name?: string;
|
||||
};
|
||||
|
||||
//TODO: rename rights to role in Backend and use MinimalBrainForUser instead of BackendMinimalBrainForUser
|
||||
|
@ -4,7 +4,13 @@ import { ApiBrainDefinition } from "../api/brain/types";
|
||||
|
||||
export const brainStatuses = ["private", "public"] as const;
|
||||
|
||||
export const brainTypes = ["doc", "api", "composite", "integration"] as const;
|
||||
export const brainTypes = [
|
||||
"doc",
|
||||
"api",
|
||||
"composite",
|
||||
"integration",
|
||||
"model",
|
||||
] as const;
|
||||
|
||||
export type BrainType = (typeof brainTypes)[number];
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { iconList } from "../helpers/iconList";
|
||||
|
||||
export interface Tab {
|
||||
label: string;
|
||||
label?: string;
|
||||
isSelected: boolean;
|
||||
disabled?: boolean;
|
||||
iconName: keyof typeof iconList;
|
||||
|
@ -86,6 +86,7 @@
|
||||
"posthog-js": "1.96.1",
|
||||
"prettier": "2.8.8",
|
||||
"pretty-bytes": "6.1.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -112,6 +113,7 @@
|
||||
"@tailwindcss/typography": "0.5.9",
|
||||
"@testing-library/jest-dom": "6.1.3",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@types/prismjs": "^1.26.4",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "4.0.4",
|
||||
"dotenv": "16.3.1",
|
||||
|
@ -2678,6 +2678,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.0.tgz"
|
||||
integrity sha512-qwfpsHmFuhAS/dVd4uBIraMxRd56vwBUYQGZ6GpXnFuM2XMRFJbIyruFKKlW2daQliuYZwe0qfn/UjFCDKic5g==
|
||||
|
||||
"@types/prismjs@^1.26.4":
|
||||
version "1.26.4"
|
||||
resolved "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.4.tgz"
|
||||
integrity sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.11"
|
||||
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz"
|
||||
@ -2721,12 +2726,7 @@
|
||||
resolved "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz"
|
||||
integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==
|
||||
|
||||
"@types/unist@*":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz"
|
||||
integrity sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==
|
||||
|
||||
"@types/unist@^2", "@types/unist@^2.0.0":
|
||||
"@types/unist@*", "@types/unist@^2", "@types/unist@^2.0.0":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz"
|
||||
integrity sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==
|
||||
@ -3372,7 +3372,7 @@ chalk@^2.4.2:
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^3.0.0:
|
||||
chalk@^3.0.0, chalk@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz"
|
||||
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
|
||||
@ -3396,14 +3396,6 @@ chalk@^4.1.0:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz"
|
||||
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
change-case@^5.4.2:
|
||||
version "5.4.2"
|
||||
resolved "https://registry.npmjs.org/change-case/-/change-case-5.4.2.tgz"
|
||||
@ -3758,14 +3750,7 @@ date-fns@2.30.0:
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
|
||||
debug@^2.2.0:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^2.6.9:
|
||||
debug@^2.2.0, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
@ -3786,7 +3771,35 @@ debug@^4.0.0:
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@4:
|
||||
debug@^4.1.0:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.1.1:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.3.1:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.3.2:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.3.4, debug@4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
@ -4813,7 +4826,7 @@ github-from-package@0.0.0:
|
||||
resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz"
|
||||
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
glob-parent@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
@ -4827,7 +4840,25 @@ glob-parent@^6.0.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@^10.2.2, glob@^10.3.10, glob@10.3.10:
|
||||
glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^10.2.2:
|
||||
version "10.3.10"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz"
|
||||
integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
|
||||
dependencies:
|
||||
foreground-child "^3.1.0"
|
||||
jackspeak "^2.3.5"
|
||||
minimatch "^9.0.1"
|
||||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
path-scurry "^1.10.1"
|
||||
|
||||
glob@^10.3.10:
|
||||
version "10.3.10"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz"
|
||||
integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
|
||||
@ -4861,6 +4892,17 @@ glob@^8.0.3:
|
||||
minimatch "^5.0.1"
|
||||
once "^1.3.0"
|
||||
|
||||
glob@10.3.10:
|
||||
version "10.3.10"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz"
|
||||
integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
|
||||
dependencies:
|
||||
foreground-child "^3.1.0"
|
||||
jackspeak "^2.3.5"
|
||||
minimatch "^9.0.1"
|
||||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
path-scurry "^1.10.1"
|
||||
|
||||
glob@7.1.6:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
|
||||
@ -5884,7 +5926,7 @@ lowlight@^2.0.0:
|
||||
fault "^2.0.0"
|
||||
highlight.js "~11.8.0"
|
||||
|
||||
lru-cache@^10.0.1, "lru-cache@^9.1.1 || ^10.0.0":
|
||||
lru-cache@^10.0.1:
|
||||
version "10.0.3"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz"
|
||||
integrity sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==
|
||||
@ -5903,6 +5945,11 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
"lru-cache@^9.1.1 || ^10.0.0":
|
||||
version "10.0.3"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz"
|
||||
integrity sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==
|
||||
|
||||
lz-string@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz"
|
||||
@ -7233,6 +7280,11 @@ pretty-format@^29.5.0:
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^18.0.0"
|
||||
|
||||
prismjs@^1.29.0:
|
||||
version "1.29.0"
|
||||
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz"
|
||||
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
|
||||
|
||||
proc-log@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user