mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-08-17 02:10:31 +03:00
feat(frontend): new chat interface (#2687)
# Description Please include a summary of the changes and the related issue. Please also include relevant motivation and context. ## Checklist before requesting a review Please delete options that are not relevant. - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented hard-to-understand areas - [ ] I have ideally added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged ## Screenshots (if appropriate):
This commit is contained in:
parent
b464ed3660
commit
faaf9b6dba
@ -7,11 +7,15 @@ export const useChatInput = () => {
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const { addQuestion, generatingAnswer, chatId } = useChat();
|
||||
|
||||
const submitQuestion = useCallback(() => {
|
||||
if (!generatingAnswer) {
|
||||
void addQuestion(message, () => setMessage(""));
|
||||
}
|
||||
}, [addQuestion, generatingAnswer, message]);
|
||||
const submitQuestion = useCallback(
|
||||
(question?: string) => {
|
||||
const finalMessage = question ?? message;
|
||||
if (!generatingAnswer) {
|
||||
void addQuestion(finalMessage, () => setMessage(""));
|
||||
}
|
||||
},
|
||||
[addQuestion, generatingAnswer, message]
|
||||
);
|
||||
|
||||
return {
|
||||
message,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ChatMessage } from "@/app/chat/[chatId]/types";
|
||||
|
||||
import { MessageRow } from "./components";
|
||||
import { MessageRow } from "./components/MessageRow/MessageRow";
|
||||
import "./styles.css";
|
||||
|
||||
type QADisplayProps = {
|
||||
@ -18,7 +18,6 @@ export const QADisplay = ({
|
||||
message_id,
|
||||
user_message,
|
||||
brain_name,
|
||||
prompt_title,
|
||||
metadata,
|
||||
brain_id,
|
||||
thumbs,
|
||||
@ -30,7 +29,6 @@ export const QADisplay = ({
|
||||
key={`user-${message_id}`}
|
||||
speaker={"user"}
|
||||
text={user_message}
|
||||
promptName={prompt_title}
|
||||
metadata={metadata} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||
/>
|
||||
<MessageRow
|
||||
@ -38,7 +36,6 @@ export const QADisplay = ({
|
||||
speaker={"assistant"}
|
||||
text={assistant}
|
||||
brainName={brain_name}
|
||||
promptName={prompt_title}
|
||||
brainId={brain_id}
|
||||
index={index}
|
||||
metadata={metadata} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||
|
@ -1,56 +1,75 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Transitions.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.message_row_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-right: Spacings.$spacing05;
|
||||
width: 85%;
|
||||
padding-block: Spacings.$spacing03;
|
||||
gap: Spacings.$spacing03;
|
||||
border-bottom: 1px solid var(--border-0);
|
||||
padding-bottom: Spacings.$spacing05;
|
||||
position: relative;
|
||||
|
||||
&.user {
|
||||
font-size: Typography.$very_large;
|
||||
font-weight: 500;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.last {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.smaller {
|
||||
font-size: Typography.$large;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ScreenSizes.$small) {
|
||||
&.user {
|
||||
font-size: Typography.$large;
|
||||
}
|
||||
}
|
||||
|
||||
.icon_rotate {
|
||||
position: absolute;
|
||||
right: Spacings.$spacing04;
|
||||
top: -(Spacings.$spacing05);
|
||||
transition: transform 0.3s Transitions.$easeOutBack;
|
||||
}
|
||||
|
||||
.icon_rotate_down {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.icon_rotate_up {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.message_row_content {
|
||||
align-self: flex-end;
|
||||
border-radius: Radius.$big;
|
||||
width: fit-content;
|
||||
padding-block: Spacings.$spacing03;
|
||||
padding-inline: Spacings.$spacing05;
|
||||
}
|
||||
|
||||
.message_header {
|
||||
padding: Spacings.$spacing02;
|
||||
.message_header_wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.user {
|
||||
align-self: flex-end;
|
||||
|
||||
.message_header {
|
||||
align-self: flex-end;
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
align-items: center;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.message_row_content {
|
||||
background-color: var(--background-2);
|
||||
}
|
||||
}
|
||||
|
||||
&.brain {
|
||||
.message_header {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing04;
|
||||
align-items: baseline;
|
||||
@include Typography.H2;
|
||||
}
|
||||
|
||||
.message_row_content {
|
||||
align-self: flex-start;
|
||||
background-color: var(--background-special-0);
|
||||
margin-left: 1px;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.metadata_wrapper {
|
||||
@ -65,64 +84,77 @@
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing03;
|
||||
max-width: 100%;
|
||||
padding-bottom: Spacings.$spacing05;
|
||||
|
||||
.title_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: Spacings.$spacing03;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
}
|
||||
}
|
||||
|
||||
.sources {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
column-gap: Spacings.$spacing06;
|
||||
row-gap: Spacings.$spacing03;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.citations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing02;
|
||||
padding: Spacings.$spacing03;
|
||||
border: 1px solid var(--primary-0);
|
||||
border-radius: Radius.$big;
|
||||
font-size: Typography.$small;
|
||||
|
||||
.file_name_wrapper {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
align-items: center;
|
||||
|
||||
.box_title {
|
||||
padding-block: Spacings.$spacing02;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.source {
|
||||
font-size: Typography.$tiny;
|
||||
}
|
||||
}
|
||||
|
||||
.box_title {
|
||||
padding-block: Spacings.$spacing02;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icons_wrapper {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
gap: Spacings.$spacing04;
|
||||
padding-top: Spacings.$spacing03;
|
||||
padding-bottom: Spacings.$spacing05;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
|
||||
.sources_icon_wrapper {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.sticky {
|
||||
visibility: visible;
|
||||
.with_border {
|
||||
border-bottom: 1px solid var(--border-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.metadata_wrapper {
|
||||
.icons_wrapper {
|
||||
visibility: visible;
|
||||
.related_questions_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing03;
|
||||
padding-block: Spacings.$spacing03;
|
||||
|
||||
.title_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: Spacings.$spacing03;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
}
|
||||
}
|
||||
|
||||
.questions_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing02;
|
||||
font-size: Typography.$small;
|
||||
|
||||
.question {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: Spacings.$spacing03;
|
||||
|
||||
.text {
|
||||
color: var(--text-4);
|
||||
transition: color 0.5s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-3);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useChatInput } from "@/app/chat/[chatId]/components/ActionsBar/components/ChatInput/hooks/useChatInput";
|
||||
import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
|
||||
import { useChatApi } from "@/lib/api/chat/useChatApi";
|
||||
import { CopyButton } from "@/lib/components/ui/CopyButton";
|
||||
@ -8,10 +9,8 @@ import { ThoughtsButton } from "@/lib/components/ui/ThoughtsButton";
|
||||
import { Source } from "@/lib/types/MessageMetadata";
|
||||
|
||||
import styles from "./MessageRow.module.scss";
|
||||
import { Citation } from "./components/Citation/Citation";
|
||||
import { MessageContent } from "./components/MessageContent/MessageContent";
|
||||
import { QuestionBrain } from "./components/QuestionBrain/QuestionBrain";
|
||||
import { QuestionPrompt } from "./components/QuestionPrompt/QuestionPrompt";
|
||||
import { SourceCitations } from "./components/Source/Source";
|
||||
import { useMessageRow } from "./hooks/useMessageRow";
|
||||
import { SourceFile } from "./types/types";
|
||||
@ -20,11 +19,11 @@ type MessageRowProps = {
|
||||
speaker: "user" | "assistant";
|
||||
text?: string;
|
||||
brainName?: string | null;
|
||||
promptName?: string | null;
|
||||
children?: React.ReactNode;
|
||||
metadata?: {
|
||||
sources?: Source[];
|
||||
thoughts?: string;
|
||||
followup_questions?: string[];
|
||||
};
|
||||
brainId?: string;
|
||||
index?: number;
|
||||
@ -33,200 +32,209 @@ type MessageRowProps = {
|
||||
lastMessage?: boolean;
|
||||
};
|
||||
|
||||
export const MessageRow = React.forwardRef(
|
||||
(
|
||||
{
|
||||
speaker,
|
||||
text,
|
||||
brainName,
|
||||
promptName,
|
||||
children,
|
||||
brainId,
|
||||
messageId,
|
||||
thumbs: initialThumbs,
|
||||
lastMessage,
|
||||
metadata,
|
||||
}: MessageRowProps,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) => {
|
||||
const { handleCopy, isUserSpeaker } = useMessageRow({
|
||||
speaker,
|
||||
text,
|
||||
});
|
||||
const { updateChatMessage } = useChatApi();
|
||||
const { chatId } = useChat();
|
||||
const [thumbs, setThumbs] = useState<boolean | undefined | null>(
|
||||
initialThumbs
|
||||
export const MessageRow = ({
|
||||
speaker,
|
||||
text,
|
||||
brainName,
|
||||
children,
|
||||
brainId,
|
||||
messageId,
|
||||
thumbs: initialThumbs,
|
||||
metadata,
|
||||
lastMessage,
|
||||
}: MessageRowProps): JSX.Element => {
|
||||
const { handleCopy, isUserSpeaker } = useMessageRow({
|
||||
speaker,
|
||||
text,
|
||||
});
|
||||
const { updateChatMessage } = useChatApi();
|
||||
const { chatId } = useChat();
|
||||
const [thumbs, setThumbs] = useState<boolean | undefined | null>(
|
||||
initialThumbs
|
||||
);
|
||||
const [folded, setFolded] = useState<boolean>(false);
|
||||
const [sourceFiles, setSourceFiles] = useState<SourceFile[]>([]);
|
||||
const { submitQuestion } = useChatInput();
|
||||
|
||||
useEffect(() => {
|
||||
setThumbs(initialThumbs);
|
||||
setSourceFiles(
|
||||
metadata?.sources?.reduce((acc, source) => {
|
||||
const existingSource = acc.find((s) => s.filename === source.name);
|
||||
if (existingSource) {
|
||||
existingSource.citations.push(source.citation);
|
||||
} else {
|
||||
acc.push({
|
||||
filename: source.name,
|
||||
file_url: source.source_url,
|
||||
citations: [source.citation],
|
||||
selected: false,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as SourceFile[]) ?? []
|
||||
);
|
||||
const [sourceFiles, setSourceFiles] = useState<SourceFile[]>([]);
|
||||
const [selectedSourceFile, setSelectedSourceFile] =
|
||||
useState<SourceFile | null>(null);
|
||||
}, [initialThumbs, metadata]);
|
||||
|
||||
const handleSourceFileClick = (sourceFile: SourceFile) => {
|
||||
setSelectedSourceFile((prev) =>
|
||||
prev && prev.filename === sourceFile.filename ? null : sourceFile
|
||||
);
|
||||
};
|
||||
const messageContent = text ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
setThumbs(initialThumbs);
|
||||
setSourceFiles(
|
||||
metadata?.sources?.reduce((acc, source) => {
|
||||
const existingSource = acc.find((s) => s.filename === source.name);
|
||||
if (existingSource) {
|
||||
existingSource.citations.push(source.citation);
|
||||
} else {
|
||||
acc.push({
|
||||
filename: source.name,
|
||||
file_url: source.source_url,
|
||||
citations: [source.citation],
|
||||
selected: false,
|
||||
});
|
||||
}
|
||||
const thumbsUp = async () => {
|
||||
if (chatId && messageId) {
|
||||
await updateChatMessage(chatId, messageId, {
|
||||
thumbs: thumbs ? null : true,
|
||||
});
|
||||
setThumbs(thumbs ? null : true);
|
||||
}
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, [] as SourceFile[]) ?? []
|
||||
);
|
||||
}, [initialThumbs, metadata]);
|
||||
const thumbsDown = async () => {
|
||||
if (chatId && messageId) {
|
||||
await updateChatMessage(chatId, messageId, {
|
||||
thumbs: thumbs === false ? null : false,
|
||||
});
|
||||
setThumbs(thumbs === false ? null : false);
|
||||
}
|
||||
};
|
||||
|
||||
const messageContent = text ?? "";
|
||||
|
||||
const thumbsUp = async () => {
|
||||
if (chatId && messageId) {
|
||||
await updateChatMessage(chatId, messageId, {
|
||||
thumbs: thumbs ? null : true,
|
||||
});
|
||||
setThumbs(thumbs ? null : true);
|
||||
}
|
||||
};
|
||||
|
||||
const thumbsDown = async () => {
|
||||
if (chatId && messageId) {
|
||||
await updateChatMessage(chatId, messageId, {
|
||||
thumbs: thumbs === false ? null : false,
|
||||
});
|
||||
setThumbs(thumbs === false ? null : false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderMessageHeader = () => {
|
||||
if (!isUserSpeaker) {
|
||||
return (
|
||||
const renderMessageHeader = () => {
|
||||
if (!isUserSpeaker && !folded) {
|
||||
return (
|
||||
<div className={styles.message_header_wrapper}>
|
||||
<div className={styles.message_header}>
|
||||
<QuestionBrain brainName={brainName} brainId={brainId} />
|
||||
<QuestionPrompt promptName={promptName} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={styles.message_header}>
|
||||
<Icon name="user" color="dark-grey" size="normal" />
|
||||
<span className={styles.me}>Me</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderMetadata = () => {
|
||||
if (!isUserSpeaker && messageContent !== "🧠") {
|
||||
return (
|
||||
const renderMetadata = () => {
|
||||
if (!isUserSpeaker && messageContent !== "🧠") {
|
||||
return (
|
||||
<div className={styles.metadata_wrapper}>
|
||||
<div
|
||||
className={`${styles.metadata_wrapper} ${
|
||||
lastMessage ? styles.sticky : ""
|
||||
className={`${styles.icons_wrapper} ${
|
||||
sourceFiles.length === 0 ? styles.with_border : ""
|
||||
}`}
|
||||
>
|
||||
{metadata?.thoughts && metadata.thoughts.trim() !== "" && (
|
||||
<ThoughtsButton text={metadata.thoughts} size="small" />
|
||||
)}
|
||||
<CopyButton handleCopy={handleCopy} size="small" />
|
||||
<Icon
|
||||
name="thumbsUp"
|
||||
handleHover={true}
|
||||
color={thumbs ? "primary" : "black"}
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
await thumbsUp();
|
||||
}}
|
||||
/>
|
||||
<Icon
|
||||
name="thumbsDown"
|
||||
handleHover={true}
|
||||
color={thumbs === false ? "primary" : "black"}
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
await thumbsDown();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sourceFiles.length > 0 && (
|
||||
<div className={styles.sources_and_citations_wrapper}>
|
||||
<div className={styles.title_wrapper}>
|
||||
<Icon name="sources" size="normal" color="black" />
|
||||
<span className={styles.title}>Sources</span>
|
||||
</div>
|
||||
<div className={styles.sources}>
|
||||
{sourceFiles.map((sourceFile, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => handleSourceFileClick(sourceFile)}
|
||||
>
|
||||
<SourceCitations
|
||||
sourceFile={sourceFile}
|
||||
isSelected={
|
||||
!!selectedSourceFile &&
|
||||
selectedSourceFile.filename === sourceFile.filename
|
||||
}
|
||||
/>
|
||||
<div key={i}>
|
||||
<SourceCitations sourceFile={sourceFile} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedSourceFile && (
|
||||
<div className={styles.citations}>
|
||||
<div className={styles.file_name_wrapper}>
|
||||
<span className={styles.box_title}>Source:</span>
|
||||
<a
|
||||
href={selectedSourceFile.file_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className={styles.source}>
|
||||
{selectedSourceFile.filename}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{selectedSourceFile.citations.map((citation, i) => (
|
||||
<div key={i}>
|
||||
<Citation citation={citation} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.icons_wrapper}>
|
||||
{metadata?.thoughts && metadata.thoughts.trim() !== "" && (
|
||||
<ThoughtsButton
|
||||
text={metadata.thoughts}
|
||||
size="normal"
|
||||
/>
|
||||
)}
|
||||
<CopyButton handleCopy={handleCopy} size="normal" />
|
||||
<Icon
|
||||
name="thumbsUp"
|
||||
handleHover={true}
|
||||
color={thumbs ? "primary" : "black"}
|
||||
size="normal"
|
||||
onClick={async () => {
|
||||
await thumbsUp();
|
||||
}}
|
||||
/>
|
||||
<Icon
|
||||
name="thumbsDown"
|
||||
handleHover={true}
|
||||
color={thumbs === false ? "primary" : "black"}
|
||||
size="normal"
|
||||
onClick={async () => {
|
||||
await thumbsDown();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
${styles.message_row_container}
|
||||
${isUserSpeaker ? styles.user : styles.brain}
|
||||
`}
|
||||
>
|
||||
{renderMessageHeader()}
|
||||
<div ref={ref} className={styles.message_row_content}>
|
||||
{children ?? (
|
||||
<>
|
||||
<MessageContent text={messageContent} isUser={isUserSpeaker} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{renderMetadata()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
MessageRow.displayName = "MessageRow";
|
||||
const renderRelatedQuestions = () => {
|
||||
if (
|
||||
!isUserSpeaker &&
|
||||
!folded &&
|
||||
(metadata?.followup_questions?.length ?? 0) > 0
|
||||
) {
|
||||
return (
|
||||
<div className={styles.related_questions_wrapper}>
|
||||
<div className={styles.title_wrapper}>
|
||||
<Icon name="search" color="black" size="normal" />
|
||||
<span className={styles.title}>Follow up questions</span>
|
||||
</div>
|
||||
<div className={styles.questions_wrapper}>
|
||||
{metadata?.followup_questions?.map((question, index) => (
|
||||
<div
|
||||
className={styles.question}
|
||||
key={index}
|
||||
onClick={() => submitQuestion(question)}
|
||||
>
|
||||
<Icon name="followUp" size="small" color="grey" />
|
||||
<span className={styles.text}>{question}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderOtherSections = () => {
|
||||
return (
|
||||
<>
|
||||
{!folded && renderMetadata()}
|
||||
{!folded && renderRelatedQuestions()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
${styles.message_row_container}
|
||||
${isUserSpeaker ? styles.user : styles.brain}
|
||||
${messageContent.length > 100 && isUserSpeaker ? styles.smaller : ""}
|
||||
${lastMessage ? styles.last : ""}
|
||||
`}
|
||||
>
|
||||
{!isUserSpeaker && messageContent !== "🧠" && (
|
||||
<div onClick={() => setFolded(!folded)}>
|
||||
<Icon
|
||||
name="chevronDown"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
size="normal"
|
||||
classname={`${styles.icon_rotate} ${
|
||||
folded ? styles.icon_rotate_down : styles.icon_rotate_up
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{renderMessageHeader()}
|
||||
<div className={styles.message_row_content}>
|
||||
{children ?? (
|
||||
<>
|
||||
<MessageContent
|
||||
text={messageContent}
|
||||
isUser={isUserSpeaker}
|
||||
hide={folded}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{renderOtherSections()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,38 +0,0 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.citation_wrapper {
|
||||
padding: Spacings.$spacing03;
|
||||
border-radius: Radius.$normal;
|
||||
background-color: var(--background-special-0);
|
||||
width: 100%;
|
||||
font-size: Typography.$tiny;
|
||||
font-style: italic;
|
||||
cursor: pointer;
|
||||
|
||||
.citation_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
.citation {
|
||||
&.folded {
|
||||
@include Typography.EllipsisOverflow;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-special-1);
|
||||
|
||||
.icon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import styles from "./Citation.module.scss";
|
||||
|
||||
type CitationProps = {
|
||||
citation: string;
|
||||
};
|
||||
export const Citation = ({ citation }: CitationProps): JSX.Element => {
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||
|
||||
const contentIndex = citation.indexOf("Content:");
|
||||
let cleanedCitation, content;
|
||||
|
||||
if (contentIndex !== -1) {
|
||||
cleanedCitation = citation.substring(contentIndex);
|
||||
[, content] = cleanedCitation.split("Content:");
|
||||
} else {
|
||||
content = citation;
|
||||
}
|
||||
|
||||
const handleIconClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.citation_wrapper}
|
||||
onClick={(event) => handleIconClick(event)}
|
||||
>
|
||||
<div className={styles.citation_header}>
|
||||
<span
|
||||
className={`${styles.citation} ${!isExpanded ? styles.folded : ""}`}
|
||||
>
|
||||
{content}
|
||||
</span>
|
||||
<div className={styles.icon}>
|
||||
<Icon
|
||||
name={isExpanded ? "fold" : "unfold"}
|
||||
size="normal"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.modal_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
|
||||
.title_wrapper {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
align-items: baseline;
|
||||
overflow: hidden;
|
||||
padding-right: Spacings.$spacing05;
|
||||
|
||||
.title {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file_link {
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
@include Typography.EllipsisOverflow;
|
||||
|
||||
.filename {
|
||||
@include Typography.EllipsisOverflow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.citation {
|
||||
font-size: Typography.$small;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import { Modal } from "@/lib/components/ui/Modal/Modal";
|
||||
|
||||
import styles from "./CitationModal.module.scss";
|
||||
|
||||
import { SourceFile } from "../../types/types";
|
||||
|
||||
type CitationModalProps = {
|
||||
citation: string;
|
||||
sourceFile: SourceFile;
|
||||
isModalOpened: boolean;
|
||||
setIsModalOpened: (isModalOpened: boolean) => void;
|
||||
};
|
||||
export const CitationModal = ({
|
||||
citation,
|
||||
sourceFile,
|
||||
isModalOpened,
|
||||
setIsModalOpened,
|
||||
}: CitationModalProps): JSX.Element => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isModalOpened}
|
||||
setOpen={setIsModalOpened}
|
||||
CloseTrigger={<div />}
|
||||
>
|
||||
<div className={styles.modal_wrapper}>
|
||||
<div className={styles.title_wrapper}>
|
||||
<span className={styles.title}>Text extract from:</span>
|
||||
<a
|
||||
href={sourceFile.file_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.file_link}
|
||||
>
|
||||
<span className={styles.filename}>{sourceFile.filename}</span>
|
||||
</a>
|
||||
</div>
|
||||
<span className={styles.citation}>
|
||||
{citation
|
||||
.split("Content:")
|
||||
.slice(1)
|
||||
.join("")
|
||||
.replace(/\n{3,}/g, "\n\n")}
|
||||
</span>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -1,6 +1,10 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.hiden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
p {
|
||||
margin: 0;
|
||||
@ -12,6 +16,9 @@
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
margin-left: Spacings.$spacing05;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing03;
|
||||
|
||||
li {
|
||||
white-space-collapse: collapse;
|
||||
|
@ -6,9 +6,11 @@ import styles from "./MessageContent.module.scss";
|
||||
export const MessageContent = ({
|
||||
text,
|
||||
isUser,
|
||||
hide,
|
||||
}: {
|
||||
text: string;
|
||||
isUser: boolean;
|
||||
hide: boolean;
|
||||
}): JSX.Element => {
|
||||
const [showLog] = useState(true);
|
||||
const [isLog, setIsLog] = useState(true);
|
||||
@ -19,7 +21,6 @@ export const MessageContent = ({
|
||||
let match;
|
||||
|
||||
while ((match = logRegex.exec(log))) {
|
||||
// Add two spaces and a newline for markdown line break
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
logs.push("- " + match[1] + " \n");
|
||||
}
|
||||
@ -41,7 +42,10 @@ export const MessageContent = ({
|
||||
const { logs, cleanedText } = extractLog(text);
|
||||
|
||||
return (
|
||||
<div data-testid="chat-message-text">
|
||||
<div
|
||||
className={hide && !isUser ? styles.hiden : ""}
|
||||
data-testid="chat-message-text"
|
||||
>
|
||||
{isLog && showLog && logs.length > 0 && (
|
||||
<div className="text-xs text-white p-2 rounded">
|
||||
<ReactMarkdown>{logs}</ReactMarkdown>
|
||||
|
@ -5,7 +5,7 @@
|
||||
.brain_name_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: Spacings.$spacing02;
|
||||
gap: Spacings.$spacing03;
|
||||
color: var(--text-3);
|
||||
overflow: hidden;
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.prompt_name_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-3);
|
||||
font-size: Typography.$small;
|
||||
overflow: hidden;
|
||||
|
||||
.prompt_name {
|
||||
@include Typography.EllipsisOverflow;
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import styles from "./QuestionPompt.module.scss";
|
||||
|
||||
type QuestionProptProps = {
|
||||
promptName?: string | null;
|
||||
};
|
||||
export const QuestionPrompt = ({
|
||||
promptName,
|
||||
}: QuestionProptProps): JSX.Element => {
|
||||
if (promptName === undefined || promptName === null) {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="prompt-tags" className={styles.prompt_name_wrapper}>
|
||||
<Icon name="hashtag" color="primary" size="small" />
|
||||
<span className={styles.prompt_name}>{promptName}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,41 +1,61 @@
|
||||
@use "@/styles/BoxShadow.module.scss";
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.source_wrapper {
|
||||
padding: Spacings.$spacing02;
|
||||
border-radius: Radius.$normal;
|
||||
border: 1px solid var(--border-1);
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
.source_and_citations_container {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
cursor: pointer;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing01;
|
||||
max-width: 240px;
|
||||
|
||||
&.selected_source {
|
||||
background-color: var(--primary-0);
|
||||
color: var(--text-0);
|
||||
border-color: var(--primary-0);
|
||||
}
|
||||
|
||||
.source_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: Spacings.$spacing03;
|
||||
font-size: Typography.$tiny;
|
||||
.source_wrapper {
|
||||
padding: Spacings.$spacing03;
|
||||
border-radius: Radius.$normal;
|
||||
border: 1px solid var(--border-1);
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
color: var(--text-2);
|
||||
max-width: 200px;
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
cursor: pointer;
|
||||
|
||||
.filename {
|
||||
@include Typography.EllipsisOverflow;
|
||||
.source_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: Spacings.$spacing03;
|
||||
font-size: Typography.$tiny;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
color: var(--text-2);
|
||||
max-width: 200px;
|
||||
|
||||
.filename {
|
||||
@include Typography.EllipsisOverflow;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-0);
|
||||
transition: border-color 0.3s ease;
|
||||
color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.selected_source) {
|
||||
background-color: var(--background-special-1);
|
||||
.citations_container {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.citation_index {
|
||||
cursor: pointer;
|
||||
color: var(--text-4);
|
||||
font-size: Typography.$tiny;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,81 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
import Tooltip from "@/lib/components/ui/Tooltip/Tooltip";
|
||||
|
||||
import styles from "./Source.module.scss";
|
||||
|
||||
import { SourceFile } from "../../types/types";
|
||||
import { CitationModal } from "../CitationModal/CitationModal";
|
||||
|
||||
type SourceProps = {
|
||||
sourceFile: SourceFile;
|
||||
isSelected: boolean;
|
||||
};
|
||||
export const SourceCitations = ({
|
||||
sourceFile,
|
||||
isSelected,
|
||||
}: SourceProps): JSX.Element => {
|
||||
export const SourceCitations = ({ sourceFile }: SourceProps): JSX.Element => {
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||
const [hovered, isHovered] = useState<boolean>(false);
|
||||
const [isCitationModalOpened, setIsCitationModalOpened] =
|
||||
useState<boolean>(false);
|
||||
const [citationIndex, setCitationIndex] = useState<number>(0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.source_wrapper} ${
|
||||
isSelected ? styles.selected_source : ""
|
||||
}`}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className={styles.source_header}>
|
||||
<span className={styles.filename}>{sourceFile.filename}</span>
|
||||
<div>
|
||||
<div className={styles.source_and_citations_container}>
|
||||
<div
|
||||
className={styles.source_wrapper}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
onMouseEnter={() => isHovered(true)}
|
||||
onMouseLeave={() => isHovered(false)}
|
||||
>
|
||||
<a
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
href={sourceFile.file_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className={styles.source_header}>
|
||||
<span className={styles.filename}>{sourceFile.filename}</span>
|
||||
<Icon
|
||||
name="externLink"
|
||||
size="small"
|
||||
color={hovered ? "primary" : "black"}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.citations_container}>
|
||||
{sourceFile.citations.map((citation, i) => (
|
||||
<div key={i}>
|
||||
<Tooltip
|
||||
tooltip={citation
|
||||
.split("Content:")
|
||||
.slice(1)
|
||||
.join("")
|
||||
.replace(/\n{3,}/g, "\n\n")}
|
||||
small={true}
|
||||
>
|
||||
<span
|
||||
className={styles.citation_index}
|
||||
onClick={() => {
|
||||
setIsCitationModalOpened(true);
|
||||
setCitationIndex(i);
|
||||
}}
|
||||
>
|
||||
[{i + 1}]
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{isCitationModalOpened && (
|
||||
<CitationModal
|
||||
citation={sourceFile.citations[citationIndex]}
|
||||
sourceFile={sourceFile}
|
||||
isModalOpened={isCitationModalOpened}
|
||||
setIsModalOpened={setIsCitationModalOpened}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1 +0,0 @@
|
||||
export * from "./MessageRow";
|
@ -1,2 +1 @@
|
||||
export * from "./QADisplay";
|
||||
export * from "./components/MessageRow/MessageRow";
|
||||
|
@ -22,6 +22,7 @@ export type ChatMessage = {
|
||||
metadata?: {
|
||||
sources?: Source[];
|
||||
thoughts?: string;
|
||||
followup_questions?: string[];
|
||||
};
|
||||
thumbs?: boolean;
|
||||
};
|
||||
|
@ -15,11 +15,12 @@
|
||||
|
||||
/* Grey */
|
||||
--grey-O: #707070;
|
||||
--grey-1: #c8c8c8;
|
||||
--grey-2: #cbcbcb;
|
||||
--grey-3: #e7e9ec;
|
||||
--grey-4: #d3d3d3;
|
||||
--grey-5: #f5f5f5;
|
||||
--grey-1: #818080;
|
||||
--grey-2: #c8c8c8;
|
||||
--grey-3: #cbcbcb;
|
||||
--grey-4: #e7e9ec;
|
||||
--grey-5: #d3d3d3;
|
||||
--grey-6: #f5f5f5;
|
||||
|
||||
/* Primary */
|
||||
--primary-0: #6142d4;
|
||||
|
@ -54,8 +54,8 @@ div:focus {
|
||||
/* Backgrounds */
|
||||
--background-0: var(--white-0);
|
||||
--background-1: var(--white-1);
|
||||
--background-2: var(--grey-5);
|
||||
--background-3: var(--grey-3);
|
||||
--background-2: var(--grey-6);
|
||||
--background-3: var(--grey-4);
|
||||
--background-4: var(--grey-0);
|
||||
--background-5: var(--black-0);
|
||||
--background-special-0: var(--primary-2);
|
||||
@ -63,21 +63,22 @@ div:focus {
|
||||
--background-blur: rgba(0, 0, 0, 0.9);
|
||||
|
||||
/* Borders */
|
||||
--border-0: var(--grey-4);
|
||||
--border-1: var(--grey-3);
|
||||
--border-2: var(--grey-1);
|
||||
--border-0: var(--grey-5);
|
||||
--border-1: var(--grey-4);
|
||||
--border-2: var(--grey-2);
|
||||
|
||||
/* Icons */
|
||||
--icon-0: var(--white-0);
|
||||
--icon-1: var(--grey-1);
|
||||
--icon-1: var(--grey-2);
|
||||
--icon-2: var(--grey-0);
|
||||
--icon-3: var(--black-0);
|
||||
|
||||
/* Text */
|
||||
--text-0: var(--white-0);
|
||||
--text-1: var(--grey-1);
|
||||
--text-1: var(--grey-2);
|
||||
--text-2: var(--grey-0);
|
||||
--text-3: var(--black-0);
|
||||
--text-4: var(--grey-1);
|
||||
|
||||
/* Box Shadow */
|
||||
--box-shadow: rgba(0, 0, 0, 0.25);
|
||||
@ -98,20 +99,21 @@ body.dark_mode {
|
||||
|
||||
/* Borders */
|
||||
--border-0: var(--white-0);
|
||||
--border-1: var(--grey-1);
|
||||
--border-2: var(--grey-2);
|
||||
--border-1: var(--grey-2);
|
||||
--border-2: var(--grey-3);
|
||||
|
||||
/* Icons */
|
||||
--icon-0: var(--black-0);
|
||||
--icon-1: var(--grey-0);
|
||||
--icon-2: var(--grey-1);
|
||||
--icon-2: var(--grey-2);
|
||||
--icon-3: var(--white-0);
|
||||
|
||||
/* Text */
|
||||
--text-0: var(--black-0);
|
||||
--text-1: var(--grey-0);
|
||||
--text-2: var(--grey-1);
|
||||
--text-2: var(--grey-2);
|
||||
--text-3: var(--white-2);
|
||||
--text-4: var(--grey-1);
|
||||
|
||||
/* Box Shadow */
|
||||
--box-shadow: rgba(255, 255, 255, 0.25);
|
||||
|
@ -37,15 +37,15 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iconRotate {
|
||||
.icon_rotate {
|
||||
transition: transform 0.3s Transitions.$easeOutBack;
|
||||
}
|
||||
|
||||
.iconRotateDown {
|
||||
.icon_rotate_down {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.iconRotateRight {
|
||||
.icon_rotate_right {
|
||||
transform: rotate(-90deg);
|
||||
|
||||
.label {
|
||||
|
@ -1,7 +1,7 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
@use "@/styles/Transitions.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.foldable_section_wrapper {
|
||||
display: flex;
|
||||
@ -53,15 +53,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.iconRotate {
|
||||
.icon_rotate {
|
||||
transition: transform 0.3s Transitions.$easeOutBack;
|
||||
}
|
||||
|
||||
.iconRotateDown {
|
||||
.icon_rotate_down {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.iconRotateRight {
|
||||
.icon_rotate_right {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
@ -43,8 +43,8 @@ export const FoldableSection = (props: FoldableSectionProps): JSX.Element => {
|
||||
name="chevronDown"
|
||||
size="normal"
|
||||
color="black"
|
||||
classname={`${styles.iconRotate} ${
|
||||
folded ? styles.iconRotateDown : styles.iconRotateRight
|
||||
classname={`${styles.icon_rotate} ${
|
||||
folded ? styles.icon_rotate_down : styles.icon_rotate_right
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,21 +1,21 @@
|
||||
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
import Tooltip from "@/lib/components/ui/Tooltip/Tooltip";
|
||||
import { IconSize } from "@/lib/types/Icons";
|
||||
|
||||
|
||||
type ThoughtsButtonProps = {
|
||||
text: string;
|
||||
size: IconSize;
|
||||
};
|
||||
|
||||
export const ThoughtsButton = ({ text, size }: ThoughtsButtonProps): JSX.Element => {
|
||||
|
||||
export const ThoughtsButton = ({
|
||||
text,
|
||||
size,
|
||||
}: ThoughtsButtonProps): JSX.Element => {
|
||||
return (
|
||||
<Tooltip tooltip={text}>
|
||||
<div>
|
||||
<Icon name="question" size={size} color="black" handleHover={true}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip tooltip={`Extra information\n\n${text}`}>
|
||||
<div>
|
||||
<Icon name="eureka" size={size} color="black" handleHover={true} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@ -12,4 +12,9 @@
|
||||
white-space: pre-wrap;
|
||||
overflow: hidden;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
&.small {
|
||||
max-height: 300px;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
@ -8,9 +8,10 @@ import styles from "./Tooltip.module.scss";
|
||||
interface TooltipProps {
|
||||
children?: ReactNode;
|
||||
tooltip?: ReactNode;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
const Tooltip = ({ children, tooltip }: TooltipProps): JSX.Element => {
|
||||
const Tooltip = ({ children, tooltip, small }: TooltipProps): JSX.Element => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@ -33,7 +34,9 @@ const Tooltip = ({ children, tooltip }: TooltipProps): JSX.Element => {
|
||||
opacity: 0,
|
||||
transition: { ease: "easeIn", duration: 0.1 },
|
||||
}}
|
||||
className={styles.tooltip_content_wrapper}
|
||||
className={`${styles.tooltip_content_wrapper} ${
|
||||
small ? styles.small : ""
|
||||
}`}
|
||||
>
|
||||
{tooltip}
|
||||
</motion.div>
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
} from "react-icons/fa";
|
||||
import { FaInfo } from "react-icons/fa6";
|
||||
import { FiUpload } from "react-icons/fi";
|
||||
import { GoLightBulb } from "react-icons/go";
|
||||
import { HiBuildingOffice } from "react-icons/hi2";
|
||||
import {
|
||||
IoIosAdd,
|
||||
@ -42,6 +43,7 @@ import {
|
||||
} from "react-icons/io";
|
||||
import {
|
||||
IoArrowUpCircleOutline,
|
||||
IoBookOutline,
|
||||
IoCloudDownloadOutline,
|
||||
IoFootsteps,
|
||||
IoHomeOutline,
|
||||
@ -57,6 +59,7 @@ import {
|
||||
LuChevronLeft,
|
||||
LuChevronRight,
|
||||
LuCopy,
|
||||
LuExternalLink,
|
||||
LuGoal,
|
||||
LuPlusCircle,
|
||||
LuSearch,
|
||||
@ -107,6 +110,8 @@ export const iconList: { [name: string]: IconType } = {
|
||||
download: IoCloudDownloadOutline,
|
||||
edit: MdOutlineModeEditOutline,
|
||||
email: MdAlternateEmail,
|
||||
eureka: GoLightBulb,
|
||||
externLink: LuExternalLink,
|
||||
feed: MdDynamicFeed,
|
||||
file: FaRegFileAlt,
|
||||
fileSelected: FaFileAlt,
|
||||
@ -142,6 +147,7 @@ export const iconList: { [name: string]: IconType } = {
|
||||
settings: IoMdSettings,
|
||||
share: IoShareSocial,
|
||||
software: CgSoftwareDownload,
|
||||
sources: IoBookOutline,
|
||||
star: FaRegStar,
|
||||
step: IoFootsteps,
|
||||
sun: MdOutlineLightMode,
|
||||
|
@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
@mixin H2 {
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
font-size: $large;
|
||||
}
|
||||
|
||||
@ -29,3 +29,5 @@ $tiny: 12px;
|
||||
$small: 14px;
|
||||
$medium: 16px;
|
||||
$large: 18px;
|
||||
$larger: 24px;
|
||||
$very_large: 28px;
|
||||
|
Loading…
Reference in New Issue
Block a user