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:
Antoine Dewez 2024-06-18 17:37:28 +02:00 committed by GitHub
parent b464ed3660
commit faaf9b6dba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 563 additions and 464 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
.brain_name_wrapper {
display: flex;
align-items: center;
gap: Spacings.$spacing02;
gap: Spacings.$spacing03;
color: var(--text-3);
overflow: hidden;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1 @@
export * from "./QADisplay";
export * from "./components/MessageRow/MessageRow";

View File

@ -22,6 +22,7 @@ export type ChatMessage = {
metadata?: {
sources?: Source[];
thoughts?: string;
followup_questions?: string[];
};
thumbs?: boolean;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,4 +12,9 @@
white-space: pre-wrap;
overflow: hidden;
max-width: 300px;
}
&.small {
max-height: 300px;
overflow: scroll;
}
}

View File

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

View File

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

View File

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