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:
Antoine Dewez 2024-08-08 16:21:28 +02:00 committed by GitHub
parent 111200184b
commit ef6037e665
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 818 additions and 302 deletions

View File

@ -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,
)

View File

@ -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__)

View File

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

View File

@ -22,5 +22,6 @@
.brain_name {
@include Typography.EllipsisOverflow;
font-size: Typography.$small;
}
}

View File

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

View File

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

View File

@ -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({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -122,3 +122,76 @@ 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;
}

View File

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

View File

@ -17,10 +17,6 @@
&:hover {
border-color: var(--primary-0);
.header {
color: var(--primary-0);
}
}
.header {

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

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

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,9 @@ export const ManageBrains = (): JSX.Element => {
/>
</div>
<BrainsList brains={brains} />
<BrainsList
brains={brains.filter((brain) => brain.brain_type === "doc")}
/>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { iconList } from "../helpers/iconList";
export interface Tab {
label: string;
label?: string;
isSelected: boolean;
disabled?: boolean;
iconName: keyof typeof iconList;

View File

@ -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",

View File

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